🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

geraph

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

geraph - npm Package Compare versions

Comparing version
0.2.0
to
0.3.0
+62
-33
dist/core/worker.js

@@ -232,26 +232,42 @@ // src/core/worker.ts

]);
function resolveImportToNode(importPath, sourceFilePath) {
if (!importPath.startsWith(".")) return null;
const sourceDir = path.dirname(sourceFilePath);
function resolveImportToNode(importPath, sourceFilePath, aliases = []) {
const checkCandidates = (resolvedBase) => {
const candidates = [
resolvedBase + ".ts",
resolvedBase + ".tsx",
resolvedBase + "/index.ts",
resolvedBase + "/index.tsx",
resolvedBase + ".js",
resolvedBase + ".jsx",
resolvedBase + "/index.js",
resolvedBase + "/index.jsx"
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
};
const normalizedImport = importPath.replace(/\.js$/, "");
const resolvedBase = path.resolve(sourceDir, normalizedImport);
const candidates = [
resolvedBase + ".ts",
resolvedBase + ".tsx",
resolvedBase + "/index.ts",
resolvedBase + "/index.tsx",
resolvedBase + ".js",
resolvedBase + ".jsx",
resolvedBase + "/index.js",
resolvedBase + "/index.jsx"
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
if (importPath.startsWith(".")) {
const sourceDir = path.dirname(sourceFilePath);
const resolvedBase = path.resolve(sourceDir, normalizedImport);
return checkCandidates(resolvedBase);
}
if (aliases && aliases.length > 0) {
for (const alias of aliases) {
if (importPath.startsWith(alias.prefix)) {
const remainder = normalizedImport.slice(alias.prefix.length);
for (const target of alias.targets) {
const resolvedBase = path.join(target, remainder);
const found = checkCandidates(resolvedBase);
if (found) return found;
}
}
}
}
return null;
}
function parseTypeScript(filePath, graph) {
function parseTypeScript(filePath, graph, aliases = []) {
const isTs = filePath.endsWith(".ts") || filePath.endsWith(".tsx");
const isTsx = filePath.endsWith(".tsx");
const basename = path.basename(filePath);
let language;

@@ -351,7 +367,8 @@ if (isTs) {

const scope = getEnclosingScope(node);
const id = scope ? `${filePath}::${scope.name}` : `${filePath}::top-level`;
if (!scope) return filePath;
const id = `${filePath}::${scope.name}`;
if (!graph.hasNode(id)) {
graph.addNode(id, {
type: scope ? scope.type : "function",
name: scope ? scope.name : `[script] ${basename}`,
type: scope.type,
name: scope.name,
file: filePath,

@@ -406,3 +423,3 @@ startLine: node.startPosition.row + 1,

if (!importPath.startsWith(".") && !importPath.startsWith("/") && (NODE_CORE_MODULES.has(normalizedPath) || NODE_CORE_MODULES.has(rootModule))) continue;
const resolvedId = resolveImportToNode(importPath, filePath);
const resolvedId = resolveImportToNode(importPath, filePath, aliases);
const targetNodeId = resolvedId || `import::${importPath}`;

@@ -489,3 +506,3 @@ if (!graph.hasNode(targetNodeId)) {

if (link.startsWith(".")) {
targetPath = resolveImportToNode(link, filePath) || link;
targetPath = resolveImportToNode(link, filePath, aliases) || link;
}

@@ -519,3 +536,3 @@ if (!graph.hasNode(targetPath)) {

} else if (importSource) {
const resolvedSource = resolveImportToNode(importSource, filePath) || importSource;
const resolvedSource = resolveImportToNode(importSource, filePath, aliases) || importSource;
targetId = `${resolvedSource}::${referencedTypeName}`;

@@ -530,3 +547,3 @@ } else {

name: referencedTypeName,
file: importSource && resolveImportToNode(importSource, filePath) || importSource || filePath,
file: importSource && resolveImportToNode(importSource, filePath, aliases) || importSource || filePath,
startLine: 0,

@@ -547,4 +564,4 @@ metadata: {

if (importSource) {
const resolvedSource = resolveImportToNode(importSource, filePath) || importSource;
const importNodeId = resolveImportToNode(importSource, filePath) || `import::${importSource}`;
const resolvedSource = resolveImportToNode(importSource, filePath, aliases) || importSource;
const importNodeId = resolveImportToNode(importSource, filePath, aliases) || `import::${importSource}`;
if (!graph.hasNode(importNodeId)) {

@@ -585,3 +602,3 @@ graph.addNode(importNodeId, { type: "file", name: importSource, file: resolvedSource, startLine: 0, metadata: { external: true } });

} else if (importSource) {
const resolvedSource = resolveImportToNode(importSource, filePath) || importSource;
const resolvedSource = resolveImportToNode(importSource, filePath, aliases) || importSource;
symId = `${resolvedSource}::${calledName}`;

@@ -594,3 +611,3 @@ } else {

const callLine = node.startPosition.row + 1;
const resolvedSource = importSource && resolveImportToNode(importSource, filePath) || importSource;
const resolvedSource = importSource && resolveImportToNode(importSource, filePath, aliases) || importSource;
graph.addNode(symId, {

@@ -615,4 +632,4 @@ type: name === "constructor_name" ? "class" : "function",

if (importSource) {
const resolvedSource = resolveImportToNode(importSource, filePath) || importSource;
const importNodeId = resolveImportToNode(importSource, filePath) || `import::${importSource}`;
const resolvedSource = resolveImportToNode(importSource, filePath, aliases) || importSource;
const importNodeId = resolveImportToNode(importSource, filePath, aliases) || `import::${importSource}`;
if (!graph.hasNode(importNodeId)) {

@@ -774,4 +791,15 @@ graph.addNode(importNodeId, {

]);
function getContextualAliases(filePath, aliasMap) {
let current = path5.dirname(filePath);
while (current) {
const aliases = aliasMap[current];
if (aliases) return aliases;
const parent = path5.dirname(current);
if (parent === current) break;
current = parent;
}
return [];
}
parentPort?.on("message", async (msg) => {
const { filePath } = msg;
const { filePath, aliasMap } = msg;
const localGraph = new MultiDirectedGraph();

@@ -790,3 +818,4 @@ try {

if (ext === "ts" || ext === "js" || ext === "tsx" || ext === "jsx") {
parseTypeScript(filePath, localGraph);
const aliases = getContextualAliases(filePath, aliasMap || {});
parseTypeScript(filePath, localGraph, aliases);
} else if (ext === "json") {

@@ -793,0 +822,0 @@ parseJson(filePath, localGraph);

@@ -5,8 +5,5 @@ #!/usr/bin/env node

import { Command } from "commander";
import chalk3 from "chalk";
import chalk4 from "chalk";
import ora from "ora";
import path5 from "path";
import { availableParallelism } from "os";
import { fileURLToPath } from "url";
import { Worker } from "worker_threads";
import path7 from "path";

@@ -50,3 +47,3 @@ // src/core/scanner.ts

}
ig.add(["node_modules", "dist", ".git", ".turbo", "build"]);
ig.add(["node_modules", "dist", ".git", ".turbo", "build", ".geraph"]);
const globPatterns = SUPPORTED_EXTENSIONS.map((ext) => `**/*.${ext}`);

@@ -60,5 +57,145 @@ const normalizedCwd = targetDir.split(path.sep).join("/");

const validRelativeFiles = ig.filter(allFiles);
return validRelativeFiles.map((file) => path.resolve(targetDir, file));
return validRelativeFiles.sort().map((file) => path.resolve(targetDir, file));
}
// src/core/ast.ts
import { availableParallelism } from "os";
import path2 from "path";
import fs2 from "fs";
import { fileURLToPath } from "url";
import { Worker } from "worker_threads";
import crypto from "crypto";
import chalk from "chalk";
function getFileHash(filePath, rootDir, aliasMap) {
const content = fs2.readFileSync(filePath);
const relPath = path2.relative(rootDir, filePath).toLowerCase();
const h = crypto.createHash("sha256");
h.update(content);
h.update(Buffer.from("\0"));
h.update(Buffer.from(relPath));
h.update(Buffer.from("\0"));
h.update(Buffer.from(JSON.stringify(aliasMap)));
return h.digest("hex");
}
async function extractAst(files, graph, targetDir, aliasMap, spinner) {
const astCacheDir = path2.join(targetDir, ".geraph", "cache", "ast");
if (!fs2.existsSync(astCacheDir))
fs2.mkdirSync(astCacheDir, { recursive: true });
const queue = [];
let cachedCount = 0;
const resultsMap = /* @__PURE__ */ new Map();
for (const file of files) {
try {
const hash = getFileHash(file, targetDir, aliasMap);
const cachePath = path2.join(astCacheDir, `${hash}.json`);
if (fs2.existsSync(cachePath)) {
const cached = JSON.parse(fs2.readFileSync(cachePath, "utf-8"));
resultsMap.set(file, { nodes: cached.nodes, edges: cached.edges });
cachedCount++;
} else {
queue.push({ file, hash });
}
} catch {
queue.push({ file, hash: "" });
}
}
if (cachedCount === files.length) {
spinner.stop();
console.log(
chalk.green(`\u26A1 Fully cached! No files needed re-parsing.`)
);
spinner.start();
} else {
spinner.text = chalk.gray(
`Parsing AST for ${queue.length} files (${cachedCount} cached)...`
);
}
if (queue.length > 0) {
const numWorkers = Math.min(availableParallelism(), queue.length);
const workerPath = path2.join(
path2.dirname(fileURLToPath(import.meta.url)),
"core",
"worker.js"
);
let parsedCount = 0;
const workers = Array.from(
{ length: numWorkers },
() => new Worker(workerPath)
);
await Promise.all(
workers.map(async (worker) => {
while (queue.length > 0) {
const item = queue.shift();
if (!item) break;
const { file, hash } = item;
await new Promise((resolve) => {
const onMessage = (msg) => {
if (msg.error) {
console.error(
chalk.yellow(
`
\u26A0\u3000Worker error for ${file}: ${msg.error}`
)
);
} else {
resultsMap.set(file, { nodes: msg.nodes, edges: msg.edges });
if (hash) {
try {
const cachePath = path2.join(astCacheDir, `${hash}.json`);
fs2.writeFileSync(
cachePath,
JSON.stringify({ nodes: msg.nodes, edges: msg.edges })
);
} catch {
}
}
}
parsedCount++;
spinner.text = chalk.gray(
`Parsing AST: ${parsedCount}/${files.length - cachedCount} files...`
);
worker.off("message", onMessage);
worker.off("error", onError);
resolve();
};
const onError = (err) => {
console.error(
chalk.red(`Worker error on ${file}: ${err.message}`)
);
parsedCount++;
worker.off("message", onMessage);
worker.off("error", onError);
resolve();
};
worker.on("message", onMessage);
worker.on("error", onError);
worker.postMessage({
filePath: file,
projectRoot: targetDir,
aliasMap
});
});
}
await worker.terminate();
})
);
}
for (const file of files) {
const res = resultsMap.get(file);
if (!res) continue;
res.nodes?.forEach((n) => {
if (!graph.hasNode(n.id)) {
graph.addNode(n.id, n.attr);
} else if (n.attr.type !== "file") {
graph.mergeNodeAttributes(n.id, n.attr);
}
});
res.edges?.forEach((e) => {
if (!graph.hasEdge(e.source, e.target)) {
graph.addEdge(e.source, e.target, e.attr);
}
});
}
}
// src/core/graph.ts

@@ -99,5 +236,5 @@ import { MultiDirectedGraph } from "graphology";

import { simpleGit } from "simple-git";
import path2 from "path";
import fs2 from "fs";
import chalk from "chalk";
import path3 from "path";
import fs3 from "fs";
import chalk2 from "chalk";
var CACHE_VERSION = "1";

@@ -122,3 +259,3 @@ var GIT_ENRICH_SKIP = /* @__PURE__ */ new Set([

try {
const raw = fs2.readFileSync(cacheFile, "utf-8");
const raw = fs3.readFileSync(cacheFile, "utf-8");
const parsed = JSON.parse(raw);

@@ -133,3 +270,3 @@ if (parsed.version !== CACHE_VERSION) return null;

try {
fs2.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), "utf-8");
fs3.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), "utf-8");
} catch {

@@ -146,3 +283,3 @@ }

if (isShallow) {
console.warn(chalk.yellow("\n\u26A0\u3000Shallow Git repository detected. Skipping Git enrichment to save time."));
console.warn(chalk2.yellow("\n\u26A0\u3000Shallow Git repository detected. Skipping Git enrichment to save time."));
return;

@@ -153,5 +290,6 @@ }

}
const outDir = path2.join(targetDir, ".geraph");
if (!fs2.existsSync(outDir)) fs2.mkdirSync(outDir, { recursive: true });
const cacheFile = path2.join(outDir, "git-cache.json");
const outDir = path3.join(targetDir, ".geraph");
const cacheDir = path3.join(outDir, "cache");
if (!fs3.existsSync(cacheDir)) fs3.mkdirSync(cacheDir, { recursive: true });
const cacheFile = path3.join(cacheDir, "git-cache.json");
const cache = loadCache(cacheFile) ?? {

@@ -178,3 +316,3 @@ version: CACHE_VERSION,

if (data.type === "file" && !data.metadata?.external) {
const basename = path2.basename(nodeId);
const basename = path3.basename(nodeId);
if (GIT_ENRICH_SKIP.has(basename)) continue;

@@ -194,4 +332,4 @@ if (!nodesByFile.has(nodeId)) {

try {
const normalizedPath = path2.normalize(filePath);
const stat = fs2.statSync(filePath);
const normalizedPath = path3.normalize(filePath);
const stat = fs3.statSync(filePath);
const currentMtime = stat.mtimeMs;

@@ -278,3 +416,3 @@ const cachedBlame = cache.blame[normalizedPath];

} catch {
console.warn(chalk.yellow("\n\u26A0\u3000Warning: Failed to bulk-fetch metadata for some commits."));
console.warn(chalk2.yellow("\n\u26A0\u3000Warning: Failed to bulk-fetch metadata for some commits."));
}

@@ -340,4 +478,9 @@ }

let communityMap = {};
let seed = 123456789;
const rng = () => {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
};
try {
communityMap = louvainAlgorithm(graph);
communityMap = louvainAlgorithm(graph, { rng });
} catch {

@@ -483,8 +626,78 @@ graph.forEachNode((nodeId) => {

// src/core/alias.ts
import fs4 from "fs";
import path4 from "path";
function parseTsConfig(filePath, visited = /* @__PURE__ */ new Set()) {
if (visited.has(filePath)) return {};
visited.add(filePath);
if (!fs4.existsSync(filePath)) return {};
try {
const content = fs4.readFileSync(filePath, "utf-8");
const stripped = content.replace(
/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
(m, g) => g ? "" : m
);
const cleanJson = stripped.replace(/,\s*([\]}])/g, "$1");
const json = JSON.parse(cleanJson);
let base = {};
if (json.extends) {
const extendsPaths = Array.isArray(json.extends) ? json.extends : [json.extends];
for (const extPath of extendsPaths) {
let resolvedExtPath = path4.resolve(path4.dirname(filePath), extPath);
if (!fs4.existsSync(resolvedExtPath) && !resolvedExtPath.endsWith(".json")) {
resolvedExtPath += ".json";
}
const extConfig = parseTsConfig(resolvedExtPath, visited);
base = mergeConfigs(base, extConfig);
}
}
return mergeConfigs(base, json);
} catch {
return {};
}
}
function mergeConfigs(base, child) {
return {
...base,
...child,
compilerOptions: {
...base.compilerOptions || {},
...child.compilerOptions || {}
}
};
}
function buildAliasMap(files) {
const map = {};
const configFiles = files.filter(
(f) => f.endsWith("tsconfig.json") || f.endsWith("jsconfig.json")
);
for (const file of configFiles) {
const config = parseTsConfig(file);
const compilerOptions = config.compilerOptions;
if (compilerOptions && compilerOptions.paths) {
const baseUrl = compilerOptions.baseUrl || ".";
const dir = path4.dirname(file);
const aliases = [];
for (const [key, targets] of Object.entries(compilerOptions.paths)) {
const prefix = key.replace(/\*$/, "");
const resolvedTargets = targets.map(
(t) => path4.join(dir, baseUrl, t.replace(/\*$/, ""))
);
aliases.push({ prefix, targets: resolvedTargets });
}
aliases.sort((a, b) => b.prefix.length - a.prefix.length);
if (aliases.length > 0) {
map[dir] = aliases;
}
}
}
return map;
}
// src/core/serializer.ts
import fs3 from "fs";
import path3 from "path";
import fs5 from "fs";
import path5 from "path";
function exportGraphJson(graph, outDir, analysis) {
if (!fs3.existsSync(outDir)) {
fs3.mkdirSync(outDir, { recursive: true });
if (!fs5.existsSync(outDir)) {
fs5.mkdirSync(outDir, { recursive: true });
}

@@ -531,9 +744,9 @@ const nodes = graph.nodes().map((nodeId) => {

}
const jsonPath = path3.join(outDir, "graph.json");
fs3.writeFileSync(jsonPath, JSON.stringify(payload, null, 2), "utf-8");
const jsonPath = path5.join(outDir, "graph.json");
fs5.writeFileSync(jsonPath, JSON.stringify(payload, null, 2), "utf-8");
return jsonPath;
}
function exportReportMarkdown(graph, outDir, analysis) {
if (!fs3.existsSync(outDir)) {
fs3.mkdirSync(outDir, { recursive: true });
if (!fs5.existsSync(outDir)) {
fs5.mkdirSync(outDir, { recursive: true });
}

@@ -618,3 +831,3 @@ let md = `# Geraph Codebase Report

const data = graph.getNodeAttributes(gap);
md += `- \`${data.type} ${data.name}\` (isolated in ${path3.basename(gap.split("::")[0] || gap)})
md += `- \`${data.type} ${data.name}\` (isolated in ${path5.basename(gap.split("::")[0] || gap)})
`;

@@ -660,9 +873,9 @@ }

}
const mdPath = path3.join(outDir, "GRAPH_REPORT.md");
fs3.writeFileSync(mdPath, md, "utf-8");
const mdPath = path5.join(outDir, "GRAPH_REPORT.md");
fs5.writeFileSync(mdPath, md, "utf-8");
return mdPath;
}
function exportGraphHtml(graph, outDir) {
if (!fs3.existsSync(outDir)) {
fs3.mkdirSync(outDir, { recursive: true });
if (!fs5.existsSync(outDir)) {
fs5.mkdirSync(outDir, { recursive: true });
}

@@ -951,4 +1164,4 @@ const counts = {

</html>`;
const htmlPath = path3.join(outDir, "graph.html");
fs3.writeFileSync(htmlPath, htmlContent, "utf-8");
const htmlPath = path5.join(outDir, "graph.html");
fs5.writeFileSync(htmlPath, htmlContent, "utf-8");
return htmlPath;

@@ -958,6 +1171,6 @@ }

// src/core/install.ts
import fs4 from "fs";
import path4 from "path";
import fs6 from "fs";
import path6 from "path";
import os from "os";
import chalk2 from "chalk";
import chalk3 from "chalk";
var START_MARKER = "<!-- GERAPH_START -->";

@@ -1015,3 +1228,3 @@ var END_MARKER = "<!-- GERAPH_END -->";

localFiles: [{ path: "CLAUDE.md", content: SHARED_RULES, inject: true }],
globalPath: path4.join(
globalPath: path6.join(
os.homedir(),

@@ -1053,3 +1266,3 @@ ".claude",

],
globalPath: path4.join(
globalPath: path6.join(
os.homedir(),

@@ -1075,3 +1288,3 @@ ".agent",

],
globalPath: path4.join(
globalPath: path6.join(
os.homedir(),

@@ -1093,3 +1306,3 @@ ".copilot",

],
globalPath: path4.join(
globalPath: path6.join(
os.homedir(),

@@ -1111,4 +1324,4 @@ ".copilot",

const normalizedPath = process.platform === "win32" ? currentFilePath.substring(1) : currentFilePath;
const templatePath = path4.resolve(
path4.dirname(normalizedPath),
const templatePath = path6.resolve(
path6.dirname(normalizedPath),
".",

@@ -1119,10 +1332,10 @@ "templates",

let skillContent = "";
if (fs4.existsSync(templatePath)) {
skillContent = fs4.readFileSync(templatePath, "utf-8");
if (fs6.existsSync(templatePath)) {
skillContent = fs6.readFileSync(templatePath, "utf-8");
}
for (const localFile of platform.localFiles) {
const fullPath = path4.join(targetDir, localFile.path);
const dir = path4.dirname(fullPath);
if (!fs4.existsSync(dir)) {
fs4.mkdirSync(dir, { recursive: true });
const fullPath = path6.join(targetDir, localFile.path);
const dir = path6.dirname(fullPath);
if (!fs6.existsSync(dir)) {
fs6.mkdirSync(dir, { recursive: true });
}

@@ -1135,5 +1348,6 @@ let fileContentToInject = localFile.content || skillContent;

if (platform.globalPath) {
const homeRelPath = platform.globalPath.replace(os.homedir(), "~").replace(/\\/g, "/");
fileContentToInject = fileContentToInject.replace(
"[GLOBAL_POINTER]",
`Before answering, you MUST read the [Geraph Skill](${platform.globalPath}) operational manual.`
`Before answering, you MUST read the [Geraph Skill](${homeRelPath}) operational manual.`
);

@@ -1149,4 +1363,4 @@ } else {

`;
if (fs4.existsSync(fullPath)) {
let content = fs4.readFileSync(fullPath, "utf-8");
if (fs6.existsSync(fullPath)) {
let content = fs6.readFileSync(fullPath, "utf-8");
const startIndex = content.indexOf(START_MARKER);

@@ -1156,14 +1370,14 @@ const endIndex = content.indexOf(END_MARKER);

content = content.substring(0, startIndex) + injection.trim() + content.substring(endIndex + END_MARKER.length);
fs4.writeFileSync(fullPath, content);
fs6.writeFileSync(fullPath, content);
results.push(`${localFile.path} updated (existing section replaced)`);
} else {
fs4.writeFileSync(fullPath, content.trim() + "\n" + injection);
fs6.writeFileSync(fullPath, content.trim() + "\n" + injection);
results.push(`${localFile.path} updated (section appended)`);
}
} else {
fs4.writeFileSync(fullPath, injection);
fs6.writeFileSync(fullPath, injection);
results.push(`${localFile.path} created`);
}
} else {
fs4.writeFileSync(fullPath, fileContentToInject);
fs6.writeFileSync(fullPath, fileContentToInject);
results.push(`${localFile.path} created/updated`);

@@ -1173,11 +1387,11 @@ }

if (skillContent && platform.globalPath) {
const globalDir = path4.dirname(platform.globalPath);
if (!fs4.existsSync(globalDir)) {
fs4.mkdirSync(globalDir, { recursive: true });
const globalDir = path6.dirname(platform.globalPath);
if (!fs6.existsSync(globalDir)) {
fs6.mkdirSync(globalDir, { recursive: true });
}
fs4.writeFileSync(platform.globalPath, skillContent);
fs6.writeFileSync(platform.globalPath, skillContent);
results.push(`Global skill installed at ${platform.globalPath}`);
} else if (!skillContent && platform.globalPath) {
console.log(
chalk2.red(`Failed to install global skill at ${platform.globalPath}.`)
chalk3.red(`Failed to install global skill at ${platform.globalPath}.`)
);

@@ -1201,6 +1415,6 @@ }

for (const localFile of platform.localFiles) {
const fullPath = path4.join(targetDir, localFile.path);
if (fs4.existsSync(fullPath)) {
const fullPath = path6.join(targetDir, localFile.path);
if (fs6.existsSync(fullPath)) {
if (localFile.inject) {
const content = fs4.readFileSync(fullPath, "utf-8");
const content = fs6.readFileSync(fullPath, "utf-8");
const startIndex = content.indexOf(START_MARKER);

@@ -1213,6 +1427,6 @@ const endIndex = content.indexOf(END_MARKER);

if (cleaned === "") {
fs4.unlinkSync(fullPath);
fs6.unlinkSync(fullPath);
results.push(`${localFile.path} deleted (empty after cleanup)`);
} else {
fs4.writeFileSync(fullPath, cleaned + "\n");
fs6.writeFileSync(fullPath, cleaned + "\n");
results.push(

@@ -1224,3 +1438,3 @@ `${localFile.path} cleaned (geraph section removed)`

} else {
fs4.unlinkSync(fullPath);
fs6.unlinkSync(fullPath);
results.push(`${localFile.path} deleted`);

@@ -1230,14 +1444,14 @@ }

}
if (platform.globalPath && fs4.existsSync(platform.globalPath)) {
fs4.unlinkSync(platform.globalPath);
if (platform.globalPath && fs6.existsSync(platform.globalPath)) {
fs6.unlinkSync(platform.globalPath);
results.push(`Global skill removed from ${platform.globalPath}`);
let currentDir = path4.dirname(platform.globalPath);
let currentDir = path6.dirname(platform.globalPath);
while (currentDir !== os.homedir()) {
if (path4.basename(currentDir) !== "geraph") {
if (path6.basename(currentDir) !== "geraph") {
break;
}
try {
if (fs4.readdirSync(currentDir).length === 0) {
fs4.rmdirSync(currentDir);
currentDir = path4.dirname(currentDir);
if (fs6.readdirSync(currentDir).length === 0) {
fs6.rmdirSync(currentDir);
currentDir = path6.dirname(currentDir);
} else {

@@ -1256,10 +1470,9 @@ break;

// src/cli.ts
import fs5 from "fs";
import fs7 from "fs";
var program = new Command();
program.name("geraph").description(chalk3.blue("Geraph: Structural memory for AI agents")).version("0.0.0", "-v, --version", "output the current version");
program.name("geraph").description(chalk4.blue("Geraph: Structural memory for AI agents")).version("0.3.0", "-v, --version", "output the current version");
program.command("scan").description("Scan the current directory and build the Knowledge Graph").action(async () => {
console.log(chalk3.cyan("\nGeraph Engine Started\n"));
const targetDir = process.cwd();
const spinner = ora({
text: chalk3.gray(`Scanning codebase in ${targetDir}...`),
text: chalk4.gray(`Scanning codebase in ${targetDir}...`),
color: "cyan",

@@ -1271,3 +1484,3 @@ spinner: "dots"

const files = await scanDirectory(targetDir);
spinner.text = chalk3.gray("Initializing Knowledge Graph...");
spinner.text = chalk4.gray("Initializing Knowledge Graph...");
const graph = createKnowledgeGraph();

@@ -1278,7 +1491,7 @@ for (const file of files) {

type: "file",
name: path5.basename(file),
name: path7.basename(file),
file,
startLine: 0,
metadata: {
extension: path5.extname(file)
extension: path7.extname(file)
}

@@ -1288,78 +1501,15 @@ });

}
spinner.text = chalk3.gray(`Parsing AST for ${files.length} files...`);
const numWorkers = Math.min(availableParallelism(), files.length);
const workerPath = path5.join(
path5.dirname(fileURLToPath(import.meta.url)),
"core",
"worker.js"
);
let parsedCount = 0;
const workers = Array.from(
{ length: numWorkers },
() => new Worker(workerPath)
);
const queue = [...files];
await Promise.all(
workers.map(async (worker) => {
while (queue.length > 0) {
const file = queue.shift();
if (!file) break;
await new Promise((resolve) => {
const onMessage = (msg) => {
if (msg.error) {
console.error(
chalk3.yellow(
`
\u26A0\u3000Worker error for ${file}: ${msg.error}`
)
);
} else {
msg.nodes?.forEach((n) => {
if (!graph.hasNode(n.id)) {
graph.addNode(n.id, n.attr);
} else if (n.attr.type !== "file") {
graph.mergeNodeAttributes(n.id, n.attr);
}
});
msg.edges?.forEach((e) => {
if (!graph.hasEdge(e.source, e.target)) {
graph.addEdge(e.source, e.target, e.attr);
}
});
}
parsedCount++;
spinner.text = chalk3.gray(
`Parsing AST: ${parsedCount}/${files.length} files...`
);
worker.off("message", onMessage);
worker.off("error", onError);
resolve();
};
const onError = (err) => {
console.error(
chalk3.red(`Worker error on ${file}: ${err.message}`)
);
parsedCount++;
worker.off("message", onMessage);
worker.off("error", onError);
resolve();
};
worker.on("message", onMessage);
worker.on("error", onError);
worker.postMessage({ filePath: file });
});
}
await worker.terminate();
})
);
spinner.text = chalk3.gray("Resolving call graph...");
spinner.text = chalk4.gray("Mapping path aliases...");
const aliasMap = buildAliasMap(files);
await extractAst(files, graph, targetDir, aliasMap, spinner);
spinner.text = chalk4.gray("Resolving call graph...");
resolveCallGraph(graph);
spinner.text = chalk3.gray(
spinner.text = chalk4.gray(
"Extracting Temporal Facts from Git history..."
);
await enrichWithGit(graph, targetDir);
spinner.text = chalk3.gray("Analyzing graph structure...");
spinner.text = chalk4.gray("Analyzing graph structure...");
const analysis = analyzeGraph(graph);
spinner.text = chalk3.gray("Compressing graph into Caveman Mode...");
const outDir = path5.join(targetDir, ".geraph");
spinner.text = chalk4.gray("Compressing graph into Caveman Mode...");
const outDir = path7.join(targetDir, ".geraph");
exportGraphJson(graph, outDir, analysis);

@@ -1371,3 +1521,3 @@ exportReportMarkdown(graph, outDir, analysis);

spinner.succeed(
chalk3.green(
chalk4.green(
`Successfully scanned and parsed ${files.length} target files.`

@@ -1377,14 +1527,14 @@ )

console.log();
console.log(chalk3.bold("Graph Stats:"));
console.log(chalk3.dim(` - Nodes: ${graph.order}`));
console.log(chalk3.dim(` - Edges: ${graph.size}`));
console.log(chalk3.dim(` - Communities: ${analysis.communities.length}`));
console.log(chalk3.dim(` - Time: ${durationSeconds}s`));
console.log(chalk4.bold("Graph Stats:"));
console.log(chalk4.dim(` - Nodes: ${graph.order}`));
console.log(chalk4.dim(` - Edges: ${graph.size}`));
console.log(chalk4.dim(` - Communities: ${analysis.communities.length}`));
console.log(chalk4.dim(` - Time: ${durationSeconds}s`));
console.log();
console.log(chalk3.cyan(`Type '/geraph' in your AI chat to begin.`));
console.log(chalk4.cyan(`Type '/geraph' in your AI chat to begin.`));
console.log();
} catch (error) {
spinner.fail(chalk3.red("Failed to scan directory."));
spinner.fail(chalk4.red("Failed to scan directory."));
if (error instanceof Error) {
console.error(chalk3.red(`Error: ${error.message}`));
console.error(chalk4.red(`Error: ${error.message}`));
}

@@ -1403,7 +1553,7 @@ process.exit(1);

console.log(
chalk3.yellow(`
chalk4.yellow(`
\u26A0\u3000Platform '${pName}' is not supported.`)
);
console.log(
chalk3.gray(
chalk4.gray(
` Run 'geraph install' to install the default AGENTS.md for basic LLM support.

@@ -1416,3 +1566,3 @@ `

const spinner = ora({
text: chalk3.gray(`Installing Geraph rules for ${pName}...`),
text: chalk4.gray(`Installing Geraph rules for ${pName}...`),
color: "blue",

@@ -1423,11 +1573,11 @@ spinner: "dots"

const platformResults = await installGeraph(targetDir, pName);
spinner.succeed(chalk3.green(`Installed Geraph rules for ${pName}.`));
spinner.succeed(chalk4.green(`Installed Geraph rules for ${pName}.`));
console.log();
platformResults.forEach((r) => console.log(chalk3.dim(` - ${r}`)));
platformResults.forEach((r) => console.log(chalk4.dim(` - ${r}`)));
console.log();
results.push(...platformResults);
} catch (error) {
spinner.fail(chalk3.red(`Failed to install rules for ${pName}.`));
spinner.fail(chalk4.red(`Failed to install rules for ${pName}.`));
if (error instanceof Error) {
console.error(chalk3.red(`Error: ${error.message}`));
console.error(chalk4.red(`Error: ${error.message}`));
}

@@ -1437,5 +1587,5 @@ }

if (results.length > 0) {
if (!fs5.existsSync(path5.join(process.cwd(), ".geraph/graph.json"))) {
if (!fs7.existsSync(path7.join(process.cwd(), ".geraph/graph.json"))) {
console.log(
chalk3.cyan(
chalk4.cyan(
"Next Step: Run 'geraph scan' to build the graphical knowledge base."

@@ -1455,3 +1605,3 @@ )

console.log(
chalk3.yellow(
chalk4.yellow(
`

@@ -1465,3 +1615,3 @@ \u26A0\u3000Platform '${pName}' is not recognized. Skipping.

const spinner = ora({
text: chalk3.gray(
text: chalk4.gray(
pName ? `Removing rules for ${pName}...` : "Removing all Geraph rules..."

@@ -1477,3 +1627,3 @@ ),

spinner.succeed(
chalk3.green(
chalk4.green(
pName ? `Successfully removed rules for ${pName}.` : "Successfully removed all Geraph rules."

@@ -1483,3 +1633,3 @@ )

console.log();
platformResults.forEach((r) => console.log(chalk3.dim(` - ${r}`)));
platformResults.forEach((r) => console.log(chalk4.dim(` - ${r}`)));
console.log();

@@ -1491,6 +1641,6 @@ } else {

spinner.fail(
chalk3.red(`Failed to remove rules for ${pName || "all"}.`)
chalk4.red(`Failed to remove rules for ${pName || "all"}.`)
);
if (error instanceof Error) {
console.error(chalk3.red(`Error: ${error.message}`));
console.error(chalk4.red(`Error: ${error.message}`));
}

@@ -1500,8 +1650,11 @@ }

if (results.length === 0) {
console.log(chalk3.yellow("\nNo Geraph rules found to remove.\n"));
console.log(chalk4.yellow("\nNo Geraph rules found to remove.\n"));
}
});
program.command("search <term>").description("Discover multiple nodes matching a partial term").option("-t, --type <type>", "Filter results by node type (e.g., 'interface', 'class', 'function', 'file')").action(async (term, options) => {
program.command("search <term>").description("Discover multiple nodes matching a partial term").option(
"-t, --type <type>",
"Filter results by node type (e.g., 'interface', 'class', 'function', 'file')"
).action(async (term, options) => {
const spinner = ora({
text: chalk3.gray(`Searching graph for: ${term}...`),
text: chalk4.gray(`Searching graph for: ${term}...`),
color: "blue",

@@ -1515,7 +1668,11 @@ spinner: "dots"

if (results.length === 0) {
console.error(chalk3.yellow(`No nodes found matching '${term}'`));
console.error(chalk4.yellow(`No nodes found matching '${term}'`));
} else {
console.log(JSON.stringify(results, null, 2));
console.error(chalk3.gray(`
Found ${results.length} nodes. Use 'geraph query <id>' to inspect a specific node.`));
console.error(
chalk4.gray(
`
Found ${results.length} nodes. Use 'geraph query <id>' to inspect a specific node.`
)
);
}

@@ -1525,3 +1682,3 @@ } catch (error) {

console.error(
chalk3.red("?? Search failed:"),
chalk4.red("?? Search failed:"),
error instanceof Error ? error.message : String(error)

@@ -1532,5 +1689,13 @@ );

});
program.command("query <symbol>").description("Query the knowledge graph for a specific symbol's relationships").option("-t, --type <type>", "Filter results by node type (e.g., 'interface', 'class', 'function', 'file')").option("-s, --source <path>", "Filter results by source file path (e.g., 'auth.ts')").action(async (symbol, options) => {
program.command("query <symbol>").description(
"Query the knowledge graph for a specific symbol's relationships"
).option(
"-t, --type <type>",
"Filter results by node type (e.g., 'interface', 'class', 'function', 'file')"
).option(
"-s, --source <path>",
"Filter results by source file path (e.g., 'auth.ts')"
).action(async (symbol, options) => {
const spinner = ora({
text: chalk3.gray(`Querying relationships for: ${symbol}...`),
text: chalk4.gray(`Querying relationships for: ${symbol}...`),
color: "blue",

@@ -1541,3 +1706,8 @@ spinner: "dots"

const { queryGraph } = await import("./query-ALVJVDRK.js");
const result = await queryGraph(process.cwd(), symbol, options.type, options.source);
const result = await queryGraph(
process.cwd(),
symbol,
options.type,
options.source
);
spinner.stop();

@@ -1547,3 +1717,3 @@ console.log(JSON.stringify(result, null, 2));

spinner.fail(
chalk3.red(
chalk4.red(
`Query failed: ${error instanceof Error ? error.message : String(error)}`

@@ -1550,0 +1720,0 @@ )

@@ -9,40 +9,63 @@ ---

Geraph is a structural memory engine that tracks dependencies, function calls, imports, and historical context (Git commits) across the codebase. It eliminates the need to rely on `grep` or blind text searches.
Geraph is a structural memory engine that tracks dependencies, function calls, imports, and historical context (Git commits) across the codebase. It builds a graph mapping all relationships without executing any code.
## How Geraph Works
When executed, Geraph parses the codebase AST (Abstract Syntax Tree) without executing any code. It builds a graph mapping all relationships and outputs its data into the `.geraph/` directory.
## What You Must Do When Invoked
### Key Outputs
- **`.geraph/GRAPH_REPORT.md`**: A human-readable architectural summary containing project stats, core architectural pillars ("God Nodes"), and community clustering. **Always read this file first** to understand the high-level architecture. Note that for large projects, this report is truncated to the "Top 100" files and "Top 50" recent commits. Use the `query` command for surgical precision and complete history.
- **`.geraph/graph.html`**: An interactive 2D/3D visualization of the codebase. Useful for humans to spot clusters and flow, but not for AI agents.
- **`.geraph/graph.json`**: The raw serialized graph data. **CRITICAL**: NEVER attempt to read this file directly as it can be massive and will overflow your context window. Always use the `query` command to fetch filtered, token-efficient data.
When you need to understand the architecture, find where a component is, or trace dependencies, follow this strict protocol:
## Operational Protocol
### Step 1: Read the Global Architecture Report
Before answering architecture or codebase questions, **always read `.geraph/GRAPH_REPORT.md` first**. It contains project stats, core architectural pillars ("God Nodes"), and community clustering.
- **CRITICAL RULE**: NEVER read `.geraph/graph.json` or `.geraph/graph.html` directly. Always use the CLI.
- **COMMAND NOT FOUND**: If the terminal returns a 'command not found' error for any `geraph` command, you MUST install it globally using `npm install -g geraph` before proceeding.
- **TERMINAL OUTPUT BUG**: If your agent environment/terminal fails to capture stdout (e.g., it returns "No output" or the command silently hangs), **DO NOT GIVE UP.** Run the command again and redirect the output to a file inside the `.geraph` directory: `geraph query 'Symbol' > .geraph/out.json`, then read that file. NEVER write output files to the project root.
- **CRITICAL RULE**: DO NOT use `| head` or `| grep`. The CLI is already token-optimized and piping hides crucial error messages.
- **MANDATORY**: You MUST wait for the terminal response after running any Geraph command. Do not hallucinate results.
### Step 2: Use `search` for Fuzzy Discovery
If you don't know the exact ID or exact name of a symbol, you MUST use `search` first.
```bash
geraph search '<term>' [--type <type>]
```
- **Inputs**: Use a broad concept, a partial filename, or a partial function name. For example: `search 'auth'`, `search 'database'`, `search 'User'`.
- **Options**: You can filter by `--type file` or `--type function` if you know what you are looking for.
- **Output**: Returns a lightweight list of matching Node IDs, sorted by significance.
### Command Reference
### Step 3: Use `query` for Deep Inspection
Once you have found the exact Node ID or exact Symbol Name from Step 2 (or if the user explicitly provided one), use `query` to fetch its full dependencies.
```bash
geraph query '<symbol_or_id>' [--type <type>] [--source <file>]
```
- **Inputs**: The exact Node ID (e.g., `src/auth/session.ts::ValidateToken`) or the exact symbol name (e.g., `ValidateToken`). If you are exploring a specific file, you can query its filepath directly (e.g., `geraph query 'src/app.ts' --type file`).
- **Options**:
- `--type <type>` (e.g., `file`, `function`, `class`, `interface`, `intent`). Use this to resolve naming conflicts (e.g., if there is a function and an interface both named "User").
- `--source <file>` (e.g., `src/auth/session.ts`). Use this if you only know the symbol name, to ensure Geraph finds the definition in the correct file.
- **Why use options?**: Always use `--type` and `--source` if you know them. They strictly reduce token bloat and guarantee you get the exact node you want.
| Command | Syntax | When to Use |
|---|---|---|
| **Search** | `geraph search '<term>' [--type <type>]` | For broad/fuzzy discovery. Use when you only know part of a name or want to see all nodes matching a concept (e.g., `search 'auth'`). Returns a lightweight array of matching IDs. |
| **Query** | `geraph query '<symbol>' [--type <type>] [--source <file>]` | For deep inspection. Use when you know the exact ID or exact name of a symbol. Returns full dependencies (`incoming`/`outgoing`) and metadata. |
| **Scan** | `geraph scan` | Run this IMMEDIATELY after you make any type of change in the codebase to ensure the graph is up to date. |
### Step 4: Trace the Context ("The Why")
When you run `query`, you will see `incoming` and `outgoing` edges.
- **Impact Analysis**: Use `incoming` edges to see who depends on this node.
- **Dependencies**: Use `outgoing` edges to see what this node uses.
- **Historical Context ("The Why")**: If you see an `intent` type node connected to your target, **query that `intent` node**. Look at its `metadata.message`. This is a Git commit message that explains *why* the code exists or was changed.
*Note on Flags: All command options/flags are optional, but it is highly recommended to use them if you know the exact type or source, as it guarantees precise results. `--type` and `--source` are the ONLY valid flags. NEVER invent flags like `--limit` or `--dfs`.*
### Step 5: Keep the Graph Current
Geraph must stay in sync with the codebase.
- You MUST run `geraph scan` after you modify any code files.
- You MUST run `geraph scan` after committing a change to the repository.
### JSON Response Schema
When you run a command, it returns pure JSON on stdout. Here is how to interpret the fields:
---
**`search` output**: Returns an array of matching node objects, sorted by connection count (most connected first). Each object contains:
- `id`: The unique node identifier (format: `filePath::symbolName` for code symbols, `commit::hash` for intents, or a raw file path for files).
- `name`: The human-readable name of the symbol.
- `type`: The node type (e.g., `function`, `class`, `interface`, `file`, `intent`).
- `file`: The source file where this node is defined.
- `links`: Total number of connections (incoming + outgoing). Higher means more architecturally significant.
## CRITICAL RULES
1. **NEVER read `.geraph/graph.json` directly.** It is massive and will overflow your context window. Always use the CLI.
2. **NO EXTRA FLAGS**: There are NO extra or hidden options for `search` or `query`. DO NOT hallucinate flags like `--limit` or `--dfs`. Only use `--type` and `--source`.
3. **COMMAND NOT FOUND**: If the terminal returns a 'command not found' error, you MUST install it globally using `npm install -g geraph` before proceeding.
4. **TERMINAL OUTPUT BUG**: If your terminal fails to capture stdout (e.g., it hangs or returns "No output"), DO NOT GIVE UP. Redirect the output to a file: `geraph query 'Symbol' > .geraph/out.json`, then read that file.
5. **NEVER use `grep`, `rg`, or `find`** for architecture questions when Geraph is available.
---
## JSON Response Interpretation
**`search` output**: Returns an array of matching node objects.
- `id`: The unique node identifier. Use this exact string for subsequent `query` calls.
- `name`: The human-readable name.
- `type`: The node type (see Glossary below).
- `file`: The source file.
- `links`: Total connections. Higher means more architecturally significant.
**`query` output**: Returns a detailed object with `target`, `incoming`, and `outgoing`:

@@ -65,25 +88,19 @@ - `target`: The queried node's full details:

### Standard Workflows
---
| Scenario | Action / Command | Why |
|---|---|---|
| **"How does [Concept] work?"** | 1. Read `GRAPH_REPORT.md` to find God Nodes.<br>2. `geraph search '<concept>'` | Geraph is an AST graph. It does not understand English words like 'auth' or 'database'. You MUST use `search` first to find the actual code symbols (e.g. `authSlice.ts`), and then `query` those symbols. NEVER `query` a raw concept. |
| **User mentions a file (e.g. @file:xyz.ts)** | `geraph query '<filepath>' --type file` | ALWAYS query a mentioned file first. Analyzing its `outgoing` connections instantly reveals all classes/functions defined inside it, so you don't have to guess symbol names. |
| **"What does this function do?"**| `geraph query '<funcName>' --type function` | Read `target.metadata.doc` for intent. Look at `outgoing` for what it calls, and `incoming` for who calls it. |
| **"Impact of changing a field/property?"** | `geraph query '<ContainerName>'` | Geraph DOES NOT index individual fields (like `avatar`). You MUST query the Interface/Class that contains the field (e.g., `UserState`), then analyze its `incoming` edges. NEVER query the field name directly. |
| **"Impact of changing a class/function?"** | `geraph query '<symbolName>'` | Analyze the `incoming` array. These are the exact entities that depend on your target and might break. |
| **"Query Failed / Not Found"** | `geraph search '<symbolName>'` | Do NOT fallback to `grep`. If the terminal fails to capture output, redirect to a file (`> .geraph/out.json`) and read it. If it returns a genuine "Not found" error, use the `geraph search` command to find the correct naming. |
## Geraph Glossary
### Geraph Glossary
Use this glossary to understand the types of nodes and edges in the graph, and to accurately choose your `--type` flag for queries.
| Node Type | Description |
### Node Types
| Type | Description |
|---|---|
| `file` | A source code file. |
| `function` | A standard function/method. |
| `function` | A standard function, method, or arrow function. |
| `class` | A class definition. |
| `interface`/`type`/`enum`| TypeScript type definitions. |
| `[script] <name>` | The top-level execution block of a file (code outside any function/class). |
| `intent` | A Git commit explaining why a node exists. |
| Edge Type | Description |
### Edge Types (`relation`)
| Relation | Description |
|---|---|

@@ -93,3 +110,11 @@ | `imports` | File A depends on File B. |

| `defines` | A file contains a function/class. |
| `references` | A function uses a specific type. |
| `explains` | A Git commit provides historical context for a node. |
| `references` | A function uses a specific type or interface. |
| `explains` | A Git commit (`intent` node) provides historical context for a specific code node. |
### Confidence Scores
Every edge has a `confidence` level:
| Confidence | Description |
|---|---|
| `EXTRACTED` | 100% deterministic. Found directly by the AST parser (e.g., an explicit function call). |
| `INFERRED` | High probability. Deduced via structural heuristics or indirect relationships. |
| `AMBIGUOUS` | Uncertain connection. Requires human/agent verification. |
{
"name": "geraph",
"version": "0.2.0",
"version": "0.3.0",
"description": "Structural memory for AI agents. Build semantic knowledge graphs from your codebase for surgical code modifications.",

@@ -32,2 +32,3 @@ "main": "dist/index.js",

},
"funding": "https://github.com/sponsors/rupam2232",
"publishConfig": {

@@ -40,3 +41,3 @@ "access": "public"

"engines": {
"node": ">=18.0.0"
"node": ">=18.14.0"
},

@@ -43,0 +44,0 @@ "type": "module",

@@ -9,2 +9,4 @@ # Geraph

**Prerequisite:** Node.js `>=18.14.0` is required.
Run the following commands to install geraph cli, platform specific rules and build the graph:

@@ -11,0 +13,0 @@