Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ on:
push:
branches: [main]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm ci --ignore-scripts
- run: npm run ci
40 changes: 31 additions & 9 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,53 @@ on:
concurrency: release

permissions:
contents: write
pull-requests: write
id-token: write
contents: read

jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 22
cache: npm

- run: node --version && npm --version
- run: npm ci --ignore-scripts
- run: npm run ci

release:
runs-on: ubuntu-latest
timeout-minutes: 20
needs: verify
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
fetch-depth: 0
persist-credentials: false

- uses: actions/setup-node@v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 24
registry-url: https://registry.npmjs.org
cache: npm

- run: npm install -g npm@latest
- run: node --version && npm --version
- run: npm ci
- run: npm run ci
- run: npm ci --ignore-scripts

- name: Create version PR or publish
uses: changesets/action@v1
uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b
with:
version: npm run version-packages
publish: npm run publish-packages
Expand Down
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
repos:
- repo: local
hooks:
- id: npm-ci
name: npm run ci
entry: npm run ci
language: system
pass_filenames: false
185 changes: 116 additions & 69 deletions extensions/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ const ToolDefinitions = [
type ToolName = (typeof ToolDefinitions)[number]["name"];
type ToolParams = Record<string, unknown> & { projectPath?: string };
type JsonRpcRequest = (method: string, params: Record<string, unknown>) => Promise<any>;
type PendingJsonRpcRequests = Map<number, {
resolve: (value: any) => void;
reject: (error: Error) => void;
}>;

const MaxDiagnosticLength = 1000;

Expand Down Expand Up @@ -194,67 +198,106 @@ async function runJsonRpcSession<T>(
signal: AbortSignal | undefined,
fn: (request: JsonRpcRequest) => Promise<T>,
): Promise<T> {
let nextId = 1;
let stdout = "";
let stderr = "";
const pending = new Map<number, {
resolve: (value: any) => void;
reject: (error: Error) => void;
}>();

const cleanup = () => {
for (const entry of pending.values()) {
entry.reject(new Error("CodeGraph MCP process closed before responding."));
}
pending.clear();
if (!child.killed) child.kill();
};

const pending: PendingJsonRpcRequests = new Map();
const stderr = { value: "" };
const cleanup = () => cleanupJsonRpcChild(child, pending);
const onAbort = () => cleanup();

signal?.addEventListener("abort", onAbort, { once: true });
attachJsonRpcHandlers(child, pending, stderr);

try {
const sendRequest = createJsonRpcRequestSender(child, pending);
await initializeJsonRpcSession(cwd, sendRequest, sendJsonRpcNotification.bind(undefined, child));
return await fn(sendRequest);
} finally {
signal?.removeEventListener("abort", onAbort);
cleanup();
}
}

function cleanupJsonRpcChild(
child: ChildProcessWithoutNullStreams,
pending: PendingJsonRpcRequests,
): void {
rejectPendingJsonRpcRequests(
pending,
new Error("CodeGraph MCP process closed before responding."),
);
if (!child.killed) child.kill();
}

function rejectPendingJsonRpcRequests(
pending: PendingJsonRpcRequests,
error: Error,
): void {
for (const entry of pending.values()) entry.reject(error);
pending.clear();
}

function attachJsonRpcHandlers(
child: ChildProcessWithoutNullStreams,
pending: PendingJsonRpcRequests,
stderr: { value: string },
): void {
const stdout = { value: "" };

child.stdout.on("data", (chunk) => {
stdout += chunk.toString("utf-8");
let newline;
while ((newline = stdout.indexOf("\n")) !== -1) {
const line = stdout.slice(0, newline).trim();
stdout = stdout.slice(newline + 1);
if (!line) continue;

let msg: any;
try {
msg = JSON.parse(line);
} catch {
continue;
}

if (msg.id !== undefined && pending.has(msg.id)) {
const { resolve, reject } = pending.get(msg.id)!;
pending.delete(msg.id);
if (msg.error) reject(new Error(msg.error.message || JSON.stringify(msg.error)));
else resolve(msg.result);
}
}
handleJsonRpcStdout(chunk, stdout, pending);
});

child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf-8");
stderr.value += chunk.toString("utf-8");
});
child.on("error", (err) => rejectPendingJsonRpcRequests(pending, err));
child.on("exit", (code) => rejectPendingJsonRpcOnExit(pending, stderr.value, code));
}

child.on("error", (err) => {
for (const entry of pending.values()) entry.reject(err);
pending.clear();
});
function handleJsonRpcStdout(
chunk: Buffer,
stdout: { value: string },
pending: PendingJsonRpcRequests,
): void {
stdout.value += chunk.toString("utf-8");
let newline;
while ((newline = stdout.value.indexOf("\n")) !== -1) {
const line = stdout.value.slice(0, newline).trim();
stdout.value = stdout.value.slice(newline + 1);
if (line) resolveJsonRpcLine(line, pending);
}
}

child.on("exit", (code) => {
if (pending.size === 0) return;
const diagnostic = sanitizeDiagnostic(stderr.trim());
const msg = diagnostic || `CodeGraph MCP process exited with code ${code}`;
for (const entry of pending.values()) entry.reject(new Error(msg));
pending.clear();
});
function resolveJsonRpcLine(line: string, pending: PendingJsonRpcRequests): void {
let msg: any;
try {
msg = JSON.parse(line);
} catch {
return;
}

if (msg.id === undefined || !pending.has(msg.id)) return;
const { resolve, reject } = pending.get(msg.id)!;
pending.delete(msg.id);
if (msg.error) reject(new Error(msg.error.message || JSON.stringify(msg.error)));
else resolve(msg.result);
}

function rejectPendingJsonRpcOnExit(
pending: PendingJsonRpcRequests,
stderr: string,
code: number | null,
): void {
if (pending.size === 0) return;
const diagnostic = sanitizeDiagnostic(stderr.trim());
const msg = diagnostic || `CodeGraph MCP process exited with code ${code}`;
rejectPendingJsonRpcRequests(pending, new Error(msg));
}

const sendRequest: JsonRpcRequest = (method, params) => {
function createJsonRpcRequestSender(
child: ChildProcessWithoutNullStreams,
pending: PendingJsonRpcRequests,
): JsonRpcRequest {
let nextId = 1;
return (method, params) => {
const id = nextId++;
const payload = { jsonrpc: "2.0", id, method, params };
const promise = new Promise<any>((resolve, reject) => {
Expand All @@ -263,26 +306,30 @@ async function runJsonRpcSession<T>(
child.stdin.write(`${JSON.stringify(payload)}\n`);
return promise;
};
}

const sendNotification = (method: string, params: Record<string, unknown>) => {
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
};
function sendJsonRpcNotification(
child: ChildProcessWithoutNullStreams,
method: string,
params: Record<string, unknown>,
): void {
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
}

try {
const rootUri = pathToFileURL(cwd).href;
await sendRequest("initialize", {
protocolVersion: "2024-11-05",
rootUri,
workspaceFolders: [{ uri: rootUri, name: cwd.split(/[\\/]/).pop() || cwd }],
capabilities: {},
clientInfo: { name: "pi-codegraph", version: "0.1.0" },
});
sendNotification("initialized", {});
return await fn(sendRequest);
} finally {
signal?.removeEventListener("abort", onAbort);
cleanup();
}
async function initializeJsonRpcSession(
cwd: string,
sendRequest: JsonRpcRequest,
sendNotification: (method: string, params: Record<string, unknown>) => void,
): Promise<void> {
const rootUri = pathToFileURL(cwd).href;
await sendRequest("initialize", {
protocolVersion: "2024-11-05",
rootUri,
workspaceFolders: [{ uri: rootUri, name: cwd.split(/[\\/]/).pop() || cwd }],
capabilities: {},
clientInfo: { name: "pi-codegraph", version: "0.1.0" },
});
sendNotification("initialized", {});
}

export async function callCodeGraphTool(
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"ci": "npm run typecheck && npm test && npm run compat:codegraph && npm pack --dry-run",
"compat:codegraph": "codegraph --version",
"local-release": "changeset version && changeset publish",
"publish-packages": "node -e \"const {execSync}=require('node:child_process'); const p=require('./package.json'); const spec=p.name+'@'+p.version; try { execSync('npm view '+spec+' version', {stdio:'ignore'}); console.log(spec+' already published; skipping.'); } catch { execSync('npm publish --access public --provenance', {stdio:'inherit'}); }\"",
"publish-packages": "node -e \"const {execSync}=require('node:child_process'); const p=require('./package.json'); const spec=p.name+'@'+p.version; try { execSync('npm view '+spec+' version', {stdio:'ignore'}); console.log(spec+' already published; skipping.'); } catch { execSync('npm publish --access public --provenance --ignore-scripts', {stdio:'inherit'}); }\"",
"prepack": "npm run typecheck && npm test",
"prepublishOnly": "npm run ci",
"typecheck": "tsc --noEmit",
Expand Down
17 changes: 17 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[tool.skylos]
complexity = 10
nesting = 3
max_args = 5
max_lines = 50
ignore = []
exclude = ["package-lock.json"]

[tool.skylos.languages.typescript]
complexity = 15
nesting = 4

[tool.skylos.gate]
fail_on_critical = true
max_security = 0
max_quality = 0
strict = true