pi-lens 3.3.0 ā 3.6.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/CHANGELOG.md +91 -0
- package/README.md +175 -13
- package/clients/cache/rule-cache.js +72 -0
- package/clients/cache/rule-cache.ts +104 -0
- package/clients/dispatch/integration.js +48 -1
- package/clients/dispatch/integration.ts +60 -2
- package/clients/dispatch/plan.js +5 -2
- package/clients/dispatch/plan.ts +5 -2
- package/clients/dispatch/runners/ast-grep-napi.js +175 -56
- package/clients/dispatch/runners/ast-grep-napi.test.js +2 -1
- package/clients/dispatch/runners/ast-grep-napi.test.ts +2 -1
- package/clients/dispatch/runners/ast-grep-napi.ts +191 -79
- package/clients/dispatch/runners/similarity.js +1 -1
- package/clients/dispatch/runners/similarity.ts +2 -2
- package/clients/dispatch/runners/tree-sitter.js +137 -10
- package/clients/dispatch/runners/tree-sitter.ts +168 -13
- package/clients/dispatch/runners/ts-lsp.js +3 -2
- package/clients/dispatch/runners/ts-lsp.ts +3 -2
- package/clients/dispatch/runners/yaml-rule-parser.js +70 -2
- package/clients/dispatch/runners/yaml-rule-parser.ts +71 -2
- package/clients/dispatch/types.js +1 -1
- package/clients/dispatch/types.ts +1 -1
- package/clients/lsp/__tests__/service.test.js +3 -0
- package/clients/lsp/__tests__/service.test.ts +3 -0
- package/clients/lsp/client.js +42 -0
- package/clients/lsp/client.ts +79 -0
- package/clients/lsp/index.js +27 -0
- package/clients/lsp/index.ts +35 -0
- package/clients/lsp/launch.js +11 -6
- package/clients/lsp/launch.ts +11 -6
- package/clients/metrics-client.js +3 -160
- package/clients/metrics-client.tdr.test.js +78 -0
- package/clients/metrics-client.test.js +30 -43
- package/clients/metrics-client.test.ts +30 -54
- package/clients/metrics-client.ts +5 -219
- package/clients/metrics-history.js +33 -7
- package/clients/metrics-history.ts +47 -10
- package/clients/pipeline.js +272 -0
- package/clients/pipeline.ts +371 -0
- package/clients/sg-runner.js +21 -3
- package/clients/sg-runner.ts +22 -3
- package/clients/tree-sitter-client.js +23 -2
- package/clients/tree-sitter-client.ts +27 -2
- package/index.ts +604 -771
- package/package.json +1 -1
- package/rules/ast-grep-rules/rules/no-architecture-violation.yml +7 -4
- package/rules/ast-grep-rules/rules/no-single-char-var.yml +3 -3
- package/rules/ast-grep-rules/slop-patterns.yml +85 -62
- package/skills/ast-grep/SKILL.md +42 -1
- package/skills/lsp-navigation/SKILL.md +62 -0
- package/tsconfig.json +1 -1
- package/rules/ast-grep-rules/rules/no-console-log.yml +0 -10
- package/rules/ast-grep-rules/rules/no-default-export.yml +0 -19
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-write pipeline for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts tool_result handler.
|
|
5
|
+
* Runs sequentially on every file write/edit:
|
|
6
|
+
* 1. Secrets scan (blocking ā early exit)
|
|
7
|
+
* 2. Auto-format (Biome, Prettier, Ruff, gofmt, etc.)
|
|
8
|
+
* 3. Auto-fix (Biome --write, Ruff --fix)
|
|
9
|
+
* 4. LSP file sync (open/update in LSP servers)
|
|
10
|
+
* 5. Dispatch lint (type errors, security rules)
|
|
11
|
+
* 6. Test runner (run corresponding test file)
|
|
12
|
+
* 7. Cascade diagnostics (other files with errors, LSP only)
|
|
13
|
+
*/
|
|
14
|
+
import * as nodeFs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { dispatchLintWithResult } from "./dispatch/integration.js";
|
|
17
|
+
import { logLatency } from "./latency-logger.js";
|
|
18
|
+
import { getLSPService } from "./lsp/index.js";
|
|
19
|
+
import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
|
|
20
|
+
function createPhaseTracker(toolName, filePath) {
|
|
21
|
+
const phases = [];
|
|
22
|
+
return {
|
|
23
|
+
start(name) {
|
|
24
|
+
phases.push({ name, startTime: Date.now(), ended: false });
|
|
25
|
+
},
|
|
26
|
+
end(name, metadata) {
|
|
27
|
+
const p = phases.find((x) => x.name === name && !x.ended);
|
|
28
|
+
if (p) {
|
|
29
|
+
p.ended = true;
|
|
30
|
+
logLatency({
|
|
31
|
+
type: "phase",
|
|
32
|
+
toolName,
|
|
33
|
+
filePath,
|
|
34
|
+
phase: name,
|
|
35
|
+
durationMs: Date.now() - p.startTime,
|
|
36
|
+
metadata,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// --- Main Pipeline ---
|
|
43
|
+
export async function runPipeline(ctx, deps) {
|
|
44
|
+
const { filePath, cwd, toolName, getFlag, dbg } = ctx;
|
|
45
|
+
const { biomeClient, ruffClient, testRunnerClient, metricsClient, getFormatService, fixedThisTurn, } = deps;
|
|
46
|
+
const phase = createPhaseTracker(toolName, filePath);
|
|
47
|
+
const pipelineStart = Date.now();
|
|
48
|
+
phase.start("total");
|
|
49
|
+
// --- Read file content ---
|
|
50
|
+
phase.start("read_file");
|
|
51
|
+
let fileContent;
|
|
52
|
+
try {
|
|
53
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// File may not exist (e.g., deleted)
|
|
57
|
+
}
|
|
58
|
+
phase.end("read_file");
|
|
59
|
+
// --- 1. Secrets scan (blocking ā early exit) ---
|
|
60
|
+
if (fileContent) {
|
|
61
|
+
const secretFindings = scanForSecrets(fileContent, filePath);
|
|
62
|
+
if (secretFindings.length > 0) {
|
|
63
|
+
const secretsOutput = formatSecrets(secretFindings, filePath);
|
|
64
|
+
logLatency({
|
|
65
|
+
type: "tool_result",
|
|
66
|
+
toolName,
|
|
67
|
+
filePath,
|
|
68
|
+
durationMs: Date.now() - pipelineStart,
|
|
69
|
+
result: "blocked_secrets",
|
|
70
|
+
metadata: { secretsFound: secretFindings.length },
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
output: `\n\n${secretsOutput}`,
|
|
74
|
+
isError: true,
|
|
75
|
+
fileModified: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// --- 2. Auto-format ---
|
|
80
|
+
phase.start("format");
|
|
81
|
+
let formatChanged = false;
|
|
82
|
+
let formattersUsed = [];
|
|
83
|
+
if (!getFlag("no-autoformat") && fileContent) {
|
|
84
|
+
const formatService = getFormatService();
|
|
85
|
+
try {
|
|
86
|
+
formatService.recordRead(filePath);
|
|
87
|
+
const result = await formatService.formatFile(filePath);
|
|
88
|
+
formattersUsed = result.formatters.map((f) => f.name);
|
|
89
|
+
if (result.anyChanged) {
|
|
90
|
+
formatChanged = true;
|
|
91
|
+
dbg(`autoformat: ${result.formatters.map((f) => `${f.name}(${f.changed ? "changed" : "unchanged"})`).join(", ")}`);
|
|
92
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
dbg(`autoformat error: ${err}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
phase.end("format", { formattersUsed, formatChanged });
|
|
100
|
+
// --- 3. LSP file sync ---
|
|
101
|
+
if (getFlag("lens-lsp") && fileContent) {
|
|
102
|
+
const lspService = getLSPService();
|
|
103
|
+
lspService
|
|
104
|
+
.hasLSP(filePath)
|
|
105
|
+
.then(async (hasLSP) => {
|
|
106
|
+
if (hasLSP) {
|
|
107
|
+
if (toolName === "write") {
|
|
108
|
+
await lspService.openFile(filePath, fileContent);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
await lspService.updateFile(filePath, fileContent);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
.catch((err) => {
|
|
116
|
+
dbg(`LSP error: ${err}`);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
let output = "";
|
|
120
|
+
// --- 4. Auto-fix ---
|
|
121
|
+
phase.start("autofix");
|
|
122
|
+
const noAutofix = getFlag("no-autofix");
|
|
123
|
+
const noAutofixBiome = getFlag("no-autofix-biome");
|
|
124
|
+
const noAutofixRuff = getFlag("no-autofix-ruff");
|
|
125
|
+
let fixedCount = 0;
|
|
126
|
+
if (!fixedThisTurn.has(filePath) && !noAutofix) {
|
|
127
|
+
if (!noAutofixRuff &&
|
|
128
|
+
(await ruffClient.ensureAvailable()) &&
|
|
129
|
+
ruffClient.isPythonFile(filePath)) {
|
|
130
|
+
const result = ruffClient.fixFile(filePath);
|
|
131
|
+
if (result.success && result.fixed > 0) {
|
|
132
|
+
fixedCount += result.fixed;
|
|
133
|
+
fixedThisTurn.add(filePath);
|
|
134
|
+
dbg(`autofix: ruff fixed ${result.fixed} issue(s) in ${filePath}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!noAutofixBiome &&
|
|
138
|
+
biomeClient.isAvailable() &&
|
|
139
|
+
biomeClient.isSupportedFile(filePath)) {
|
|
140
|
+
const result = biomeClient.fixFile(filePath);
|
|
141
|
+
if (result.success && result.fixed > 0) {
|
|
142
|
+
fixedCount += result.fixed;
|
|
143
|
+
fixedThisTurn.add(filePath);
|
|
144
|
+
dbg(`autofix: biome fixed ${result.fixed} issue(s) in ${filePath}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
phase.end("autofix", { fixedCount, tools: ["ruff", "biome"] });
|
|
149
|
+
// --- 5. Dispatch lint ---
|
|
150
|
+
phase.start("dispatch_lint");
|
|
151
|
+
dbg(`dispatch: running lint tools for ${filePath}`);
|
|
152
|
+
const piApi = {
|
|
153
|
+
getFlag: getFlag,
|
|
154
|
+
};
|
|
155
|
+
const dispatchResult = await dispatchLintWithResult(filePath, cwd, piApi);
|
|
156
|
+
if (dispatchResult.output) {
|
|
157
|
+
output += `\n\n${dispatchResult.output}`;
|
|
158
|
+
}
|
|
159
|
+
if (fixedCount > 0) {
|
|
160
|
+
output += `\n\nā
Auto-fixed ${fixedCount} issue(s) in ${path.basename(filePath)}`;
|
|
161
|
+
}
|
|
162
|
+
if (formatChanged || fixedCount > 0) {
|
|
163
|
+
output += `\n\nā ļø **File modified by auto-format/fix. Re-read before next edit.**`;
|
|
164
|
+
}
|
|
165
|
+
phase.end("dispatch_lint", {
|
|
166
|
+
hasOutput: !!dispatchResult.output,
|
|
167
|
+
diagnosticCount: dispatchResult.diagnostics.length,
|
|
168
|
+
});
|
|
169
|
+
// --- 6. Test runner ---
|
|
170
|
+
phase.start("test_runner");
|
|
171
|
+
let testInfoFound = false;
|
|
172
|
+
let testRunnerRan = false;
|
|
173
|
+
if (!getFlag("no-tests")) {
|
|
174
|
+
const testInfo = testRunnerClient.findTestFile(filePath, cwd);
|
|
175
|
+
testInfoFound = !!testInfo;
|
|
176
|
+
if (testInfo) {
|
|
177
|
+
dbg(`test-runner: found test file ${testInfo.testFile} for ${filePath}`);
|
|
178
|
+
const detectedRunner = testRunnerClient.detectRunner(cwd);
|
|
179
|
+
if (detectedRunner) {
|
|
180
|
+
testRunnerRan = true;
|
|
181
|
+
const testStart = Date.now();
|
|
182
|
+
const testResult = testRunnerClient.runTestFile(testInfo.testFile, cwd, detectedRunner.runner, detectedRunner.config);
|
|
183
|
+
const testDuration = Date.now() - testStart;
|
|
184
|
+
logLatency({
|
|
185
|
+
type: "phase",
|
|
186
|
+
toolName,
|
|
187
|
+
filePath,
|
|
188
|
+
phase: "test_runner",
|
|
189
|
+
durationMs: testDuration,
|
|
190
|
+
metadata: {
|
|
191
|
+
testFile: testInfo.testFile,
|
|
192
|
+
runner: detectedRunner.runner,
|
|
193
|
+
success: !testResult?.error,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
if (testResult && !testResult.error) {
|
|
197
|
+
const testOutput = testRunnerClient.formatResult(testResult);
|
|
198
|
+
if (testOutput) {
|
|
199
|
+
output += `\n\n${testOutput}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
phase.end("test_runner", { found: testInfoFound, ran: testRunnerRan });
|
|
206
|
+
// --- 7. Cascade diagnostics (LSP only) ---
|
|
207
|
+
if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
|
|
208
|
+
const MAX_CASCADE_FILES = 5;
|
|
209
|
+
const MAX_DIAGNOSTICS_PER_FILE = 20;
|
|
210
|
+
const cascadeStart = Date.now();
|
|
211
|
+
try {
|
|
212
|
+
const lspService = getLSPService();
|
|
213
|
+
const allDiags = await lspService.getAllDiagnostics();
|
|
214
|
+
const normalizedEditedPath = path.resolve(filePath);
|
|
215
|
+
const otherFileErrors = [];
|
|
216
|
+
for (const [diagPath, diags] of allDiags) {
|
|
217
|
+
if (path.resolve(diagPath) === normalizedEditedPath)
|
|
218
|
+
continue;
|
|
219
|
+
const errors = diags.filter((d) => d.severity === 1);
|
|
220
|
+
if (errors.length > 0) {
|
|
221
|
+
otherFileErrors.push({ file: diagPath, errors });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (otherFileErrors.length > 0) {
|
|
225
|
+
output += `\n\nš Cascade errors detected in ${otherFileErrors.length} other file(s):`;
|
|
226
|
+
for (const { file, errors } of otherFileErrors.slice(0, MAX_CASCADE_FILES)) {
|
|
227
|
+
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE);
|
|
228
|
+
const suffix = errors.length > MAX_DIAGNOSTICS_PER_FILE
|
|
229
|
+
? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more`
|
|
230
|
+
: "";
|
|
231
|
+
output += `\n<diagnostics file="${file}">`;
|
|
232
|
+
for (const e of limited) {
|
|
233
|
+
const line = (e.range?.start?.line ?? 0) + 1;
|
|
234
|
+
const col = (e.range?.start?.character ?? 0) + 1;
|
|
235
|
+
const code = e.code ? ` [${e.code}]` : "";
|
|
236
|
+
output += `\n ${code} (${line}:${col}) ${e.message.split("\n")[0].slice(0, 100)}`;
|
|
237
|
+
}
|
|
238
|
+
output += `${suffix}\n</diagnostics>`;
|
|
239
|
+
}
|
|
240
|
+
if (otherFileErrors.length > MAX_CASCADE_FILES) {
|
|
241
|
+
output += `\n... and ${otherFileErrors.length - MAX_CASCADE_FILES} more files with errors`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
logLatency({
|
|
245
|
+
type: "phase",
|
|
246
|
+
toolName,
|
|
247
|
+
filePath,
|
|
248
|
+
phase: "cascade_diagnostics",
|
|
249
|
+
durationMs: Date.now() - cascadeStart,
|
|
250
|
+
metadata: { filesWithErrors: otherFileErrors.length },
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
dbg(`cascade diagnostics error: ${err}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// --- Final timing ---
|
|
258
|
+
const elapsed = Date.now() - pipelineStart;
|
|
259
|
+
phase.end("total", { hasOutput: !!output });
|
|
260
|
+
logLatency({
|
|
261
|
+
type: "tool_result",
|
|
262
|
+
toolName,
|
|
263
|
+
filePath,
|
|
264
|
+
durationMs: elapsed,
|
|
265
|
+
result: output ? "completed" : "no_output",
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
output,
|
|
269
|
+
isError: false,
|
|
270
|
+
fileModified: formatChanged || fixedCount > 0,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-write pipeline for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts tool_result handler.
|
|
5
|
+
* Runs sequentially on every file write/edit:
|
|
6
|
+
* 1. Secrets scan (blocking ā early exit)
|
|
7
|
+
* 2. Auto-format (Biome, Prettier, Ruff, gofmt, etc.)
|
|
8
|
+
* 3. Auto-fix (Biome --write, Ruff --fix)
|
|
9
|
+
* 4. LSP file sync (open/update in LSP servers)
|
|
10
|
+
* 5. Dispatch lint (type errors, security rules)
|
|
11
|
+
* 6. Test runner (run corresponding test file)
|
|
12
|
+
* 7. Cascade diagnostics (other files with errors, LSP only)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as nodeFs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import type { BiomeClient } from "./biome-client.js";
|
|
18
|
+
import { dispatchLintWithResult } from "./dispatch/integration.js";
|
|
19
|
+
import type { PiAgentAPI } from "./dispatch/types.js";
|
|
20
|
+
import type { FormatService } from "./format-service.js";
|
|
21
|
+
import { logLatency } from "./latency-logger.js";
|
|
22
|
+
import { getLSPService } from "./lsp/index.js";
|
|
23
|
+
import type { MetricsClient } from "./metrics-client.js";
|
|
24
|
+
import type { RuffClient } from "./ruff-client.js";
|
|
25
|
+
import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
|
|
26
|
+
import type { TestRunnerClient } from "./test-runner-client.js";
|
|
27
|
+
|
|
28
|
+
// --- Types ---
|
|
29
|
+
|
|
30
|
+
export interface PipelineContext {
|
|
31
|
+
filePath: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
toolName: string;
|
|
34
|
+
/** pi.getFlag accessor */
|
|
35
|
+
getFlag: (name: string) => boolean | string | undefined;
|
|
36
|
+
/** Debug logger */
|
|
37
|
+
dbg: (msg: string) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PipelineDeps {
|
|
41
|
+
biomeClient: BiomeClient;
|
|
42
|
+
ruffClient: RuffClient;
|
|
43
|
+
testRunnerClient: TestRunnerClient;
|
|
44
|
+
metricsClient: MetricsClient;
|
|
45
|
+
getFormatService: () => FormatService;
|
|
46
|
+
fixedThisTurn: Set<string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PipelineResult {
|
|
50
|
+
/** Text to append to tool_result content */
|
|
51
|
+
output: string;
|
|
52
|
+
/** True if secrets found ā block the agent */
|
|
53
|
+
isError: boolean;
|
|
54
|
+
/** True if file was modified by format/autofix */
|
|
55
|
+
fileModified: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Phase timing helpers ---
|
|
59
|
+
|
|
60
|
+
interface PhaseTracker {
|
|
61
|
+
start(name: string): void;
|
|
62
|
+
end(name: string, metadata?: Record<string, unknown>): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createPhaseTracker(toolName: string, filePath: string): PhaseTracker {
|
|
66
|
+
const phases: Array<{
|
|
67
|
+
name: string;
|
|
68
|
+
startTime: number;
|
|
69
|
+
ended: boolean;
|
|
70
|
+
}> = [];
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
start(name: string) {
|
|
74
|
+
phases.push({ name, startTime: Date.now(), ended: false });
|
|
75
|
+
},
|
|
76
|
+
end(name: string, metadata?: Record<string, unknown>) {
|
|
77
|
+
const p = phases.find((x) => x.name === name && !x.ended);
|
|
78
|
+
if (p) {
|
|
79
|
+
p.ended = true;
|
|
80
|
+
logLatency({
|
|
81
|
+
type: "phase",
|
|
82
|
+
toolName,
|
|
83
|
+
filePath,
|
|
84
|
+
phase: name,
|
|
85
|
+
durationMs: Date.now() - p.startTime,
|
|
86
|
+
metadata,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Main Pipeline ---
|
|
94
|
+
|
|
95
|
+
export async function runPipeline(
|
|
96
|
+
ctx: PipelineContext,
|
|
97
|
+
deps: PipelineDeps,
|
|
98
|
+
): Promise<PipelineResult> {
|
|
99
|
+
const { filePath, cwd, toolName, getFlag, dbg } = ctx;
|
|
100
|
+
const {
|
|
101
|
+
biomeClient,
|
|
102
|
+
ruffClient,
|
|
103
|
+
testRunnerClient,
|
|
104
|
+
metricsClient,
|
|
105
|
+
getFormatService,
|
|
106
|
+
fixedThisTurn,
|
|
107
|
+
} = deps;
|
|
108
|
+
|
|
109
|
+
const phase = createPhaseTracker(toolName, filePath);
|
|
110
|
+
const pipelineStart = Date.now();
|
|
111
|
+
phase.start("total");
|
|
112
|
+
|
|
113
|
+
// --- Read file content ---
|
|
114
|
+
phase.start("read_file");
|
|
115
|
+
let fileContent: string | undefined;
|
|
116
|
+
try {
|
|
117
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
118
|
+
} catch {
|
|
119
|
+
// File may not exist (e.g., deleted)
|
|
120
|
+
}
|
|
121
|
+
phase.end("read_file");
|
|
122
|
+
|
|
123
|
+
// --- 1. Secrets scan (blocking ā early exit) ---
|
|
124
|
+
if (fileContent) {
|
|
125
|
+
const secretFindings = scanForSecrets(fileContent, filePath);
|
|
126
|
+
if (secretFindings.length > 0) {
|
|
127
|
+
const secretsOutput = formatSecrets(secretFindings, filePath);
|
|
128
|
+
logLatency({
|
|
129
|
+
type: "tool_result",
|
|
130
|
+
toolName,
|
|
131
|
+
filePath,
|
|
132
|
+
durationMs: Date.now() - pipelineStart,
|
|
133
|
+
result: "blocked_secrets",
|
|
134
|
+
metadata: { secretsFound: secretFindings.length },
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
output: `\n\n${secretsOutput}`,
|
|
138
|
+
isError: true,
|
|
139
|
+
fileModified: false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- 2. Auto-format ---
|
|
145
|
+
phase.start("format");
|
|
146
|
+
let formatChanged = false;
|
|
147
|
+
let formattersUsed: string[] = [];
|
|
148
|
+
if (!getFlag("no-autoformat") && fileContent) {
|
|
149
|
+
const formatService = getFormatService();
|
|
150
|
+
try {
|
|
151
|
+
formatService.recordRead(filePath);
|
|
152
|
+
const result = await formatService.formatFile(filePath);
|
|
153
|
+
formattersUsed = result.formatters.map((f) => f.name);
|
|
154
|
+
if (result.anyChanged) {
|
|
155
|
+
formatChanged = true;
|
|
156
|
+
dbg(
|
|
157
|
+
`autoformat: ${result.formatters.map((f) => `${f.name}(${f.changed ? "changed" : "unchanged"})`).join(", ")}`,
|
|
158
|
+
);
|
|
159
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
dbg(`autoformat error: ${err}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
phase.end("format", { formattersUsed, formatChanged });
|
|
166
|
+
|
|
167
|
+
// --- 3. LSP file sync ---
|
|
168
|
+
if (getFlag("lens-lsp") && fileContent) {
|
|
169
|
+
const lspService = getLSPService();
|
|
170
|
+
lspService
|
|
171
|
+
.hasLSP(filePath)
|
|
172
|
+
.then(async (hasLSP) => {
|
|
173
|
+
if (hasLSP) {
|
|
174
|
+
if (toolName === "write") {
|
|
175
|
+
await lspService.openFile(filePath, fileContent);
|
|
176
|
+
} else {
|
|
177
|
+
await lspService.updateFile(filePath, fileContent);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
.catch((err) => {
|
|
182
|
+
dbg(`LSP error: ${err}`);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let output = "";
|
|
187
|
+
|
|
188
|
+
// --- 4. Auto-fix ---
|
|
189
|
+
phase.start("autofix");
|
|
190
|
+
const noAutofix = getFlag("no-autofix");
|
|
191
|
+
const noAutofixBiome = getFlag("no-autofix-biome");
|
|
192
|
+
const noAutofixRuff = getFlag("no-autofix-ruff");
|
|
193
|
+
let fixedCount = 0;
|
|
194
|
+
|
|
195
|
+
if (!fixedThisTurn.has(filePath) && !noAutofix) {
|
|
196
|
+
if (
|
|
197
|
+
!noAutofixRuff &&
|
|
198
|
+
(await ruffClient.ensureAvailable()) &&
|
|
199
|
+
ruffClient.isPythonFile(filePath)
|
|
200
|
+
) {
|
|
201
|
+
const result = ruffClient.fixFile(filePath);
|
|
202
|
+
if (result.success && result.fixed > 0) {
|
|
203
|
+
fixedCount += result.fixed;
|
|
204
|
+
fixedThisTurn.add(filePath);
|
|
205
|
+
dbg(`autofix: ruff fixed ${result.fixed} issue(s) in ${filePath}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (
|
|
210
|
+
!noAutofixBiome &&
|
|
211
|
+
biomeClient.isAvailable() &&
|
|
212
|
+
biomeClient.isSupportedFile(filePath)
|
|
213
|
+
) {
|
|
214
|
+
const result = biomeClient.fixFile(filePath);
|
|
215
|
+
if (result.success && result.fixed > 0) {
|
|
216
|
+
fixedCount += result.fixed;
|
|
217
|
+
fixedThisTurn.add(filePath);
|
|
218
|
+
dbg(`autofix: biome fixed ${result.fixed} issue(s) in ${filePath}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
phase.end("autofix", { fixedCount, tools: ["ruff", "biome"] });
|
|
223
|
+
|
|
224
|
+
// --- 5. Dispatch lint ---
|
|
225
|
+
phase.start("dispatch_lint");
|
|
226
|
+
dbg(`dispatch: running lint tools for ${filePath}`);
|
|
227
|
+
|
|
228
|
+
const piApi: PiAgentAPI = {
|
|
229
|
+
getFlag: getFlag as (flag: string) => boolean | string | undefined,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const dispatchResult = await dispatchLintWithResult(filePath, cwd, piApi);
|
|
233
|
+
|
|
234
|
+
if (dispatchResult.output) {
|
|
235
|
+
output += `\n\n${dispatchResult.output}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (fixedCount > 0) {
|
|
239
|
+
output += `\n\nā
Auto-fixed ${fixedCount} issue(s) in ${path.basename(filePath)}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (formatChanged || fixedCount > 0) {
|
|
243
|
+
output += `\n\nā ļø **File modified by auto-format/fix. Re-read before next edit.**`;
|
|
244
|
+
}
|
|
245
|
+
phase.end("dispatch_lint", {
|
|
246
|
+
hasOutput: !!dispatchResult.output,
|
|
247
|
+
diagnosticCount: dispatchResult.diagnostics.length,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// --- 6. Test runner ---
|
|
251
|
+
phase.start("test_runner");
|
|
252
|
+
let testInfoFound = false;
|
|
253
|
+
let testRunnerRan = false;
|
|
254
|
+
if (!getFlag("no-tests")) {
|
|
255
|
+
const testInfo = testRunnerClient.findTestFile(filePath, cwd);
|
|
256
|
+
testInfoFound = !!testInfo;
|
|
257
|
+
if (testInfo) {
|
|
258
|
+
dbg(`test-runner: found test file ${testInfo.testFile} for ${filePath}`);
|
|
259
|
+
const detectedRunner = testRunnerClient.detectRunner(cwd);
|
|
260
|
+
if (detectedRunner) {
|
|
261
|
+
testRunnerRan = true;
|
|
262
|
+
const testStart = Date.now();
|
|
263
|
+
const testResult = testRunnerClient.runTestFile(
|
|
264
|
+
testInfo.testFile,
|
|
265
|
+
cwd,
|
|
266
|
+
detectedRunner.runner,
|
|
267
|
+
detectedRunner.config,
|
|
268
|
+
);
|
|
269
|
+
const testDuration = Date.now() - testStart;
|
|
270
|
+
logLatency({
|
|
271
|
+
type: "phase",
|
|
272
|
+
toolName,
|
|
273
|
+
filePath,
|
|
274
|
+
phase: "test_runner",
|
|
275
|
+
durationMs: testDuration,
|
|
276
|
+
metadata: {
|
|
277
|
+
testFile: testInfo.testFile,
|
|
278
|
+
runner: detectedRunner.runner,
|
|
279
|
+
success: !testResult?.error,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
if (testResult && !testResult.error) {
|
|
283
|
+
const testOutput = testRunnerClient.formatResult(testResult);
|
|
284
|
+
if (testOutput) {
|
|
285
|
+
output += `\n\n${testOutput}`;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
phase.end("test_runner", { found: testInfoFound, ran: testRunnerRan });
|
|
292
|
+
|
|
293
|
+
// --- 7. Cascade diagnostics (LSP only) ---
|
|
294
|
+
if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
|
|
295
|
+
const MAX_CASCADE_FILES = 5;
|
|
296
|
+
const MAX_DIAGNOSTICS_PER_FILE = 20;
|
|
297
|
+
const cascadeStart = Date.now();
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const lspService = getLSPService();
|
|
301
|
+
const allDiags = await lspService.getAllDiagnostics();
|
|
302
|
+
const normalizedEditedPath = path.resolve(filePath);
|
|
303
|
+
const otherFileErrors: Array<{
|
|
304
|
+
file: string;
|
|
305
|
+
errors: import("./lsp/client.js").LSPDiagnostic[];
|
|
306
|
+
}> = [];
|
|
307
|
+
|
|
308
|
+
for (const [diagPath, diags] of allDiags) {
|
|
309
|
+
if (path.resolve(diagPath) === normalizedEditedPath) continue;
|
|
310
|
+
const errors = diags.filter((d) => d.severity === 1);
|
|
311
|
+
if (errors.length > 0) {
|
|
312
|
+
otherFileErrors.push({ file: diagPath, errors });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (otherFileErrors.length > 0) {
|
|
317
|
+
output += `\n\nš Cascade errors detected in ${otherFileErrors.length} other file(s):`;
|
|
318
|
+
for (const { file, errors } of otherFileErrors.slice(
|
|
319
|
+
0,
|
|
320
|
+
MAX_CASCADE_FILES,
|
|
321
|
+
)) {
|
|
322
|
+
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE);
|
|
323
|
+
const suffix =
|
|
324
|
+
errors.length > MAX_DIAGNOSTICS_PER_FILE
|
|
325
|
+
? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more`
|
|
326
|
+
: "";
|
|
327
|
+
output += `\n<diagnostics file="${file}">`;
|
|
328
|
+
for (const e of limited) {
|
|
329
|
+
const line = (e.range?.start?.line ?? 0) + 1;
|
|
330
|
+
const col = (e.range?.start?.character ?? 0) + 1;
|
|
331
|
+
const code = e.code ? ` [${e.code}]` : "";
|
|
332
|
+
output += `\n ${code} (${line}:${col}) ${e.message.split("\n")[0].slice(0, 100)}`;
|
|
333
|
+
}
|
|
334
|
+
output += `${suffix}\n</diagnostics>`;
|
|
335
|
+
}
|
|
336
|
+
if (otherFileErrors.length > MAX_CASCADE_FILES) {
|
|
337
|
+
output += `\n... and ${otherFileErrors.length - MAX_CASCADE_FILES} more files with errors`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
logLatency({
|
|
342
|
+
type: "phase",
|
|
343
|
+
toolName,
|
|
344
|
+
filePath,
|
|
345
|
+
phase: "cascade_diagnostics",
|
|
346
|
+
durationMs: Date.now() - cascadeStart,
|
|
347
|
+
metadata: { filesWithErrors: otherFileErrors.length },
|
|
348
|
+
});
|
|
349
|
+
} catch (err) {
|
|
350
|
+
dbg(`cascade diagnostics error: ${err}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// --- Final timing ---
|
|
355
|
+
const elapsed = Date.now() - pipelineStart;
|
|
356
|
+
phase.end("total", { hasOutput: !!output });
|
|
357
|
+
|
|
358
|
+
logLatency({
|
|
359
|
+
type: "tool_result",
|
|
360
|
+
toolName,
|
|
361
|
+
filePath,
|
|
362
|
+
durationMs: elapsed,
|
|
363
|
+
result: output ? "completed" : "no_output",
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
output,
|
|
368
|
+
isError: false,
|
|
369
|
+
fileModified: formatChanged || fixedCount > 0,
|
|
370
|
+
};
|
|
371
|
+
}
|
package/clients/sg-runner.js
CHANGED
|
@@ -139,11 +139,29 @@ export class SgRunner {
|
|
|
139
139
|
});
|
|
140
140
|
proc.on("close", (code) => {
|
|
141
141
|
if (code !== 0 && !stdout.trim()) {
|
|
142
|
+
// Enhanced error messages for common pattern issues
|
|
143
|
+
let errorMsg = stderr.trim() || `Exit code ${code}`;
|
|
144
|
+
if (stderr.includes("Multiple AST nodes are detected")) {
|
|
145
|
+
errorMsg =
|
|
146
|
+
`Invalid AST pattern: The pattern appears to contain multiple AST nodes or is malformed.\n` +
|
|
147
|
+
`Common causes:\n` +
|
|
148
|
+
` 1. Missing parentheses: use it($TEST) not it"test"\n` +
|
|
149
|
+
` 2. Raw text without structure: use console.log($MSG) not just "console.log"\n` +
|
|
150
|
+
` 3. Unclosed quotes or brackets\n\n` +
|
|
151
|
+
`Original error: ${errorMsg}`;
|
|
152
|
+
}
|
|
153
|
+
else if (stderr.includes("Cannot parse query")) {
|
|
154
|
+
errorMsg =
|
|
155
|
+
`Pattern syntax error: The pattern could not be parsed as valid code.\n` +
|
|
156
|
+
`Tips:\n` +
|
|
157
|
+
` - Patterns must be valid ${args.includes("--lang") ? args[args.indexOf("--lang") + 1] : "language"} syntax\n` +
|
|
158
|
+
` - Use metavariables like $NAME, $ARGS for variable parts\n` +
|
|
159
|
+
` - Example: 'function $NAME($$$PARAMS) { $$$BODY }'\n\n` +
|
|
160
|
+
`Original error: ${errorMsg}`;
|
|
161
|
+
}
|
|
142
162
|
resolve({
|
|
143
163
|
matches: [],
|
|
144
|
-
error: stderr.includes("No files found")
|
|
145
|
-
? undefined
|
|
146
|
-
: stderr.trim() || `Exit code ${code}`,
|
|
164
|
+
error: stderr.includes("No files found") ? undefined : errorMsg,
|
|
147
165
|
});
|
|
148
166
|
return;
|
|
149
167
|
}
|