clast-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.claude-plugin/plugin.json +22 -0
  2. package/.mcp.json +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +419 -0
  5. package/commands/clast-status.md +9 -0
  6. package/dist/config/defaults.d.ts +19 -0
  7. package/dist/config/defaults.d.ts.map +1 -0
  8. package/dist/config/defaults.js +44 -0
  9. package/dist/config/defaults.js.map +1 -0
  10. package/dist/config/index.d.ts +5 -0
  11. package/dist/config/index.d.ts.map +1 -0
  12. package/dist/config/index.js +48 -0
  13. package/dist/config/index.js.map +1 -0
  14. package/dist/db/index.d.ts +16 -0
  15. package/dist/db/index.d.ts.map +1 -0
  16. package/dist/db/index.js +54 -0
  17. package/dist/db/index.js.map +1 -0
  18. package/dist/db/queries.d.ts +44 -0
  19. package/dist/db/queries.d.ts.map +1 -0
  20. package/dist/db/queries.js +251 -0
  21. package/dist/db/queries.js.map +1 -0
  22. package/dist/db/schema.d.ts +3 -0
  23. package/dist/db/schema.d.ts.map +1 -0
  24. package/dist/db/schema.js +94 -0
  25. package/dist/db/schema.js.map +1 -0
  26. package/dist/db/types.d.ts +44 -0
  27. package/dist/db/types.d.ts.map +1 -0
  28. package/dist/db/types.js +2 -0
  29. package/dist/db/types.js.map +1 -0
  30. package/dist/llm/comment-generator.d.ts +26 -0
  31. package/dist/llm/comment-generator.d.ts.map +1 -0
  32. package/dist/llm/comment-generator.js +106 -0
  33. package/dist/llm/comment-generator.js.map +1 -0
  34. package/dist/llm/index.d.ts +24 -0
  35. package/dist/llm/index.d.ts.map +1 -0
  36. package/dist/llm/index.js +54 -0
  37. package/dist/llm/index.js.map +1 -0
  38. package/dist/llm/prompts.d.ts +9 -0
  39. package/dist/llm/prompts.d.ts.map +1 -0
  40. package/dist/llm/prompts.js +79 -0
  41. package/dist/llm/prompts.js.map +1 -0
  42. package/dist/parser/call-resolver.d.ts +15 -0
  43. package/dist/parser/call-resolver.d.ts.map +1 -0
  44. package/dist/parser/call-resolver.js +16 -0
  45. package/dist/parser/call-resolver.js.map +1 -0
  46. package/dist/parser/extractor.d.ts +17 -0
  47. package/dist/parser/extractor.d.ts.map +1 -0
  48. package/dist/parser/extractor.js +254 -0
  49. package/dist/parser/extractor.js.map +1 -0
  50. package/dist/parser/index.d.ts +24 -0
  51. package/dist/parser/index.d.ts.map +1 -0
  52. package/dist/parser/index.js +190 -0
  53. package/dist/parser/index.js.map +1 -0
  54. package/dist/parser/languages.d.ts +11 -0
  55. package/dist/parser/languages.d.ts.map +1 -0
  56. package/dist/parser/languages.js +74 -0
  57. package/dist/parser/languages.js.map +1 -0
  58. package/dist/server/index.d.ts +3 -0
  59. package/dist/server/index.d.ts.map +1 -0
  60. package/dist/server/index.js +88 -0
  61. package/dist/server/index.js.map +1 -0
  62. package/dist/server/register-tools.d.ts +7 -0
  63. package/dist/server/register-tools.d.ts.map +1 -0
  64. package/dist/server/register-tools.js +17 -0
  65. package/dist/server/register-tools.js.map +1 -0
  66. package/dist/server/tools/clast-call-graph.d.ts +4 -0
  67. package/dist/server/tools/clast-call-graph.d.ts.map +1 -0
  68. package/dist/server/tools/clast-call-graph.js +70 -0
  69. package/dist/server/tools/clast-call-graph.js.map +1 -0
  70. package/dist/server/tools/clast-comment.d.ts +5 -0
  71. package/dist/server/tools/clast-comment.d.ts.map +1 -0
  72. package/dist/server/tools/clast-comment.js +64 -0
  73. package/dist/server/tools/clast-comment.js.map +1 -0
  74. package/dist/server/tools/clast-file-summary.d.ts +4 -0
  75. package/dist/server/tools/clast-file-summary.d.ts.map +1 -0
  76. package/dist/server/tools/clast-file-summary.js +42 -0
  77. package/dist/server/tools/clast-file-summary.js.map +1 -0
  78. package/dist/server/tools/clast-get-context.d.ts +4 -0
  79. package/dist/server/tools/clast-get-context.d.ts.map +1 -0
  80. package/dist/server/tools/clast-get-context.js +80 -0
  81. package/dist/server/tools/clast-get-context.js.map +1 -0
  82. package/dist/server/tools/clast-reindex.d.ts +5 -0
  83. package/dist/server/tools/clast-reindex.d.ts.map +1 -0
  84. package/dist/server/tools/clast-reindex.js +36 -0
  85. package/dist/server/tools/clast-reindex.js.map +1 -0
  86. package/dist/server/tools/clast-search.d.ts +4 -0
  87. package/dist/server/tools/clast-search.d.ts.map +1 -0
  88. package/dist/server/tools/clast-search.js +29 -0
  89. package/dist/server/tools/clast-search.js.map +1 -0
  90. package/dist/server/tools/clast-status.d.ts +5 -0
  91. package/dist/server/tools/clast-status.d.ts.map +1 -0
  92. package/dist/server/tools/clast-status.js +23 -0
  93. package/dist/server/tools/clast-status.js.map +1 -0
  94. package/dist/types.d.ts +52 -0
  95. package/dist/types.d.ts.map +1 -0
  96. package/dist/types.js +2 -0
  97. package/dist/types.js.map +1 -0
  98. package/dist/watcher/gitignore.d.ts +9 -0
  99. package/dist/watcher/gitignore.d.ts.map +1 -0
  100. package/dist/watcher/gitignore.js +38 -0
  101. package/dist/watcher/gitignore.js.map +1 -0
  102. package/dist/watcher/index.d.ts +23 -0
  103. package/dist/watcher/index.d.ts.map +1 -0
  104. package/dist/watcher/index.js +111 -0
  105. package/dist/watcher/index.js.map +1 -0
  106. package/package.json +65 -0
  107. package/skills/clast-explore/SKILL.md +41 -0
  108. package/src/config/defaults.ts +61 -0
  109. package/src/config/index.ts +55 -0
  110. package/src/db/index.ts +63 -0
  111. package/src/db/queries.ts +332 -0
  112. package/src/db/schema.ts +106 -0
  113. package/src/db/types.ts +47 -0
  114. package/src/llm/comment-generator.ts +131 -0
  115. package/src/llm/index.ts +80 -0
  116. package/src/llm/prompts.ts +95 -0
  117. package/src/parser/call-resolver.ts +17 -0
  118. package/src/parser/extractor.ts +303 -0
  119. package/src/parser/index.ts +226 -0
  120. package/src/parser/languages.ts +86 -0
  121. package/src/server/index.ts +109 -0
  122. package/src/server/register-tools.ts +30 -0
  123. package/src/server/tools/clast-call-graph.ts +82 -0
  124. package/src/server/tools/clast-comment.ts +76 -0
  125. package/src/server/tools/clast-file-summary.ts +59 -0
  126. package/src/server/tools/clast-get-context.ts +97 -0
  127. package/src/server/tools/clast-reindex.ts +52 -0
  128. package/src/server/tools/clast-search.ts +38 -0
  129. package/src/server/tools/clast-status.ts +37 -0
  130. package/src/types.ts +69 -0
  131. package/src/watcher/gitignore.ts +42 -0
  132. package/src/watcher/index.ts +131 -0
  133. package/tsconfig.json +20 -0
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: clast-explore
3
+ description: >
4
+ Use when exploring or understanding code structure: finding functions, tracing call graphs,
5
+ understanding symbol context before editing, or generating documentation. Triggers when Claude
6
+ needs to navigate a codebase, find definitions, understand relationships between functions,
7
+ or prepare context for code modifications.
8
+ version: 0.1.0
9
+ ---
10
+
11
+ # Clast Code Intelligence
12
+
13
+ This project has Clast AST indexing active. Before reading entire files or using grep to find
14
+ code, prefer using Clast tools for faster, more structured results:
15
+
16
+ ## Available Tools
17
+
18
+ 1. **clast_search** — Find functions, classes, methods by name pattern.
19
+ Use this BEFORE grep when looking for a symbol definition.
20
+
21
+ 2. **clast_get_context** — Get full context for a symbol: definition, comments, who calls it,
22
+ what it calls. Use this BEFORE reading a file to understand a specific function or class.
23
+
24
+ 3. **clast_call_graph** — Trace the call chain: what calls a function (callers) and what it
25
+ calls (callees). Use this before refactoring to understand impact.
26
+
27
+ 4. **clast_file_summary** — Get all symbols in a file grouped by type. Use this instead of
28
+ reading a whole file when you just need to understand its structure.
29
+
30
+ 5. **clast_status** — Check if the index is up to date.
31
+
32
+ 6. **clast_reindex** — Force re-index if data seems stale.
33
+
34
+ 7. **clast_comment** — Get or generate documentation for a function/class.
35
+
36
+ ## Workflow Tips
37
+
38
+ - When asked to edit a function: use `clast_get_context` first to understand it, then edit.
39
+ - When asked to refactor: use `clast_call_graph` to understand the blast radius.
40
+ - When exploring unfamiliar code: use `clast_file_summary` on key files.
41
+ - When adding features: use `clast_search` to find related existing code.
@@ -0,0 +1,61 @@
1
+ export interface ClastConfig {
2
+ languages: string[];
3
+ ignoredPaths: string[];
4
+ dbPath: string;
5
+ llm: {
6
+ endpoint: string;
7
+ model: string;
8
+ apiKey: string;
9
+ maxConcurrent: number;
10
+ alwaysGenerate: boolean;
11
+ };
12
+ watch: {
13
+ debounceMs: number;
14
+ enabled: boolean;
15
+ };
16
+ maxBodySize: number;
17
+ }
18
+
19
+ export const DEFAULT_CONFIG: ClastConfig = {
20
+ languages: [
21
+ 'typescript',
22
+ 'javascript',
23
+ 'python',
24
+ 'java',
25
+ 'csharp',
26
+ 'go',
27
+ 'rust',
28
+ 'c',
29
+ 'cpp',
30
+ 'ruby',
31
+ 'php',
32
+ ],
33
+ ignoredPaths: [
34
+ 'node_modules',
35
+ '.git',
36
+ 'dist',
37
+ 'build',
38
+ 'out',
39
+ '__pycache__',
40
+ '.venv',
41
+ 'venv',
42
+ '.clast',
43
+ 'vendor',
44
+ 'target',
45
+ '.next',
46
+ 'coverage',
47
+ ],
48
+ dbPath: '.clast/clast.db',
49
+ llm: {
50
+ endpoint: 'http://localhost:11434/v1',
51
+ model: '',
52
+ apiKey: '',
53
+ maxConcurrent: 3,
54
+ alwaysGenerate: false,
55
+ },
56
+ watch: {
57
+ debounceMs: 300,
58
+ enabled: true,
59
+ },
60
+ maxBodySize: 2000,
61
+ };
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ import { readFileSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { DEFAULT_CONFIG, type ClastConfig } from './defaults.js';
5
+
6
+ const LlmConfigSchema = z.object({
7
+ endpoint: z.string().default(DEFAULT_CONFIG.llm.endpoint),
8
+ model: z.string().default(DEFAULT_CONFIG.llm.model),
9
+ apiKey: z.string().default(DEFAULT_CONFIG.llm.apiKey),
10
+ maxConcurrent: z.number().int().min(1).max(10).default(DEFAULT_CONFIG.llm.maxConcurrent),
11
+ alwaysGenerate: z.boolean().default(DEFAULT_CONFIG.llm.alwaysGenerate),
12
+ });
13
+
14
+ const WatchConfigSchema = z.object({
15
+ debounceMs: z.number().int().min(50).max(5000).default(DEFAULT_CONFIG.watch.debounceMs),
16
+ enabled: z.boolean().default(DEFAULT_CONFIG.watch.enabled),
17
+ });
18
+
19
+ const ClastConfigSchema = z.object({
20
+ languages: z.array(z.string()).default(DEFAULT_CONFIG.languages),
21
+ ignoredPaths: z.array(z.string()).default(DEFAULT_CONFIG.ignoredPaths),
22
+ dbPath: z.string().default(DEFAULT_CONFIG.dbPath),
23
+ llm: LlmConfigSchema.default(DEFAULT_CONFIG.llm),
24
+ watch: WatchConfigSchema.default(DEFAULT_CONFIG.watch),
25
+ maxBodySize: z.number().int().min(100).max(50000).default(DEFAULT_CONFIG.maxBodySize),
26
+ });
27
+
28
+ export type { ClastConfig } from './defaults.js';
29
+
30
+ export function loadConfig(projectRoot: string): ClastConfig {
31
+ const candidates = [
32
+ join(projectRoot, 'clast.config.json'),
33
+ join(projectRoot, '.claude', 'clast.config.json'),
34
+ ];
35
+
36
+ for (const candidate of candidates) {
37
+ if (existsSync(candidate)) {
38
+ try {
39
+ const raw = JSON.parse(readFileSync(candidate, 'utf-8'));
40
+ return ClastConfigSchema.parse(raw) as ClastConfig;
41
+ } catch (err) {
42
+ console.error(`[clast] Failed to parse config at ${candidate}:`, err);
43
+ }
44
+ }
45
+ }
46
+
47
+ return ClastConfigSchema.parse({}) as ClastConfig;
48
+ }
49
+
50
+ export function resolveDbPath(projectRoot: string, config: ClastConfig): string {
51
+ if (config.dbPath.startsWith('/') || config.dbPath.match(/^[a-zA-Z]:\\/)) {
52
+ return config.dbPath;
53
+ }
54
+ return join(projectRoot, config.dbPath);
55
+ }
@@ -0,0 +1,63 @@
1
+ import initSqlJs, { type Database } from 'sql.js';
2
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { initializeSchema } from './schema.js';
5
+ import { Queries } from './queries.js';
6
+
7
+ export class ClastDatabase {
8
+ private db: Database;
9
+ private dbPath: string;
10
+ private saveTimer: ReturnType<typeof setInterval> | null = null;
11
+ public queries: Queries;
12
+
13
+ private constructor(db: Database, dbPath: string) {
14
+ this.db = db;
15
+ this.dbPath = dbPath;
16
+ initializeSchema(this.db);
17
+ this.queries = new Queries(this.db);
18
+
19
+ // Auto-save every 10 seconds
20
+ this.saveTimer = setInterval(() => this.save(), 10000);
21
+ }
22
+
23
+ static async open(dbPath: string): Promise<ClastDatabase> {
24
+ mkdirSync(dirname(dbPath), { recursive: true });
25
+
26
+ const SQL = await initSqlJs();
27
+
28
+ let db: Database;
29
+ if (existsSync(dbPath)) {
30
+ const buffer = readFileSync(dbPath);
31
+ db = new SQL.Database(buffer);
32
+ } else {
33
+ db = new SQL.Database();
34
+ }
35
+
36
+ return new ClastDatabase(db, dbPath);
37
+ }
38
+
39
+ save(): void {
40
+ try {
41
+ const data = this.db.export();
42
+ writeFileSync(this.dbPath, Buffer.from(data));
43
+ } catch (err) {
44
+ console.error('[clast] Failed to save database:', err);
45
+ }
46
+ }
47
+
48
+ close(): void {
49
+ if (this.saveTimer) {
50
+ clearInterval(this.saveTimer);
51
+ this.saveTimer = null;
52
+ }
53
+ this.save();
54
+ this.db.close();
55
+ }
56
+
57
+ get raw(): Database {
58
+ return this.db;
59
+ }
60
+ }
61
+
62
+ export { Queries } from './queries.js';
63
+ export type { FileRow, AstNodeRow, CallEdgeRow, CommentRow, StatusRow } from './types.js';
@@ -0,0 +1,332 @@
1
+ import type { Database } from 'sql.js';
2
+ import type { FileRow, AstNodeRow, CallEdgeRow, CommentRow, StatusRow } from './types.js';
3
+
4
+ export class Queries {
5
+ private db: Database;
6
+
7
+ constructor(db: Database) {
8
+ this.db = db;
9
+ }
10
+
11
+ private run(sql: string, params: unknown[] = []): void {
12
+ this.db.run(sql, params as (string | number | null | Uint8Array)[]);
13
+ }
14
+
15
+ private get<T>(sql: string, params: unknown[] = []): T | undefined {
16
+ const result = this.db.exec(sql, params as (string | number | null | Uint8Array)[]);
17
+ if (result.length === 0 || result[0]!.values.length === 0) return undefined;
18
+ const columns = result[0]!.columns;
19
+ const values = result[0]!.values[0]!;
20
+ const obj: Record<string, unknown> = {};
21
+ for (let i = 0; i < columns.length; i++) {
22
+ obj[columns[i]!] = values[i];
23
+ }
24
+ return obj as T;
25
+ }
26
+
27
+ private all<T>(sql: string, params: unknown[] = []): T[] {
28
+ const result = this.db.exec(sql, params as (string | number | null | Uint8Array)[]);
29
+ if (result.length === 0) return [];
30
+ const columns = result[0]!.columns;
31
+ return result[0]!.values.map(values => {
32
+ const obj: Record<string, unknown> = {};
33
+ for (let i = 0; i < columns.length; i++) {
34
+ obj[columns[i]!] = values[i];
35
+ }
36
+ return obj as T;
37
+ });
38
+ }
39
+
40
+ private getLastInsertId(): number {
41
+ const result = this.db.exec('SELECT last_insert_rowid() as id');
42
+ if (result.length > 0 && result[0]!.values.length > 0) {
43
+ return Number(result[0]!.values[0]![0]);
44
+ }
45
+ return 0;
46
+ }
47
+
48
+ // --- File operations ---
49
+
50
+ upsertFile(filePath: string, hash: string, language: string, nodeCount: number): void {
51
+ this.run(
52
+ `INSERT OR REPLACE INTO files (file_path, hash, language, last_indexed, node_count)
53
+ VALUES (?, ?, ?, ?, ?)`,
54
+ [filePath, hash, language, Date.now(), nodeCount]
55
+ );
56
+ }
57
+
58
+ deleteFile(filePath: string): void {
59
+ // Manually cascade since sql.js foreign key cascade can be unreliable
60
+ this.run(`DELETE FROM comments WHERE node_id IN (SELECT id FROM ast_nodes WHERE file_path = ?)`, [filePath]);
61
+ this.run(`DELETE FROM call_edges WHERE caller_id IN (SELECT id FROM ast_nodes WHERE file_path = ?)`, [filePath]);
62
+ this.run(`DELETE FROM call_edges WHERE callee_id IN (SELECT id FROM ast_nodes WHERE file_path = ?)`, [filePath]);
63
+ this.run(`DELETE FROM ast_nodes WHERE file_path = ?`, [filePath]);
64
+ this.run(`DELETE FROM files WHERE file_path = ?`, [filePath]);
65
+ }
66
+
67
+ getFileHash(filePath: string): string | null {
68
+ const row = this.get<{ hash: string }>(`SELECT hash FROM files WHERE file_path = ?`, [filePath]);
69
+ return row?.hash ?? null;
70
+ }
71
+
72
+ getAllFiles(): FileRow[] {
73
+ return this.all<FileRow>(`SELECT * FROM files`);
74
+ }
75
+
76
+ // --- Node operations ---
77
+
78
+ insertNode(node: Omit<AstNodeRow, 'id'>): number {
79
+ this.run(
80
+ `INSERT INTO ast_nodes (file_path, name, type, signature, start_line, end_line, start_col, end_col, parent_id, language, body_text)
81
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
82
+ [
83
+ node.file_path, node.name, node.type, node.signature,
84
+ node.start_line, node.end_line, node.start_col, node.end_col,
85
+ node.parent_id, node.language, node.body_text,
86
+ ]
87
+ );
88
+ return this.getLastInsertId();
89
+ }
90
+
91
+ insertNodes(nodes: Omit<AstNodeRow, 'id'>[]): number[] {
92
+ const ids: number[] = [];
93
+ this.run('BEGIN TRANSACTION');
94
+ try {
95
+ for (const node of nodes) {
96
+ ids.push(this.insertNode(node));
97
+ }
98
+ this.run('COMMIT');
99
+ } catch (err) {
100
+ this.run('ROLLBACK');
101
+ throw err;
102
+ }
103
+ return ids;
104
+ }
105
+
106
+ getNodeById(id: number): AstNodeRow | undefined {
107
+ return this.get<AstNodeRow>(`SELECT * FROM ast_nodes WHERE id = ?`, [id]);
108
+ }
109
+
110
+ getNodeByName(name: string): AstNodeRow[] {
111
+ return this.all<AstNodeRow>(
112
+ `SELECT * FROM ast_nodes WHERE name = ? ORDER BY file_path, start_line`,
113
+ [name]
114
+ );
115
+ }
116
+
117
+ getNodesByFile(filePath: string): AstNodeRow[] {
118
+ return this.all<AstNodeRow>(
119
+ `SELECT * FROM ast_nodes WHERE file_path = ? ORDER BY start_line`,
120
+ [filePath]
121
+ );
122
+ }
123
+
124
+ searchNodes(query: string, type?: string, limit: number = 20): AstNodeRow[] {
125
+ const pattern = `%${query}%`;
126
+ if (type) {
127
+ return this.all<AstNodeRow>(
128
+ `SELECT * FROM ast_nodes WHERE name LIKE ? AND type = ? LIMIT ?`,
129
+ [pattern, type, limit]
130
+ );
131
+ }
132
+ return this.all<AstNodeRow>(
133
+ `SELECT * FROM ast_nodes WHERE name LIKE ? LIMIT ?`,
134
+ [pattern, limit]
135
+ );
136
+ }
137
+
138
+ // --- Edge operations ---
139
+
140
+ insertEdge(edge: Omit<CallEdgeRow, 'id'>): number {
141
+ this.run(
142
+ `INSERT INTO call_edges (caller_id, callee_id, callee_name, file_path, line)
143
+ VALUES (?, ?, ?, ?, ?)`,
144
+ [edge.caller_id, edge.callee_id, edge.callee_name, edge.file_path, edge.line]
145
+ );
146
+ return this.getLastInsertId();
147
+ }
148
+
149
+ insertEdges(edges: Omit<CallEdgeRow, 'id'>[]): void {
150
+ this.run('BEGIN TRANSACTION');
151
+ try {
152
+ for (const edge of edges) {
153
+ this.insertEdge(edge);
154
+ }
155
+ this.run('COMMIT');
156
+ } catch (err) {
157
+ this.run('ROLLBACK');
158
+ throw err;
159
+ }
160
+ }
161
+
162
+ getCallersOf(nameOrId: string | number, depth: number = 2): AstNodeRow[] {
163
+ const whereClause = typeof nameOrId === 'number'
164
+ ? 'callee_id = ?'
165
+ : 'callee_name = ?';
166
+
167
+ const sql = `
168
+ WITH RECURSIVE caller_chain(id, depth) AS (
169
+ SELECT caller_id, 1
170
+ FROM call_edges
171
+ WHERE ${whereClause}
172
+ UNION
173
+ SELECT ce.caller_id, cc.depth + 1
174
+ FROM call_edges ce
175
+ JOIN caller_chain cc ON ce.callee_id = cc.id
176
+ WHERE cc.depth < ?
177
+ )
178
+ SELECT DISTINCT an.* FROM ast_nodes an
179
+ JOIN caller_chain cc ON an.id = cc.id
180
+ ORDER BY an.file_path, an.start_line
181
+ `;
182
+ return this.all<AstNodeRow>(sql, [nameOrId, depth]);
183
+ }
184
+
185
+ getCalleesOf(nameOrId: string | number, depth: number = 2): AstNodeRow[] {
186
+ const whereClause = typeof nameOrId === 'number'
187
+ ? 'an.id = ?'
188
+ : 'an.name = ?';
189
+
190
+ const sql = `
191
+ WITH RECURSIVE callee_chain(id, name, depth) AS (
192
+ SELECT ce.callee_id, ce.callee_name, 1
193
+ FROM call_edges ce
194
+ JOIN ast_nodes an ON ce.caller_id = an.id
195
+ WHERE ${whereClause}
196
+ UNION
197
+ SELECT ce2.callee_id, ce2.callee_name, cc.depth + 1
198
+ FROM call_edges ce2
199
+ JOIN callee_chain cc ON ce2.caller_id = cc.id
200
+ WHERE cc.depth < ? AND cc.id IS NOT NULL
201
+ )
202
+ SELECT DISTINCT an.* FROM ast_nodes an
203
+ JOIN callee_chain cc ON an.id = cc.id
204
+ WHERE cc.id IS NOT NULL
205
+ ORDER BY an.file_path, an.start_line
206
+ `;
207
+ return this.all<AstNodeRow>(sql, [nameOrId, depth]);
208
+ }
209
+
210
+ getCallEdgesForNode(nodeId: number): { outgoing: CallEdgeRow[]; incoming: CallEdgeRow[] } {
211
+ const outgoing = this.all<CallEdgeRow>(
212
+ `SELECT * FROM call_edges WHERE caller_id = ?`,
213
+ [nodeId]
214
+ );
215
+ const incoming = this.all<CallEdgeRow>(
216
+ `SELECT * FROM call_edges WHERE callee_id = ?`,
217
+ [nodeId]
218
+ );
219
+ return { outgoing, incoming };
220
+ }
221
+
222
+ // --- Comment operations ---
223
+
224
+ insertComment(nodeId: number, text: string, source: 'original' | 'generated'): number {
225
+ this.run(
226
+ `INSERT INTO comments (node_id, text, source, generated_at)
227
+ VALUES (?, ?, ?, ?)`,
228
+ [nodeId, text, source, source === 'generated' ? Date.now() : null]
229
+ );
230
+ return this.getLastInsertId();
231
+ }
232
+
233
+ getCommentsForNode(nodeId: number): CommentRow[] {
234
+ return this.all<CommentRow>(`SELECT * FROM comments WHERE node_id = ?`, [nodeId]);
235
+ }
236
+
237
+ deleteCommentsBySource(nodeId: number, source: 'original' | 'generated'): void {
238
+ this.run(`DELETE FROM comments WHERE node_id = ? AND source = ?`, [nodeId, source]);
239
+ }
240
+
241
+ // --- Context (combined) ---
242
+
243
+ getNodeWithContext(name: string): {
244
+ nodes: AstNodeRow[];
245
+ comments: CommentRow[];
246
+ callers: AstNodeRow[];
247
+ callees: AstNodeRow[];
248
+ } {
249
+ const nodes = this.getNodeByName(name);
250
+ if (nodes.length === 0) {
251
+ return { nodes: [], comments: [], callers: [], callees: [] };
252
+ }
253
+
254
+ const allComments: CommentRow[] = [];
255
+ for (const node of nodes) {
256
+ allComments.push(...this.getCommentsForNode(node.id));
257
+ }
258
+
259
+ const callers = this.getCallersOf(name, 1);
260
+ const callees = this.getCalleesOf(name, 1);
261
+
262
+ return { nodes, comments: allComments, callers, callees };
263
+ }
264
+
265
+ // --- Status ---
266
+
267
+ getStatus(): StatusRow {
268
+ const result = this.get<StatusRow>(`
269
+ SELECT
270
+ (SELECT COUNT(*) FROM files) as files_indexed,
271
+ (SELECT COUNT(*) FROM ast_nodes) as total_nodes,
272
+ (SELECT COUNT(*) FROM call_edges) as total_edges,
273
+ (SELECT COUNT(*) FROM comments) as total_comments,
274
+ (SELECT MAX(last_indexed) FROM files) as last_indexed
275
+ `);
276
+ return result ?? {
277
+ files_indexed: 0,
278
+ total_nodes: 0,
279
+ total_edges: 0,
280
+ total_comments: 0,
281
+ last_indexed: null,
282
+ };
283
+ }
284
+
285
+ // --- Bulk operations ---
286
+
287
+ deleteNodesForFile(filePath: string): void {
288
+ this.run(`DELETE FROM comments WHERE node_id IN (SELECT id FROM ast_nodes WHERE file_path = ?)`, [filePath]);
289
+ this.run(`DELETE FROM call_edges WHERE caller_id IN (SELECT id FROM ast_nodes WHERE file_path = ?)`, [filePath]);
290
+ this.run(`DELETE FROM ast_nodes WHERE file_path = ?`, [filePath]);
291
+ }
292
+
293
+ resolveEdges(): number {
294
+ // Count unresolved before
295
+ const before = this.get<{ cnt: number }>(
296
+ `SELECT COUNT(*) as cnt FROM call_edges WHERE callee_id IS NULL`
297
+ );
298
+ const beforeCount = before?.cnt ?? 0;
299
+
300
+ this.run(`
301
+ UPDATE call_edges
302
+ SET callee_id = (
303
+ SELECT an.id FROM ast_nodes an
304
+ WHERE an.name = call_edges.callee_name
305
+ LIMIT 1
306
+ )
307
+ WHERE callee_id IS NULL
308
+ `);
309
+
310
+ const after = this.get<{ cnt: number }>(
311
+ `SELECT COUNT(*) as cnt FROM call_edges WHERE callee_id IS NULL`
312
+ );
313
+ const afterCount = after?.cnt ?? 0;
314
+
315
+ return beforeCount - afterCount;
316
+ }
317
+
318
+ getNodesWithoutComments(limit: number = 100): AstNodeRow[] {
319
+ return this.all<AstNodeRow>(`
320
+ SELECT an.* FROM ast_nodes an
321
+ LEFT JOIN comments c ON an.id = c.node_id
322
+ WHERE c.id IS NULL
323
+ AND an.type IN ('function', 'method', 'class', 'interface', 'struct', 'trait')
324
+ LIMIT ?
325
+ `, [limit]);
326
+ }
327
+
328
+ /** Flush the in-memory database to a Uint8Array for saving to disk. */
329
+ export(): Uint8Array {
330
+ return this.db.export();
331
+ }
332
+ }
@@ -0,0 +1,106 @@
1
+ import type { Database } from 'sql.js';
2
+
3
+ const SCHEMA_VERSION = 1;
4
+
5
+ const CREATE_TABLES = `
6
+ CREATE TABLE IF NOT EXISTS _clast_meta (
7
+ key TEXT PRIMARY KEY,
8
+ value TEXT NOT NULL
9
+ );
10
+
11
+ CREATE TABLE IF NOT EXISTS files (
12
+ file_path TEXT PRIMARY KEY,
13
+ hash TEXT NOT NULL,
14
+ language TEXT NOT NULL,
15
+ last_indexed INTEGER NOT NULL,
16
+ node_count INTEGER NOT NULL DEFAULT 0
17
+ );
18
+
19
+ CREATE TABLE IF NOT EXISTS ast_nodes (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ file_path TEXT NOT NULL,
22
+ name TEXT NOT NULL,
23
+ type TEXT NOT NULL,
24
+ signature TEXT NOT NULL DEFAULT '',
25
+ start_line INTEGER NOT NULL,
26
+ end_line INTEGER NOT NULL,
27
+ start_col INTEGER NOT NULL,
28
+ end_col INTEGER NOT NULL,
29
+ parent_id INTEGER,
30
+ language TEXT NOT NULL,
31
+ body_text TEXT NOT NULL DEFAULT '',
32
+ FOREIGN KEY (file_path) REFERENCES files(file_path) ON DELETE CASCADE,
33
+ FOREIGN KEY (parent_id) REFERENCES ast_nodes(id) ON DELETE CASCADE
34
+ );
35
+
36
+ CREATE INDEX IF NOT EXISTS idx_nodes_name ON ast_nodes(name);
37
+ CREATE INDEX IF NOT EXISTS idx_nodes_type ON ast_nodes(type);
38
+ CREATE INDEX IF NOT EXISTS idx_nodes_file ON ast_nodes(file_path);
39
+ CREATE INDEX IF NOT EXISTS idx_nodes_name_type ON ast_nodes(name, type);
40
+
41
+ CREATE TABLE IF NOT EXISTS call_edges (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ caller_id INTEGER NOT NULL,
44
+ callee_id INTEGER,
45
+ callee_name TEXT NOT NULL,
46
+ file_path TEXT NOT NULL,
47
+ line INTEGER NOT NULL,
48
+ FOREIGN KEY (caller_id) REFERENCES ast_nodes(id) ON DELETE CASCADE,
49
+ FOREIGN KEY (callee_id) REFERENCES ast_nodes(id) ON DELETE SET NULL
50
+ );
51
+
52
+ CREATE INDEX IF NOT EXISTS idx_edges_caller ON call_edges(caller_id);
53
+ CREATE INDEX IF NOT EXISTS idx_edges_callee ON call_edges(callee_id);
54
+ CREATE INDEX IF NOT EXISTS idx_edges_callee_name ON call_edges(callee_name);
55
+
56
+ CREATE TABLE IF NOT EXISTS comments (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ node_id INTEGER NOT NULL,
59
+ text TEXT NOT NULL,
60
+ source TEXT NOT NULL CHECK(source IN ('original', 'generated')),
61
+ generated_at INTEGER,
62
+ FOREIGN KEY (node_id) REFERENCES ast_nodes(id) ON DELETE CASCADE
63
+ );
64
+
65
+ CREATE INDEX IF NOT EXISTS idx_comments_node ON comments(node_id);
66
+ `;
67
+
68
+ export function initializeSchema(db: Database): void {
69
+ db.run('PRAGMA foreign_keys = ON');
70
+
71
+ const currentVersion = getSchemaVersion(db);
72
+
73
+ if (currentVersion < SCHEMA_VERSION) {
74
+ // sql.js doesn't support multi-statement exec with CREATE TABLE IF NOT EXISTS well,
75
+ // so we split and execute each statement
76
+ const statements = CREATE_TABLES.split(';')
77
+ .map(s => s.trim())
78
+ .filter(s => s.length > 0);
79
+
80
+ for (const stmt of statements) {
81
+ db.run(stmt);
82
+ }
83
+ setSchemaVersion(db, SCHEMA_VERSION);
84
+ }
85
+ }
86
+
87
+ function getSchemaVersion(db: Database): number {
88
+ try {
89
+ const result = db.exec(
90
+ `SELECT value FROM _clast_meta WHERE key = 'schema_version'`
91
+ );
92
+ if (result.length > 0 && result[0]!.values.length > 0) {
93
+ return parseInt(String(result[0]!.values[0]![0]), 10);
94
+ }
95
+ return 0;
96
+ } catch {
97
+ return 0;
98
+ }
99
+ }
100
+
101
+ function setSchemaVersion(db: Database, version: number): void {
102
+ db.run(
103
+ `INSERT OR REPLACE INTO _clast_meta (key, value) VALUES ('schema_version', ?)`,
104
+ [String(version)]
105
+ );
106
+ }
@@ -0,0 +1,47 @@
1
+ export interface FileRow {
2
+ file_path: string;
3
+ hash: string;
4
+ language: string;
5
+ last_indexed: number;
6
+ node_count: number;
7
+ }
8
+
9
+ export interface AstNodeRow {
10
+ id: number;
11
+ file_path: string;
12
+ name: string;
13
+ type: string;
14
+ signature: string;
15
+ start_line: number;
16
+ end_line: number;
17
+ start_col: number;
18
+ end_col: number;
19
+ parent_id: number | null;
20
+ language: string;
21
+ body_text: string;
22
+ }
23
+
24
+ export interface CallEdgeRow {
25
+ id: number;
26
+ caller_id: number;
27
+ callee_id: number | null;
28
+ callee_name: string;
29
+ file_path: string;
30
+ line: number;
31
+ }
32
+
33
+ export interface CommentRow {
34
+ id: number;
35
+ node_id: number;
36
+ text: string;
37
+ source: 'original' | 'generated';
38
+ generated_at: number | null;
39
+ }
40
+
41
+ export interface StatusRow {
42
+ files_indexed: number;
43
+ total_nodes: number;
44
+ total_edges: number;
45
+ total_comments: number;
46
+ last_indexed: number | null;
47
+ }