Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ models/
site/node_modules
site/.docusaurus
site/build
.mcp.json
.notes/
53 changes: 53 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch",
"site:dev": "cd site && npm start",
"site:build": "cd site && npm run build",
"site:serve": "cd site && npm run serve"
"site:serve": "cd site && npm run serve",
"postinstall": "node scripts/postinstall.js"
},
"repository": {
"type": "git",
Expand All @@ -44,6 +45,8 @@
},
"files": [
"dist/",
"wasm/",
"scripts/postinstall.js",
"README.md"
],
"publishConfig": {
Expand All @@ -69,6 +72,7 @@
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"redis": "^5.11.0",
"tree-sitter": "^0.21.1",
"web-tree-sitter": "^0.26.7",
"ws": "^8.19.0",
"yaml": "^2.8.2",
Expand All @@ -88,6 +92,7 @@
"graphology-types": "^0.24.8",
"jest": "^30.3.0",
"supertest": "^7.2.2",
"tree-sitter-gdscript": "^6.1.0",
"ts-jest": "^29.4.6",
"tsc-alias": "^1.8.16",
"tsx": "^4.21.0",
Expand Down
22 changes: 22 additions & 0 deletions scripts/postinstall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env node
'use strict';

const path = require('path');
const fs = require('fs');

const src = path.join(__dirname, '..', 'wasm', 'tree-sitter-gdscript.wasm');

let wasmDir;
try {
wasmDir = path.join(path.dirname(require.resolve('@vscode/tree-sitter-wasm/package.json')), 'wasm');
} catch {
// @vscode/tree-sitter-wasm not installed yet — skip
process.exit(0);
}

const dest = path.join(wasmDir, 'tree-sitter-gdscript.wasm');

if (!fs.existsSync(dest)) {
fs.copyFileSync(src, dest);
console.log('graphmemory: installed tree-sitter-gdscript.wasm');
}
20 changes: 20 additions & 0 deletions src/graphs/file-lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,26 @@ export const EXT_TO_LANGUAGE: Record<string, string> = {

// Zig
'.zig': 'zig',

// Godot
'.gd': 'gdscript',
'.gdshader': 'glsl',
'.gdshaderinc': 'glsl',
'.tscn': 'godot-scene',
'.escn': 'godot-scene',
'.tres': 'godot-resource',
'.godot': 'godot-project',
'.gdextension': 'godot-extension',

// Shaders
'.glsl': 'glsl',
'.vert': 'glsl',
'.frag': 'glsl',
'.geom': 'glsl',
'.tesc': 'glsl',
'.tese': 'glsl',
'.comp': 'glsl',
'.hlsl': 'glsl',
};

/** Look up language from file extension. Returns null if unknown. */
Expand Down
2 changes: 1 addition & 1 deletion src/lib/multi-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ const SERVER_DEFAULTS: Omit<ServerConfig, 'embedding'> & { embedding: EmbeddingC

const PROJECT_DEFAULTS = {
docsInclude: '**/*.md',
codeInclude: '**/*.{js,ts,jsx,tsx,mjs,mts,cjs,cts}',
codeInclude: '**/*.{js,ts,jsx,tsx,mjs,mts,cjs,cts,gd,gdshader,gdshaderinc,glsl,tscn,escn,tres,godot,gdextension}',
chunkDepth: 4,
};

Expand Down
99 changes: 62 additions & 37 deletions src/lib/parsers/code.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import fs from 'fs';
import path from 'path';
import type { CodeNodeAttributes, CodeEdgeAttributes } from '@/graphs/code-types';
import { parseSource, getMapper, isLanguageSupported } from '@/lib/parsers/languages';
import {
parseSource,
getMapper,
getRegexMapper,
isLanguageSupported,
isRegexLanguageSupported,
} from '@/lib/parsers/languages';
import type {
ExtractedSymbol,
ExtractedEdge,
ExtractedImport,
} from '@/lib/parsers/languages';
import { getLanguage } from '@/graphs/file-lang';

// Strip line and block comments from JSONC, preserving string contents.
Expand Down Expand Up @@ -172,6 +183,18 @@ function resolveAliasImport(specifier: string, fromFile: string, projectDir: str
// Main parser
// ---------------------------------------------------------------------------

function makeFileOnlyResult(fileId: string, mtime: number): ParsedFile {
return {
fileId,
mtime,
nodes: [{
id: fileId,
attrs: makeFileAttrs(fileId, '', '', 1, mtime),
}],
edges: [],
};
}

export async function parseCodeFile(
absolutePath: string,
codeDir: string,
Expand All @@ -183,46 +206,48 @@ export async function parseCodeFile(
const ext = path.extname(absolutePath);
const language = getLanguage(ext);

if (!language || !isLanguageSupported(language)) {
// Unsupported language — return file-only node, no symbols
return {
fileId,
mtime,
nodes: [{
id: fileId,
attrs: makeFileAttrs(fileId, '', '', 1, mtime),
}],
edges: [],
};
if (!language) return makeFileOnlyResult(fileId, mtime);

const treeSitterAvailable = isLanguageSupported(language);
const regexAvailable = !treeSitterAvailable && isRegexLanguageSupported(language);

if (!treeSitterAvailable && !regexAvailable) {
// Language is detected but no parser available — return file-only node.
return makeFileOnlyResult(fileId, mtime);
}

const source = fs.readFileSync(absolutePath, 'utf-8');
const tree = await parseSource(source, language);

if (!tree) {
return {
fileId,
mtime,
nodes: [{
id: fileId,
attrs: makeFileAttrs(fileId, '', '', 1, mtime),
}],
edges: [],
};
}

const rootNode = tree.rootNode;
const mapper = getMapper(language)!;
let symbols, edgeInfos, imports, fileDocComment, importSummary, lastLine;
try {
symbols = mapper.extractSymbols(rootNode);
edgeInfos = mapper.extractEdges(rootNode);
imports = mapper.extractImports(rootNode);
fileDocComment = extractFileDocComment(rootNode);
importSummary = buildImportSummary(rootNode);
lastLine = (rootNode.endPosition?.row ?? 0) + 1;
} finally {
tree.delete();
let symbols: ExtractedSymbol[];
let edgeInfos: ExtractedEdge[];
let imports: ExtractedImport[];
let fileDocComment = '';
let importSummary = '';
let lastLine: number;

if (treeSitterAvailable) {
const tree = await parseSource(source, language);
if (!tree) return makeFileOnlyResult(fileId, mtime);

const rootNode = tree.rootNode;
const mapper = getMapper(language)!;
try {
symbols = mapper.extractSymbols(rootNode);
edgeInfos = mapper.extractEdges(rootNode);
imports = mapper.extractImports(rootNode);
fileDocComment = extractFileDocComment(rootNode);
importSummary = buildImportSummary(rootNode);
lastLine = (rootNode.endPosition?.row ?? 0) + 1;
} finally {
tree.delete();
}
} else {
// Regex fallback path — operates on raw source text.
const mapper = getRegexMapper(language)!;
symbols = mapper.extractSymbols(source);
edgeInfos = mapper.extractEdges(source);
imports = mapper.extractImports(source);
lastLine = source.split(/\r?\n/).length;
}

const nodes: ParsedFile['nodes'] = [];
Expand Down
86 changes: 86 additions & 0 deletions src/lib/parsers/languages/bash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { ExtractedSymbol, ExtractedEdge, ExtractedImport, LanguageMapper } from './types';
import { registerLanguage } from './registry';
import {
type TSNode,
truncate,
startLine,
endLine,
sliceBeforeBody,
buildBody,
getPrecedingDoc,
} from './helpers';

function getDoc(node: TSNode): string {
return getPrecedingDoc(node, ['comment'], '#');
}

function buildSig(node: TSNode): string {
const body = node.childForFieldName('body');
if (!body) return truncate(node.text ?? '');
const header = sliceBeforeBody(node, body);
return truncate(header ?? (node.text ?? '').split('\n')[0]);
}

const bashMapper: LanguageMapper = {
extractSymbols(rootNode: TSNode): ExtractedSymbol[] {
const symbols: ExtractedSymbol[] = [];

function visit(node: TSNode): void {
if (node.type === 'function_definition') {
const nameNode = node.childForFieldName('name');
const name = nameNode?.text ?? '';
if (name) {
const doc = getDoc(node);
symbols.push({
name,
kind: 'function',
signature: buildSig(node),
docComment: doc,
body: buildBody(node, doc),
startLine: startLine(node),
endLine: endLine(node),
isExported: !name.startsWith('_'),
});
return; // don't recurse into function body
}
}
for (const child of node.children ?? []) visit(child);
}

visit(rootNode);
return symbols;
},

extractEdges(_rootNode: TSNode): ExtractedEdge[] {
return [];
},

extractImports(rootNode: TSNode): ExtractedImport[] {
const imports: ExtractedImport[] = [];

function visit(node: TSNode): void {
// source ./foo or . ./foo
if (node.type === 'command') {
const nameNode = node.childForFieldName('name');
if (nameNode?.text === 'source' || nameNode?.text === '.') {
const args = node.childForFieldName('argument');
const arg = args ?? node.namedChildren?.find((c: TSNode) => c.type === 'word');
if (arg) imports.push({ specifier: arg.text });
}
}
for (const child of node.children ?? []) visit(child);
}

visit(rootNode);
return imports;
},
};

let _registered = false;

export function registerBash(): void {
if (_registered) return;
_registered = true;
registerLanguage('shell', 'tree-sitter-bash.wasm', bashMapper);
registerLanguage('bash', 'tree-sitter-bash.wasm', bashMapper);
}
Loading