pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
140 lines (139 loc) • 6.11 kB
JavaScript
/**
* Tier 2 of the MCP path: drive pi-lens's *real* lifecycle handlers
* (`handleSessionStart`, `handleTurnEnd`) instead of re-implementing the runners
* they orchestrate. This is what exposes the layers the per-edit dispatch slice
* doesn't — session-start warms the dominant-language LSP (so warm `analyze` is
* LSP-complete), establishes the error-debt baseline + complexity baselines, and
* kicks off knip/jscpd/type-coverage/dep/secrets scans; turn-end runs
* knip/jscpd incrementally, dep-circular, tests, cascade, and the
* actionable/code-quality aggregation.
*
* The handlers don't return findings — they emit them through the same
* cache/context bridge pi consumes (`consume*` from runtime-context). We drive
* the handler, then consume that bridge and hand the text back as the tool result.
*
* The host coupling stays thin: a `getFlag` shim, no-op notify/dbg/log, a
* persistent RuntimeCoordinator + CacheManager (so baselines/turn-state survive
* across calls, like a real session), and the bootstrap client bundle.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { AstGrepClient } from "../ast-grep-client.js";
import { loadBootstrapClients } from "../bootstrap.js";
import { CacheManager } from "../cache-manager.js";
import { resetDispatchBaselines } from "../dispatch/integration.js";
import { resetFormatService } from "../format-service.js";
import { getLSPService, resetLSPService } from "../lsp/index.js";
import { consumeSessionStartGuidance, consumeTestFindings, consumeTurnEndFindings, } from "../runtime-context.js";
import { RuntimeCoordinator } from "../runtime-coordinator.js";
import { handleSessionStart } from "../runtime-session.js";
import { handleTurnEnd } from "../runtime-turn.js";
import { createMcpHost } from "./host-shim.js";
let contextPromise;
/** Lazily build (once) the persistent session context shared across MCP calls. */
export function getMcpSessionContext() {
contextPromise ??= (async () => ({
runtime: new RuntimeCoordinator(),
cacheManager: new CacheManager(),
astGrepClient: new AstGrepClient(),
clients: await loadBootstrapClients(),
}))();
return contextPromise;
}
/** Test hook — drop the cached context so a fresh one is built next call. */
export function _resetMcpSessionContext() {
contextPromise = undefined;
}
const noop = () => { };
function joinMessages(consumed) {
if (!consumed?.messages?.length)
return undefined;
return consumed.messages.map((message) => message.content).join("\n\n");
}
/**
* Run pi-lens's real session_start. Much of the work (scans, baseline, LSP warm)
* runs in the background, so the immediate return carries the synchronous
* guidance + whatever baseline/LSP state is ready; query `pilens_diagnostics` /
* `pilens_health` afterwards for the scan results as they land.
*/
export async function runSessionStart(cwd) {
const ctx = await getMcpSessionContext();
const host = createMcpHost();
await handleSessionStart({
ctxCwd: cwd,
getFlag: host.getFlag,
notify: noop,
dbg: noop,
log: noop,
runtime: ctx.runtime,
metricsClient: ctx.clients.metricsClient,
cacheManager: ctx.cacheManager,
todoScanner: ctx.clients.todoScanner,
astGrepClient: ctx.astGrepClient,
biomeClient: ctx.clients.biomeClient,
ruffClient: ctx.clients.ruffClient,
knipClient: ctx.clients.knipClient,
jscpdClient: ctx.clients.jscpdClient,
deadCodeClients: ctx.clients.deadCodeClients,
govulncheckClient: ctx.clients.govulncheckClient,
gitleaksClient: ctx.clients.gitleaksClient,
trivyClient: ctx.clients.trivyClient,
opengrepClient: ctx.clients.opengrepClient,
depChecker: ctx.clients.depChecker,
testRunnerClient: ctx.clients.testRunnerClient,
goClient: ctx.clients.goClient,
rustClient: ctx.clients.rustClient,
ensureTool: async (name) => (await import("../installer/index.js")).ensureTool(name),
cleanStaleTsBuildInfo: () => [],
resetDispatchBaselines: (cwdArg) => resetDispatchBaselines(cwdArg ?? cwd),
resetLSPService,
});
const baseline = ctx.runtime.errorDebtBaseline;
return {
guidance: joinMessages(consumeSessionStartGuidance(ctx.cacheManager, cwd)),
errorDebtBaseline: baseline
? { testsPassed: baseline.testsPassed, buildPassed: baseline.buildPassed }
: undefined,
aliveLspClients: getLSPService().getAliveClientCount(),
};
}
/**
* Run pi-lens's real turn_end over the files edited this "turn". The handler
* reads edited files from turn-state, so we register the caller-supplied files
* first (a full-file range, importsChanged=true so dep/knip re-check broadly).
*/
export async function runTurnEnd(cwd, files = []) {
const ctx = await getMcpSessionContext();
const host = createMcpHost();
let registered = 0;
for (const file of files) {
const abs = path.isAbsolute(file) ? file : path.resolve(cwd, file);
let lineCount = 1;
try {
lineCount = fs.readFileSync(abs, "utf8").split("\n").length;
}
catch {
continue; // unreadable / deleted — skip
}
ctx.cacheManager.addModifiedRange(abs, { start: 1, end: lineCount }, true, cwd, ctx.runtime.telemetrySessionId);
registered++;
}
await handleTurnEnd({
ctxCwd: cwd,
getFlag: host.getFlag,
dbg: noop,
runtime: ctx.runtime,
cacheManager: ctx.cacheManager,
knipClient: ctx.clients.knipClient,
deadCodeClients: ctx.clients.deadCodeClients,
depChecker: ctx.clients.depChecker,
testRunnerClient: ctx.clients.testRunnerClient,
resetLSPService,
resetFormatService,
});
return {
turnEnd: joinMessages(consumeTurnEndFindings(ctx.cacheManager, cwd)),
tests: joinMessages(consumeTestFindings(ctx.cacheManager, cwd)),
filesRegistered: registered,
};
}