#!/usr/bin/env node
/**
 * OpenClaw CodeGraph — Code Knowledge Graph
 *
 * Regex-based code symbol extractor with SQLite knowledge graph.
 * Migrated from CodeGraph `src/extraction/tree-sitter.ts`
 *
 * Supports: JS/TS, Python, Go, Rust, Java, Ruby, Shell, C/C++
 *
 * Usage:
 *   node src/codegraph/index.js index <project_dir>
 *   node src/codegraph/index.js search "function name"
 *   node src/codegraph/index.js explore "how to send HTTP requests"
 *   node src/codegraph/index.js callers <symbol_name>
 */

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const Database = require('better-sqlite3');

// ─── Language Grammars (regex-based) ─────────────────────────────

const GRAMMARS = {
  // TypeScript / JavaScript
  '.ts': {
    function: [
      // named function: function foo(...)
      /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g,
      // arrow function: const foo = (...) => ...
      /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/g,
    ],
    class: [
      /(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+(.+?))?\s*\{/g,
    ],
    method: [
      /(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*\{/g,
    ],
    export: [
      /export\s+(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/g,
      /export\s+\{\s*([^}]+)\s*\}/g,
      /export\s+default\s+(?:function|class)?\s*(\w+)?/g,
    ],
    import: [
      /import\s+\{?\s*([^}]+?)\s*\}?\s*from\s+['"]([^'"]+)['"]/g,
      /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
    ],
  },

  '.js': { inherit: '.ts' },
  '.jsx': { inherit: '.ts' },
  '.tsx': { inherit: '.ts' },
  '.mjs': { inherit: '.ts' },

  // Python
  '.py': {
    function: [
      /def\s+(\w+)\s*\(([^)]*)\)/g,
      /async\s+def\s+(\w+)\s*\(([^)]*)\)/g,
    ],
    class: [
      /class\s+(\w+)(?:\(([^)]*)\))?\s*:/g,
    ],
    import: [
      /from\s+(\S+)\s+import\s+(.+)/g,
      /import\s+(\S+)/g,
    ],
    decorator: [
      /@(\w+)/g,
    ],
  },

  // Rust
  '.rs': {
    function: [
      /(?:pub(?:\s*\(\s*crate\s*\))?\s+)?(?:async\s+)?fn\s+(\w+)\s*\(([^)]*)\)/g,
    ],
    struct: [/(?:pub\s+)?struct\s+(\w+)/g],
    enum: [/(?:pub\s+)?enum\s+(\w+)/g],
    trait: [/(?:pub\s+)?trait\s+(\w+)/g],
    impl: [/impl\s+(?:(\w+)\s+for\s+)?(\w+)/g],
    import: [/use\s+(.+?);/g],
  },

  // Go
  '.go': {
    function: [
      /func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?(\w+)\s*\(([^)]*)\)/g,
    ],
    struct: [/type\s+(\w+)\s+struct\s*\{/g],
    interface: [/type\s+(\w+)\s+interface\s*\{/g],
    import: [/import\s+\(\s*((?:.|\n)*?)\s*\)/g, /import\s+"([^"]+)"/g],
  },

  // Java
  '.java': {
    class: [
      /(?:public|private|protected)?\s*(?:abstract|final)?\s*class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+(.+?))?\s*\{/g,
    ],
    method: [
      /(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)?(?:\w+)\s+(\w+)\s*\(([^)]*)\)/g,
    ],
    import: [/import\s+([^;]+);/g],
  },

  // Ruby
  '.rb': {
    function: [/def\s+(?:self\.)?(\w+)(?:\(([^)]*)\))?/g],
    class: [/class\s+(\w+)(?:\s*<\s*(\w+))?/g],
    module: [/module\s+(\w+)/g],
    require: [/require\s+['"]([^'"]+)['"]/g],
  },

  // Shell
  '.sh': {
    function: [/function\s+(\w+)\s*\{/g, /^(\w+)\s*\(\s*\)\s*\{/gm],
    source: [/(?:source|\.)\s+([^\s;]+)/g],
  },

  // C / C++
  '.c': { inherit: '.cpp' },
  '.h': { inherit: '.cpp' },
  '.cpp': {
    function: [
      /(?:static\s+)?(?:inline\s+)?(?:virtual\s+)?(?:\w+(?:::?\w+)*)\s+(\w+)\s*\(([^)]*)\)/g,
    ],
    class: [
      /(?:class|struct)\s+(\w+)(?:\s*(?:::|:)\s*(?:public|private|protected)?\s*(\w+))?\s*\{/g,
    ],
    include: [/#include\s+[<"]([^>"]+)[>"]/g],
  },
};

// ─── File Discovery ───────────────────────────────────────────────

function discoverFiles(rootDir, options = {}) {
  const { exclude = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', 'target', '.venv', 'vendor'] } = options;
  const extensions = new Set(Object.keys(GRAMMARS));
  const files = [];

  function walk(dir) {
    let entries;
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
    catch { return; }

    for (const entry of entries) {
      if (exclude.includes(entry.name)) continue;
      if (entry.name.startsWith('.')) continue; // skip dotfiles (except .ts, .js, etc.)

      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        walk(fullPath);
      } else if (entry.isFile()) {
        const ext = path.extname(entry.name);
        if (extensions.has(ext) || extensions.has('.' + entry.name)) {
          files.push(fullPath);
        }
      }
    }
  }

  walk(rootDir);
  return files;
}

// ─── File Hashing (for incremental indexing) ─────────────────────

function fileHash(filePath) {
  try {
    const content = fs.readFileSync(filePath);
    return crypto.createHash('md5').update(content).digest('hex').substring(0, 16);
  } catch {
    return null;
  }
}

// ─── Language Detection ──────────────────────────────────────────

function detectLanguage(filePath) {
  const ext = path.extname(filePath);
  const map = {
    '.ts': 'typescript', '.tsx': 'typescript',
    '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript',
    '.py': 'python',
    '.rs': 'rust',
    '.go': 'go',
    '.java': 'java',
    '.rb': 'ruby',
    '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell',
    '.c': 'c', '.h': 'c', '.cpp': 'cpp', '.cc': 'cpp', '.hpp': 'cpp',
    '.sql': 'sql',
    '.json': 'json',
    '.md': 'markdown',
  };
  return map[ext] || ext.replace('.', '') || 'unknown';
}

// ─── CodeGraph Class ──────────────────────────────────────────────

class CodeGraph {
  constructor(dbPath) {
    this.dbPath = dbPath || path.join(process.cwd(), '.codegraph', 'codegraph.db');
    this.db = null;
  }

  /**
   * Open database and initialize schema.
   */
  open() {
    const dir = path.dirname(this.dbPath);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

    this.db = new Database(this.dbPath);
    this.db.pragma('journal_mode = WAL');
    this.db.pragma('foreign_keys = ON');

    // Load schema
    const schemaPath = path.join(__dirname, 'schema.sql');
    const schema = fs.readFileSync(schemaPath, 'utf8');
    this.db.exec(schema);

    return this;
  }

  close() {
    if (this.db) { this.db.close(); this.db = null; }
  }

  /**
   * Index a project directory.
   * @returns {object} { indexed, skipped, errors }
   */
  index(projectDir, options = {}) {
    if (!this.db) this.open();

    const { force = false, onProgress } = options;
    const files = discoverFiles(projectDir);
    const stats = { indexed: 0, skipped: 0, errors: 0 };

    // Get existing file hashes for incremental
    const existingHashes = {};
    if (!force) {
      const rows = this.db.prepare('SELECT path, hash FROM files').all();
      for (const row of rows) existingHashes[row.path] = row.hash;
    }

    // Prepare statements
    const insertFile = this.db.prepare(`
      INSERT INTO files (path, hash, language, size_bytes, line_count, last_indexed)
      VALUES (?, ?, ?, ?, ?, datetime('now'))
      ON CONFLICT(path) DO UPDATE SET
        hash = excluded.hash,
        language = excluded.language,
        size_bytes = excluded.size_bytes,
        line_count = excluded.line_count,
        last_indexed = excluded.last_indexed
    `);
    const insertNode = this.db.prepare(`
      INSERT INTO nodes (kind, name, qualified_name, file_id, line_start, line_end, signature, visibility)
      VALUES (?, ?, ?, ?, ?, ?, ?, 'public')
    `);
    const insertEdge = this.db.prepare(`
      INSERT OR IGNORE INTO edges (from_node_id, to_node_id, kind)
      VALUES (?, ?, ?)
    `);
    const insertUnresolved = this.db.prepare(`
      INSERT INTO unresolved_refs (file_id, name, kind, line_number)
      VALUES (?, ?, 'call', ?)
    `);
    const getFileId = this.db.prepare('SELECT id FROM files WHERE path = ?');

    // Begin transaction for performance
    const indexTransaction = this.db.transaction(() => {
      for (const filePath of files) {
        try {
          const hash = fileHash(filePath);
          if (!hash) { stats.errors++; continue; }

          // Incremental: skip unchanged files
          if (!force && existingHashes[filePath] === hash) {
            stats.skipped++;
            continue;
          }

          const content = fs.readFileSync(filePath, 'utf8');
          const language = detectLanguage(filePath);
          const lineCount = content.split('\n').length;
          const sizeBytes = Buffer.byteLength(content, 'utf8');

          // Get grammar
          const ext = path.extname(filePath);
          let grammar = GRAMMARS[ext] || GRAMMARS['.' + path.basename(filePath)];
          if (!grammar) { stats.skipped++; continue; }

          // Resolve inheritance (e.g., .js inherits .ts grammar)
          if (grammar.inherit) grammar = GRAMMARS[grammar.inherit] || grammar;

          // Remove existing nodes for this file (if re-indexing)
          const oldFileId = getFileId.get(filePath);
          if (oldFileId) {
            this.db.prepare('DELETE FROM nodes WHERE file_id = ?').run(oldFileId.id);
            this.db.prepare('DELETE FROM unresolved_refs WHERE file_id = ?').run(oldFileId.id);
          }

          // Insert file record
          insertFile.run(filePath, hash, language, sizeBytes, lineCount);
          const fileId = getFileId.get(filePath).id;

          const lines = content.split('\n');
          const nodeMap = new Map(); // name+line → node_id, for edge creation later

          // Extract symbols
          for (const [kind, patterns] of Object.entries(grammar)) {
            if (!Array.isArray(patterns)) continue;

            for (const pattern of patterns) {
              let match;
              pattern.lastIndex = 0; // Reset regex state (important for global flag)

              while ((match = pattern.exec(content)) !== null) {
                const name = match[1];
                const params = match[2] || '';

                // Skip if no name captured
                if (!name || name.length > 200) continue;

                // Compute line number
                const pos = match.index;
                const lineStart = content.substring(0, pos).split('\n').length;

                // Compute line end
                let lineEnd = lineStart;
                const blockMatch = content.substring(pos).match(/\{/);
                if (blockMatch) {
                  const blockPos = pos + blockMatch.index;
                  let depth = 0;
                  for (let j = blockPos; j < content.length; j++) {
                    if (content[j] === '{') depth++;
                    if (content[j] === '}') { depth--; if (depth === 0) { lineEnd = content.substring(0, j).split('\n').length; break; } }
                  }
                } else {
                  lineEnd = lineStart;
                }

                // Qualified name (file-based)
                const relPath = path.relative(projectDir, filePath).replace(/\//g, '.').replace(/\.[^.]+$/, '');
                const qualifiedName = kind === 'method' ? `${relPath}.${name}` : `${relPath}.${name}`;

                // Build signature
                let signature = '';
                if (['function', 'method'].includes(kind)) {
                  const sigLine = lines[lineStart - 1]?.trim() || '';
                  signature = sigLine.substring(0, Math.min(sigLine.length, 200));
                } else if (kind === 'class') {
                  signature = `class ${name}` + (params ? `(${params})` : '');
                }

                // Insert node
                insertNode.run(kind, name, qualifiedName, fileId, lineStart, lineEnd, signature || null);
                const nodeId = this.db.prepare('SELECT last_insert_rowid()').get()['last_insert_rowid()'];

                nodeMap.set(`${kind}:${name}:${lineStart}`, { id: nodeId, kind, name, lineStart });

                // Handle imports/extends edges
                if (kind === 'import' && params) {
                  const importedFile = params.replace(/['"]/g, '');
                  this._createImportEdge(fileId, nodeId, importedFile, projectDir);
                }

                if (kind === 'class' && params) {
                  // Record extends relationship as unresolved reference
                  const extendsParts = params.split(',').map(s => s.trim()).filter(Boolean);
                  for (const ext of extendsParts) {
                    insertUnresolved.run(fileId, ext, lineStart);
                  }
                }
              }
            }
          }

          // Build intra-file edges (calls, references)
          this._buildCallEdges(content, fileId, nodeMap, lines, insertEdge, insertUnresolved);

          stats.indexed++;
          if (onProgress) onProgress(filePath, stats);
        } catch (e) {
          stats.errors++;
          console.error(`Error indexing ${filePath}: ${e.message}`);
        }
      }
    });

    indexTransaction();

    // Resolve unresolved references
    this._resolveReferences();

    return stats;
  }

  /**
   * Build call edges within a file.
   */
  _buildCallEdges(content, fileId, nodeMap, lines, insertEdge, insertUnresolved) {
    const callPattern = /(\w+)\s*\(/g;
    let match;

    while ((match = callPattern.exec(content)) !== null) {
      const calledName = match[1];
      if (!calledName || ['if', 'for', 'while', 'switch', 'return', 'new', 'throw', 'typeof'].includes(calledName)) continue;

      const pos = match.index;
      const lineNum = content.substring(0, pos).split('\n').length;

      // Find the enclosing function/method (caller)
      let callerId = null;
      for (const [key, node] of nodeMap) {
        if (['function', 'method'].includes(node.kind) && node.lineStart <= lineNum && lineNum <= (node.lineEnd || node.lineStart + 1)) {
          callerId = node.id;
          break;
        }
      }

      if (!callerId) continue;

      // Find the callee node
      for (const [key, node] of nodeMap) {
        if (node.name === calledName && (node.kind === 'function' || node.kind === 'method')) {
          insertEdge.run(callerId, node.id, 'calls');
          break;
        }
      }

      // Record as unresolved if not found
      if (![...nodeMap.values()].some(n => n.name === calledName)) {
        insertUnresolved.run(fileId, calledName, lineNum);
      }
    }
  }

  /**
   * Create import edge to another file.
   */
  _createImportEdge(fileId, importNodeId, importedPath, projectDir) {
    const resolved = this._resolveImportPath(importedPath, fileId, projectDir);
    if (!resolved) return;

    const existingNode = this.db.prepare('SELECT id FROM nodes WHERE file_id = ? AND kind = ? LIMIT 1')
      .get(resolved.fileId, 'export');

    if (existingNode) {
      this.db.prepare('INSERT OR IGNORE INTO edges (from_node_id, to_node_id, kind) VALUES (?, ?, ?)')
        .run(importNodeId, existingNode.id, 'imports');
    }
  }

  _resolveImportPath(importedPath, fileId, projectDir) {
    const fileRow = this.db.prepare('SELECT path FROM files WHERE id = ?').get(fileId);
    if (!fileRow) return null;
    const fromDir = path.dirname(fileRow.path);

    // Try relative path resolution
    const candidates = [
      importedPath,
      importedPath + '.ts', importedPath + '.js', importedPath + '.tsx', importedPath + '.jsx',
      importedPath + '/index.ts', importedPath + '/index.js',
      path.join(fromDir, importedPath),
      path.join(fromDir, importedPath + '.ts'),
    ];

    for (const cand of candidates) {
      const fileRow = this.db.prepare('SELECT id FROM files WHERE path = ?').get(cand);
      if (fileRow) return { fileId: fileRow.id };
    }

    return null;
  }

  /**
   * Resolve unresolved references by name matching.
   */
  _resolveReferences() {
    const unresolved = this.db.prepare(`
      SELECT ur.id, ur.name, ur.file_id, f.path as file_path
      FROM unresolved_refs ur
      JOIN files f ON f.id = ur.file_id
      WHERE ur.resolved_node_id IS NULL
      LIMIT 1000
    `).all();

    const resolve = this.db.prepare('UPDATE unresolved_refs SET resolved_node_id = ? WHERE id = ?');
    const insertEdge = this.db.prepare('INSERT OR IGNORE INTO edges (from_node_id, to_node_id, kind) VALUES (?, ?, ?)');

    for (const ref of unresolved) {
      // Find matching node by name
      const candidates = this.db.prepare(`
        SELECT id FROM nodes
        WHERE name = ?
        ORDER BY
          CASE WHEN file_id = ? THEN 0 ELSE 1 END,
          CASE WHEN kind = 'export' THEN 0
               WHEN kind = 'function' THEN 1
               WHEN kind = 'class' THEN 2
               ELSE 3 END
        LIMIT 1
      `).all(ref.name, ref.file_id);

      if (candidates.length > 0) {
        resolve.run(candidates[0].id, ref.id);
      }
    }
  }

  // ─── Search ───────────────────────────────────────────────────

  /**
   * Mixed search: FTS5 + exact name + prefix matching.
   */
  search(query, options = {}) {
    const { limit = 20, kind, filePattern } = options;
    if (!this.db) this.open();

    const terms = query.split(/\s+/).filter(t => t.length > 0);

    // Build FTS5 query
    const ftsQuery = terms.map(t => `"${t}"*`).join(' ');
    let sql = `
      SELECT n.id, n.kind, n.name, n.qualified_name, n.signature, n.line_start,
             f.path as file_path, f.language,
             nodes_fts.rank as score
      FROM nodes_fts
      JOIN nodes n ON n.id = nodes_fts.rowid
      JOIN files f ON f.id = n.file_id
      WHERE nodes_fts MATCH ?
    `;
    const params = [ftsQuery];

    if (kind) {
      sql += ' AND n.kind = ?';
      params.push(kind);
    }
    if (filePattern) {
      sql += ' AND f.path LIKE ?';
      params.push('%' + filePattern.replace(/\*/g, '%') + '%');
    }

    sql += ' ORDER BY score LIMIT ?';
    params.push(limit);

    const results = this.db.prepare(sql).all(...params);

    // If no FTS results, try LIKE
    if (results.length === 0 && terms.length === 1) {
      const likeResults = this.db.prepare(`
        SELECT n.id, n.kind, n.name, n.qualified_name, n.signature, n.line_start,
               f.path as file_path, f.language, 0.5 as score
        FROM nodes n
        JOIN files f ON f.id = n.file_id
        WHERE n.name LIKE ?
        ORDER BY
          CASE WHEN n.name = ? THEN 0
               WHEN n.name LIKE ? THEN 1
               ELSE 2 END
        LIMIT ?
      `).all(`%${terms[0]}%`, terms[0], `${terms[0]}%`, limit);
      return likeResults;
    }

    return results;
  }

  /**
   * Get callers of a symbol.
   */
  getCallers(symbolName, options = {}) {
    const { limit = 50 } = options;

    const nodes = this.db.prepare(`
      SELECT id FROM nodes WHERE name = ? OR name LIKE ?
    `).all(symbolName, `%${symbolName}%`);

    const nodeIds = nodes.map(n => n.id);
    if (nodeIds.length === 0) return [];

    const placeholders = nodeIds.map(() => '?').join(',');
    return this.db.prepare(`
      SELECT DISTINCT n2.id, n2.kind, n2.name, n2.qualified_name, n2.line_start,
             f.path as file_path, f.language
      FROM edges e
      JOIN nodes n2 ON n2.id = e.from_node_id
      JOIN files f ON f.id = n2.file_id
      WHERE e.to_node_id IN (${placeholders}) AND e.kind = 'calls'
      LIMIT ?
    `).all(...nodeIds, limit);
  }

  /**
   * Get callees of a symbol.
   */
  getCallees(symbolName, options = {}) {
    const { limit = 50 } = options;

    const nodes = this.db.prepare(`
      SELECT id FROM nodes WHERE name = ? OR name LIKE ?
    `).all(symbolName, `%${symbolName}%`);

    const nodeIds = nodes.map(n => n.id);
    if (nodeIds.length === 0) return [];

    const placeholders = nodeIds.map(() => '?').join(',');
    return this.db.prepare(`
      SELECT DISTINCT n2.id, n2.kind, n2.name, n2.qualified_name, n2.line_start,
             f.path as file_path, f.language
      FROM edges e
      JOIN nodes n2 ON n2.id = e.to_node_id
      JOIN files f ON f.id = n2.file_id
      WHERE e.from_node_id IN (${placeholders}) AND e.kind = 'calls'
      LIMIT ?
    `).all(...nodeIds, limit);
  }

  /**
   * BFS traversal from a starting node.
   */
  bfs(startId, maxDepth = 2, maxNodes = 100) {
    const visited = new Set();
    const queue = [{ id: startId, depth: 0 }];
    const nodes = [];
    const edges = [];

    while (queue.length > 0 && nodes.length < maxNodes) {
      const { id, depth } = queue.shift();
      if (visited.has(id)) continue;
      visited.add(id);

      const node = this.db.prepare(`
        SELECT n.*, f.path as file_path, f.language
        FROM nodes n JOIN files f ON f.id = n.file_id
        WHERE n.id = ?
      `).get(id);
      if (node) nodes.push(node);

      if (depth >= maxDepth) continue;

      // Follow edges
      const outgoing = this.db.prepare(`
        SELECT to_node_id as neighbor, kind FROM edges WHERE from_node_id = ?
        UNION
        SELECT from_node_id as neighbor, kind FROM edges WHERE to_node_id = ?
      `).all(id, id);

      for (const edge of outgoing) {
        edges.push({ from: id, to: edge.neighbor, kind: edge.kind });
        if (!visited.has(edge.neighbor)) {
          queue.push({ id: edge.neighbor, depth: depth + 1 });
        }
      }
    }

    return { nodes, edges };
  }

  /**
   * Impact analysis: what depends on this symbol?
   */
  getImpact(symbolName, maxDepth = 3) {
    const nodes = this.db.prepare('SELECT id FROM nodes WHERE name = ?').all(symbolName);
    if (nodes.length === 0) return { roots: [], nodes: [], edges: [] };

    return this.bfs(nodes[0].id, maxDepth);
  }

  // ─── Stats ─────────────────────────────────────────────────────

  stats() {
    if (!this.db) this.open();
    return {
      files: this.db.prepare('SELECT COUNT(*) as c FROM files').get().c,
      nodes: this.db.prepare('SELECT COUNT(*) as c FROM nodes').get().c,
      edges: this.db.prepare('SELECT COUNT(*) as c FROM edges').get().c,
      unresolved: this.db.prepare('SELECT COUNT(*) as c FROM unresolved_refs').get().c,
      byKind: this.db.prepare('SELECT kind, COUNT(*) as count FROM nodes GROUP BY kind ORDER BY count DESC').all(),
      byLanguage: this.db.prepare('SELECT language, COUNT(*) as count FROM files GROUP BY language ORDER BY count DESC').all(),
    };
  }

  /**
   * Get file-level dependencies.
   */
  getFileDependencies(filePath) {
    return this.db.prepare(`
      SELECT DISTINCT f2.path as dependency
      FROM edges e
      JOIN nodes n1 ON n1.id = e.from_node_id
      JOIN nodes n2 ON n2.id = e.to_node_id
      JOIN files f1 ON f1.id = n1.file_id
      JOIN files f2 ON f2.id = n2.file_id
      WHERE f1.path = ? AND f2.path != f1.path
    `).all(filePath);
  }
}

// ─── CLI ─────────────────────────────────────────────────────────

function main() {
  const args = process.argv.slice(2);

  if (args.length === 0 || args[0] === '--help') {
    console.log(`CodeGraph — Code Knowledge Graph for OpenClaw

Usage:
  node index.js index <project_dir>            Index a codebase
  node index.js search <query>                 Search for symbols
  node index.js callers <symbol_name>          Find callers
  node index.js callees <symbol_name>          Find callees
  node index.js explore <query>                Deep exploration
  node index.js impact <symbol_name>           Impact analysis
  node index.js stats                          Show index statistics

Options:
  --db <path>        Database path (default: .codegraph/codegraph.db)
  --force            Force re-index (ignore incremental)
  --limit N          Result limit (default: 20)`);
    return;
  }

  const cmd = args[0];
  const dbIdx = args.indexOf('--db');
  const dbPath = dbIdx >= 0 ? args[dbIdx + 1] : null;
  const cg = new CodeGraph(dbPath);
  cg.open();

  try {
    switch (cmd) {
      case 'index': {
        const dir = args[1] || '.';
        const force = args.includes('--force');
        console.log(`Indexing ${dir}...`);
        const stats = cg.index(dir, { force });
        console.log(JSON.stringify(stats, null, 2));
        break;
      }
      case 'search': {
        const query = args.slice(1).join(' ');
        const limit = parseInt(args[args.indexOf('--limit') + 1] || '20');
        const results = cg.search(query, { limit });
        console.log(JSON.stringify(results, null, 2));
        break;
      }
      case 'callers': {
        const name = args[1];
        const results = cg.getCallers(name);
        console.log(JSON.stringify(results, null, 2));
        break;
      }
      case 'callees': {
        const name = args[1];
        const results = cg.getCallees(name);
        console.log(JSON.stringify(results, null, 2));
        break;
      }
      case 'explore': {
        const query = args.slice(1).join(' ');
        const results = cg.search(query, { limit: 10 });
        // For each result, get BFS neighbors
        const exploration = results.map(r => ({
          node: r,
          context: cg.bfs(r.id, 2, 20),
        }));
        console.log(JSON.stringify(exploration, null, 2));
        break;
      }
      case 'impact': {
        const name = args[1];
        const results = cg.getImpact(name);
        console.log(JSON.stringify(results, null, 2));
        break;
      }
      case 'stats': {
        console.log(JSON.stringify(cg.stats(), null, 2));
        break;
      }
      default:
        console.error(`Unknown command: ${cmd}`);
    }
  } finally {
    cg.close();
  }
}

module.exports = { CodeGraph, GRAMMARS, discoverFiles, detectLanguage };

if (require.main === module) {
  main();
}
