@psnext/lscg 0.1.1

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 (42) hide show
  1. package/README.md +143 -0
  2. package/dist/bin/lscg.d.ts +3 -0
  3. package/dist/bin/lscg.js +11 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +524 -0
  6. package/dist/src/config/paths.d.ts +12 -0
  7. package/dist/src/config/paths.js +54 -0
  8. package/dist/src/graph/attribution.d.ts +8 -0
  9. package/dist/src/graph/attribution.js +112 -0
  10. package/dist/src/graph/extract.d.ts +4 -0
  11. package/dist/src/graph/extract.js +199 -0
  12. package/dist/src/graph/repository.d.ts +93 -0
  13. package/dist/src/graph/repository.js +361 -0
  14. package/dist/src/index.d.ts +5 -0
  15. package/dist/src/index.js +5 -0
  16. package/dist/src/mcp/server.d.ts +6 -0
  17. package/dist/src/mcp/server.js +81 -0
  18. package/dist/src/parser/treeSitter.d.ts +13 -0
  19. package/dist/src/parser/treeSitter.js +61 -0
  20. package/dist/src/scanner/discover.d.ts +4 -0
  21. package/dist/src/scanner/discover.js +62 -0
  22. package/dist/src/scanner/fingerprint.d.ts +3 -0
  23. package/dist/src/scanner/fingerprint.js +27 -0
  24. package/dist/src/storage/database.d.ts +72 -0
  25. package/dist/src/storage/database.js +563 -0
  26. package/dist/src/storage/schema.d.ts +4 -0
  27. package/dist/src/storage/schema.js +93 -0
  28. package/dist/src/types.d.ts +233 -0
  29. package/dist/src/types.js +2 -0
  30. package/dist/src/view/index.d.ts +27 -0
  31. package/dist/src/view/index.js +42 -0
  32. package/dist/src/view/layout.d.ts +28 -0
  33. package/dist/src/view/layout.js +235 -0
  34. package/dist/src/view/model.d.ts +64 -0
  35. package/dist/src/view/model.js +396 -0
  36. package/dist/src/view/open.d.ts +15 -0
  37. package/dist/src/view/open.js +37 -0
  38. package/dist/src/view/render.d.ts +9 -0
  39. package/dist/src/view/render.js +321 -0
  40. package/dist/src/watch.d.ts +40 -0
  41. package/dist/src/watch.js +118 -0
  42. package/package.json +65 -0
@@ -0,0 +1,361 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { databasePathForScope, homeDatabasePath, normalizeScope, repoDatabasePath, resolveProjectRoot } from '../config/paths.js';
5
+ import { parseSource } from '../parser/treeSitter.js';
6
+ import { discoverSourceFiles } from '../scanner/discover.js';
7
+ import { collectRepositoryFingerprint } from '../scanner/fingerprint.js';
8
+ import { attachDatabase, closeDatabase, getStatus, openGraphDatabase, replaceFileGraph, runSelect, selectCallGraph, selectEdges, selectNeighbors, selectNodes, selectNodeTextRows, upsertRepository, reconcileDeletedFiles, selectFileInventory, selectContextCandidates, selectContextRelationships } from '../storage/database.js';
9
+ import { extractGraph, hashParts } from './extract.js';
10
+ import { buildFileAttribution } from './attribution.js';
11
+ export function repositoryForRoot(root = process.cwd()) {
12
+ const resolvedRoot = resolveProjectRoot(root);
13
+ return {
14
+ id: hashParts(['repository', resolvedRoot]),
15
+ root: resolvedRoot,
16
+ name: path.basename(resolvedRoot)
17
+ };
18
+ }
19
+ export function initGraph({ root = process.cwd(), scope = 'repo' } = {}) {
20
+ const repository = repositoryForRoot(root);
21
+ const scopes = normalizeScope(scope);
22
+ const initialized = [];
23
+ for (const graphScope of scopes) {
24
+ const databasePath = databasePathForScope(graphScope, repository.root);
25
+ const db = openGraphDatabase(databasePath);
26
+ try {
27
+ upsertRepository(db, repository);
28
+ initialized.push({ scope: graphScope, databasePath });
29
+ }
30
+ finally {
31
+ closeDatabase(db);
32
+ }
33
+ }
34
+ return { repository, initialized };
35
+ }
36
+ export async function scanRepository({ root = process.cwd(), scope = 'repo' } = {}) {
37
+ const repository = repositoryForRoot(root);
38
+ const scopes = normalizeScope(scope);
39
+ const files = discoverSourceFiles(repository.root);
40
+ const opened = scopes.map((graphScope) => {
41
+ const databasePath = databasePathForScope(graphScope, repository.root);
42
+ return { scope: graphScope, databasePath, db: openGraphDatabase(databasePath) };
43
+ });
44
+ let filesScanned = 0;
45
+ let nodesWritten = 0;
46
+ let edgesWritten = 0;
47
+ const skipped = [];
48
+ try {
49
+ for (const handle of opened)
50
+ upsertRepository(handle.db, repository);
51
+ for (const relativePath of files) {
52
+ const absolutePath = path.join(repository.root, relativePath);
53
+ const source = readFileSync(absolutePath, 'utf8');
54
+ const parseResult = parseSource(absolutePath, source);
55
+ if (!parseResult) {
56
+ skipped.push(relativePath);
57
+ continue;
58
+ }
59
+ const stat = statSync(absolutePath);
60
+ const sourceHash = sha256(source);
61
+ const fileId = hashParts([repository.id, relativePath]);
62
+ const file = {
63
+ id: fileId,
64
+ path: relativePath,
65
+ language: parseResult.language,
66
+ hash: sourceHash,
67
+ size: stat.size,
68
+ mtimeMs: Math.round(stat.mtimeMs)
69
+ };
70
+ const graph = extractGraph({
71
+ repositoryId: repository.id,
72
+ fileId,
73
+ relativePath,
74
+ source,
75
+ sourceHash,
76
+ parseResult
77
+ });
78
+ const attribution = buildFileAttribution({
79
+ root: repository.root,
80
+ relativePath,
81
+ source,
82
+ nodes: graph.nodes
83
+ });
84
+ for (const handle of opened) {
85
+ replaceFileGraph(handle.db, { repository, file, nodes: graph.nodes, edges: graph.edges, attribution });
86
+ }
87
+ filesScanned += 1;
88
+ nodesWritten += graph.nodes.length + (attribution?.contributorEmails.length ?? 0);
89
+ edgesWritten += graph.edges.length + (attribution?.nodeAttributions.reduce((sum, entry) => sum + entry.contributorEmails.length, 0) ?? 0);
90
+ }
91
+ }
92
+ finally {
93
+ for (const handle of opened)
94
+ closeDatabase(handle.db);
95
+ }
96
+ return {
97
+ repository,
98
+ scopes: opened.map(({ scope: graphScope, databasePath }) => ({ scope: graphScope, databasePath })),
99
+ filesDiscovered: files.length,
100
+ filesScanned,
101
+ nodesWritten,
102
+ edgesWritten,
103
+ skipped
104
+ };
105
+ }
106
+ export function graphStatus({ root = process.cwd(), scope = 'repo' } = {}) {
107
+ const repository = repositoryForRoot(root);
108
+ return withScopes(repository, scope, (db, graphScope, databasePath) => ({
109
+ scope: graphScope,
110
+ databasePath,
111
+ ...getStatus(db, repository.id)
112
+ }));
113
+ }
114
+ export function listNodes({ root = process.cwd(), scope = 'repo', kind, limit = 50 } = {}) {
115
+ const repository = repositoryForRoot(root);
116
+ return withScopes(repository, scope, (db, graphScope) => selectNodes(db, repository.id, { kind, limit }).map((row) => ({ scope: graphScope, ...row }))).flat();
117
+ }
118
+ export function listNodeText({ root = process.cwd(), scope = 'repo', kind, term, limit = 50 } = {}) {
119
+ const repository = repositoryForRoot(root);
120
+ return withScopes(repository, scope, (db, graphScope) => selectNodeTextRows(db, repository.id, { kind, term, limit }).map((row) => ({
121
+ scope: graphScope,
122
+ root: repository.root,
123
+ ...row,
124
+ path: row.path ? path.resolve(repository.root, row.path) : null
125
+ }))).flat();
126
+ }
127
+ export function listEdges({ root = process.cwd(), scope = 'repo', kind, limit = 50 } = {}) {
128
+ const repository = repositoryForRoot(root);
129
+ return withScopes(repository, scope, (db, graphScope) => selectEdges(db, repository.id, { kind, limit }).map((row) => ({ scope: graphScope, ...row }))).flat();
130
+ }
131
+ export function neighbors({ root = process.cwd(), scope = 'repo', nodeId, depth = 1, limit = 100 } = {}) {
132
+ if (!nodeId)
133
+ throw new Error('neighbors requires a nodeId');
134
+ const repository = repositoryForRoot(root);
135
+ return withScopes(repository, scope, (db, graphScope) => selectNeighbors(db, repository.id, nodeId, { depth, limit }).map((row) => ({ scope: graphScope, ...row }))).flat();
136
+ }
137
+ export function callGraph({ root = process.cwd(), scope = 'repo', term, kind, depth = 5, limit = 100 } = {}) {
138
+ if (!term?.trim())
139
+ throw new Error('callgraph requires a term');
140
+ const repository = repositoryForRoot(root);
141
+ return withScopes(repository, scope, (db, graphScope) => selectCallGraph(db, repository.id, term.trim(), { kind, depth, limit }).map((row) => ({ scope: graphScope, ...row }))).flat();
142
+ }
143
+ const CONTEXT_DEFAULTS = {
144
+ depth: 2,
145
+ relationshipLimit: 50,
146
+ candidateLimit: 20,
147
+ excerptLines: 80,
148
+ excerptBytes: 12_000
149
+ };
150
+ export async function contextGraph(options) {
151
+ if (!options.symbol?.trim())
152
+ throw new Error('context requires a symbol');
153
+ const repository = repositoryForRoot(options.root);
154
+ const scopeValue = options.scope ?? 'repo';
155
+ const budget = {
156
+ depth: clampContext(options.depth, 0, 5, CONTEXT_DEFAULTS.depth),
157
+ relationshipLimit: clampContext(options.limit, 1, 500, CONTEXT_DEFAULTS.relationshipLimit),
158
+ candidateLimit: clampContext(options.candidateLimit, 1, 100, CONTEXT_DEFAULTS.candidateLimit),
159
+ excerptLines: clampContext(options.excerptLines, 1, 500, CONTEXT_DEFAULTS.excerptLines),
160
+ excerptBytes: clampContext(options.excerptBytes, 1, 100_000, CONTEXT_DEFAULTS.excerptBytes)
161
+ };
162
+ const request = {
163
+ symbol: options.symbol.trim(),
164
+ root: repository.root,
165
+ scope: scopeValue,
166
+ ...(options.kind ? { kind: options.kind } : {}),
167
+ ...(options.file ? { file: options.file } : {}),
168
+ budget,
169
+ excerpts: Boolean(options.excerpts)
170
+ };
171
+ const scopes = normalizeScope(scopeValue);
172
+ const scopeResults = [];
173
+ for (const graphScope of scopes) {
174
+ scopeResults.push(await contextForScope(repository, graphScope, request, options.excerpts === true));
175
+ }
176
+ return { contract_version: 1, repository, request, scopes: scopeResults };
177
+ }
178
+ async function contextForScope(repository, graphScope, request, excerpts) {
179
+ const databasePath = databasePathForScope(graphScope, repository.root);
180
+ const discovered = discoverSourceFiles(repository.root);
181
+ const beforeFingerprint = collectRepositoryFingerprint(repository.root);
182
+ let db = openGraphDatabase(databasePath);
183
+ let refresh = {
184
+ outcome: 'reused', filesDiscovered: discovered.length, filesScanned: 0, skipped: []
185
+ };
186
+ let freshness = 'unchanged';
187
+ let priorGraph = false;
188
+ try {
189
+ upsertRepository(db, repository);
190
+ const inventory = selectFileInventory(db, repository.id);
191
+ priorGraph = inventory.length > 0;
192
+ const inventoryChanged = inventory.length !== discovered.length || discovered.some((file, index) => {
193
+ const row = inventory[index];
194
+ if (!row || row.path !== file)
195
+ return true;
196
+ try {
197
+ const stat = statSync(path.join(repository.root, file));
198
+ return row.size !== stat.size || row.mtimeMs !== Math.round(stat.mtimeMs);
199
+ }
200
+ catch {
201
+ return true;
202
+ }
203
+ });
204
+ if (inventoryChanged) {
205
+ closeDatabase(db);
206
+ db = undefined;
207
+ try {
208
+ const summary = await scanRepository({ root: repository.root, scope: graphScope });
209
+ refresh = {
210
+ outcome: summary.skipped.length > 0 ? 'refresh_failed' : 'refreshed',
211
+ filesDiscovered: summary.filesDiscovered,
212
+ filesScanned: summary.filesScanned,
213
+ skipped: summary.skipped
214
+ };
215
+ if (summary.skipped.length > 0)
216
+ return failedContextScope(graphScope, databasePath, refresh, priorGraph);
217
+ const refreshedDb = openGraphDatabase(databasePath);
218
+ try {
219
+ reconcileDeletedFiles(refreshedDb, repository.id, discovered);
220
+ }
221
+ finally {
222
+ closeDatabase(refreshedDb);
223
+ }
224
+ freshness = 'refreshed';
225
+ db = openGraphDatabase(databasePath);
226
+ upsertRepository(db, repository);
227
+ }
228
+ catch (error) {
229
+ refresh = { ...refresh, outcome: 'refresh_failed', skipped: [error instanceof Error ? error.message : String(error)] };
230
+ return failedContextScope(graphScope, databasePath, refresh, priorGraph);
231
+ }
232
+ }
233
+ const afterFingerprint = collectRepositoryFingerprint(repository.root);
234
+ if (afterFingerprint !== beforeFingerprint) {
235
+ return failedContextScope(graphScope, databasePath, refresh, priorGraph, 'stale');
236
+ }
237
+ const candidateRows = selectContextCandidates(db, repository.id, request.symbol, {
238
+ kind: request.kind,
239
+ file: request.file,
240
+ limit: request.budget.candidateLimit + 1
241
+ });
242
+ const candidateTruncated = candidateRows.length > request.budget.candidateLimit;
243
+ const candidates = candidateRows.slice(0, request.budget.candidateLimit);
244
+ const truncation = candidateTruncated
245
+ ? [{ section: 'candidates', reason: 'candidate_limit', omitted: 1 }]
246
+ : [];
247
+ if (candidates.length === 0) {
248
+ return { scope: graphScope, databasePath, result: { state: 'not_found', impact: [], dependencies: [], warnings: [], truncation }, freshness: { state: freshness, verification: 'metadata_only', refresh } };
249
+ }
250
+ if (candidateTruncated || candidates.length !== 1) {
251
+ return { scope: graphScope, databasePath, result: { state: 'ambiguous', candidates: candidates.map(stripCandidate), impact: [], dependencies: [], warnings: [], truncation }, freshness: { state: freshness, verification: 'metadata_only', refresh } };
252
+ }
253
+ const anchor = stripInternalReference(candidates[0]);
254
+ const rawRelations = request.budget.depth === 0
255
+ ? []
256
+ : selectContextRelationships(db, repository.id, anchor.id, request.budget.relationshipLimit, request.budget.depth);
257
+ const impact = [];
258
+ const dependencies = [];
259
+ for (const relation of rawRelations) {
260
+ const source = stripInternalReference(relation.source);
261
+ const target = stripInternalReference(relation.target);
262
+ if (relation.edgeKind === 'calls') {
263
+ impact.push({ category: 'impact', edgeKind: relation.edgeKind, direction: relation.source.id === anchor.id ? 'callee' : 'caller', source, target, confidence: relation.confidence, provenance: relation.metadata, depth: relation.depth });
264
+ }
265
+ else {
266
+ dependencies.push({ category: 'dependency', edgeKind: relation.edgeKind, direction: relation.edgeKind === 'imports' ? 'import' : 'export', source, target, confidence: relation.confidence, provenance: relation.metadata, depth: relation.depth });
267
+ }
268
+ }
269
+ const warnings = candidates[0]?.match === 'substring' ? ['substring_search=bounded_like'] : [];
270
+ let verification = 'metadata_only';
271
+ if (excerpts) {
272
+ const excerptStatus = await addExcerpt(repository.root, anchor, request.budget);
273
+ if (excerptStatus === 'checked')
274
+ verification = 'content_hash';
275
+ if (excerptStatus === 'unavailable')
276
+ warnings.push('excerpt_unavailable');
277
+ if (excerptStatus === 'mismatch')
278
+ warnings.push('excerpt_unavailable:source_hash_mismatch');
279
+ }
280
+ return {
281
+ scope: graphScope,
282
+ databasePath,
283
+ result: { state: 'ok', anchor, definition: anchor, impact, dependencies, warnings, truncation },
284
+ freshness: { state: freshness, verification, refresh }
285
+ };
286
+ }
287
+ finally {
288
+ if (db)
289
+ closeDatabase(db);
290
+ }
291
+ }
292
+ function failedContextScope(scope, databasePath, refresh, priorGeneration, state = 'refresh_failed') {
293
+ return {
294
+ scope,
295
+ databasePath,
296
+ result: { state, impact: [], dependencies: [], warnings: refresh.skipped, truncation: [] },
297
+ freshness: { state: state === 'stale' || priorGeneration ? 'stale' : 'refresh_failed', verification: 'metadata_only', refresh: { ...refresh, outcome: state === 'stale' ? refresh.outcome : 'refresh_failed' } }
298
+ };
299
+ }
300
+ function stripCandidate(candidate) {
301
+ const { fileId: _fileId, ...publicCandidate } = candidate;
302
+ return publicCandidate;
303
+ }
304
+ function stripInternalReference(reference) {
305
+ const { fileId: _fileId, rank: _rank, match: _match, ...publicReference } = reference;
306
+ return publicReference;
307
+ }
308
+ async function addExcerpt(root, anchor, budget) {
309
+ if (!anchor.path || !anchor.sourceHash)
310
+ return 'unavailable';
311
+ try {
312
+ const source = readFileSync(path.join(root, anchor.path), 'utf8');
313
+ if (sha256(source) !== anchor.sourceHash)
314
+ return 'mismatch';
315
+ const lines = source.split(/\r?\n/);
316
+ const start = Math.max(0, anchor.span.start.row);
317
+ const end = Math.min(lines.length, start + budget.excerptLines);
318
+ const bounded = Buffer.from(lines.slice(start, end).join('\\n'), 'utf8').subarray(0, budget.excerptBytes).toString('utf8');
319
+ anchor.excerpt = bounded;
320
+ return 'checked';
321
+ }
322
+ catch {
323
+ return 'unavailable';
324
+ }
325
+ }
326
+ function clampContext(value, min, max, fallback) {
327
+ if (value === undefined || !Number.isFinite(value))
328
+ return fallback;
329
+ return Math.max(min, Math.min(max, Math.trunc(value)));
330
+ }
331
+ export function runReadOnlySql({ root = process.cwd(), scope = 'repo', sql, attachHome = false, limit = 200 } = {}) {
332
+ if (!sql)
333
+ throw new Error('query requires SQL text');
334
+ const repository = repositoryForRoot(root);
335
+ return withScopes(repository, scope, (db, graphScope) => {
336
+ if (attachHome && graphScope === 'repo')
337
+ attachDatabase(db, 'home_graph', homeDatabasePath());
338
+ const rows = runSelect(db, sql, { limit });
339
+ return { scope: graphScope, rows };
340
+ });
341
+ }
342
+ function withScopes(repository, scope, fn) {
343
+ const scopes = normalizeScope(scope);
344
+ const results = [];
345
+ for (const graphScope of scopes) {
346
+ const databasePath = graphScope === 'repo' ? repoDatabasePath(repository.root) : homeDatabasePath();
347
+ const db = openGraphDatabase(databasePath);
348
+ try {
349
+ upsertRepository(db, repository);
350
+ results.push(fn(db, graphScope, databasePath));
351
+ }
352
+ finally {
353
+ closeDatabase(db);
354
+ }
355
+ }
356
+ return results;
357
+ }
358
+ function sha256(value) {
359
+ return createHash('sha256').update(value).digest('hex');
360
+ }
361
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1,5 @@
1
+ export { initGraph, listEdges, listNodes, graphStatus, callGraph, neighbors, contextGraph, runReadOnlySql, scanRepository } from './graph/repository.js';
2
+ export { watchRepository } from './watch.js';
3
+ export { viewGraph, loadViewSnapshot, buildViewModel, renderInteractiveHtml, renderSvgMarkup, openInDefaultBrowser, openHtmlArtifactInBrowser, writeTemporaryHtmlArtifact } from './view/index.js';
4
+ export { repoDatabasePath, homeDatabasePath, resolveProjectRoot } from './config/paths.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ export { initGraph, listEdges, listNodes, graphStatus, callGraph, neighbors, contextGraph, runReadOnlySql, scanRepository } from './graph/repository.js';
2
+ export { watchRepository } from './watch.js';
3
+ export { viewGraph, loadViewSnapshot, buildViewModel, renderInteractiveHtml, renderSvgMarkup, openInDefaultBrowser, openHtmlArtifactInBrowser, writeTemporaryHtmlArtifact } from './view/index.js';
4
+ export { repoDatabasePath, homeDatabasePath, resolveProjectRoot } from './config/paths.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ interface ServerOptions {
2
+ root?: string | undefined;
3
+ }
4
+ export declare function startMcpServer({ root }?: ServerOptions): Promise<void>;
5
+ export {};
6
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { graphStatus, listEdges, listNodes, neighbors, runReadOnlySql, scanRepository } from '../graph/repository.js';
5
+ const scopeSchema = z.enum(['repo', 'home', 'both']).default('repo');
6
+ export async function startMcpServer({ root = process.cwd() } = {}) {
7
+ const server = new McpServer({
8
+ name: 'lscg',
9
+ version: '0.1.0'
10
+ });
11
+ const tool = server.tool.bind(server);
12
+ tool('context_graph_scan', 'Parse repository files with Tree-sitter and write discovered graph nodes/edges to SQLite.', {
13
+ root: z.string().optional().describe('Repository root. Defaults to the MCP server cwd.'),
14
+ scope: scopeSchema.describe('Graph storage scope to update.')
15
+ }, async (input) => jsonResponse(await scanRepository({ root: input.root ?? root, scope: input.scope })));
16
+ tool('context_graph_status', 'Return repository graph counts and database paths.', {
17
+ root: z.string().optional(),
18
+ scope: scopeSchema
19
+ }, async (input) => jsonResponse(graphStatus({ root: input.root ?? root, scope: input.scope })));
20
+ tool('context_graph_nodes', 'List graph nodes discovered in the repository.', {
21
+ root: z.string().optional(),
22
+ scope: scopeSchema,
23
+ kind: z.enum(['file', 'symbol', 'import', 'export', 'call', 'user']).optional(),
24
+ limit: z.number().int().positive().max(500).default(50)
25
+ }, async (input) => jsonResponse(listNodes({
26
+ root: input.root ?? root,
27
+ scope: input.scope,
28
+ kind: input.kind,
29
+ limit: input.limit
30
+ })));
31
+ tool('context_graph_edges', 'List graph edges discovered in the repository.', {
32
+ root: z.string().optional(),
33
+ scope: scopeSchema,
34
+ kind: z.enum(['contains', 'defines', 'imports', 'exports', 'calls', 'attributed_to']).optional(),
35
+ limit: z.number().int().positive().max(500).default(50)
36
+ }, async (input) => jsonResponse(listEdges({
37
+ root: input.root ?? root,
38
+ scope: input.scope,
39
+ kind: input.kind,
40
+ limit: input.limit
41
+ })));
42
+ tool('context_graph_neighbors', 'Return nearby nodes around a graph node id.', {
43
+ root: z.string().optional(),
44
+ scope: scopeSchema,
45
+ nodeId: z.string().min(1),
46
+ depth: z.number().int().positive().max(5).default(1),
47
+ limit: z.number().int().positive().max(500).default(100)
48
+ }, async (input) => jsonResponse(neighbors({
49
+ root: input.root ?? root,
50
+ scope: input.scope,
51
+ nodeId: input.nodeId,
52
+ depth: input.depth,
53
+ limit: input.limit
54
+ })));
55
+ tool('context_graph_query', 'Run a read-only SQLite query against the graph database. Only SELECT, WITH, and PRAGMA are allowed.', {
56
+ root: z.string().optional(),
57
+ scope: scopeSchema,
58
+ sql: z.string().min(1),
59
+ attachHome: z.boolean().default(false).describe('When querying repo scope, ATTACH the home graph as home_graph.'),
60
+ limit: z.number().int().positive().max(1000).default(200)
61
+ }, async (input) => jsonResponse(runReadOnlySql({
62
+ root: input.root ?? root,
63
+ scope: input.scope,
64
+ sql: input.sql,
65
+ attachHome: input.attachHome,
66
+ limit: input.limit
67
+ })));
68
+ const transport = new StdioServerTransport();
69
+ await server.connect(transport);
70
+ }
71
+ function jsonResponse(value) {
72
+ return {
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: JSON.stringify(value, null, 2)
77
+ }
78
+ ]
79
+ };
80
+ }
81
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,13 @@
1
+ import type { ParseResult } from '../types.js';
2
+ type LanguageConfig = {
3
+ name: string;
4
+ parserVersion: string;
5
+ extensions: string[];
6
+ language: any;
7
+ };
8
+ export declare function supportedExtensions(): string[];
9
+ export declare function languageForPath(filePath: string): LanguageConfig | undefined;
10
+ export declare function isSupportedSourceFile(filePath: string): boolean;
11
+ export declare function parseSource(filePath: string, source: string): ParseResult | null;
12
+ export {};
13
+ //# sourceMappingURL=treeSitter.d.ts.map
@@ -0,0 +1,61 @@
1
+ import path from 'node:path';
2
+ import Parser from 'tree-sitter';
3
+ import JavaScriptLanguage from 'tree-sitter-javascript';
4
+ import TypeScriptLanguages from 'tree-sitter-typescript';
5
+ const typeScriptModule = TypeScriptLanguages.default ?? TypeScriptLanguages;
6
+ const LANGUAGE_CONFIGS = [
7
+ {
8
+ name: 'javascript',
9
+ parserVersion: 'tree-sitter-javascript',
10
+ extensions: ['.js', '.mjs', '.cjs', '.jsx'],
11
+ language: JavaScriptLanguage.default ?? JavaScriptLanguage
12
+ },
13
+ {
14
+ name: 'typescript',
15
+ parserVersion: 'tree-sitter-typescript',
16
+ extensions: ['.ts', '.mts', '.cts'],
17
+ language: typeScriptModule.typescript ?? typeScriptModule
18
+ },
19
+ {
20
+ name: 'tsx',
21
+ parserVersion: 'tree-sitter-typescript-tsx',
22
+ extensions: ['.tsx'],
23
+ language: typeScriptModule.tsx ?? typeScriptModule.typescript ?? typeScriptModule
24
+ }
25
+ ];
26
+ const byExtension = new Map();
27
+ for (const config of LANGUAGE_CONFIGS) {
28
+ for (const extension of config.extensions) {
29
+ byExtension.set(extension, config);
30
+ }
31
+ }
32
+ const parserCache = new Map();
33
+ export function supportedExtensions() {
34
+ return [...byExtension.keys()];
35
+ }
36
+ export function languageForPath(filePath) {
37
+ return byExtension.get(path.extname(filePath));
38
+ }
39
+ export function isSupportedSourceFile(filePath) {
40
+ return Boolean(languageForPath(filePath));
41
+ }
42
+ export function parseSource(filePath, source) {
43
+ const config = languageForPath(filePath);
44
+ if (!config)
45
+ return null;
46
+ let parser = parserCache.get(config.name);
47
+ if (!parser) {
48
+ parser = new Parser();
49
+ parser.setLanguage(config.language);
50
+ parserCache.set(config.name, parser);
51
+ }
52
+ let tree;
53
+ try {
54
+ tree = parser.parse(source);
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ return { tree, language: config.name, parser: 'tree-sitter', parserVersion: config.parserVersion };
60
+ }
61
+ //# sourceMappingURL=treeSitter.js.map
@@ -0,0 +1,4 @@
1
+ export declare function discoverSourceFiles(root: string, { ignores }?: {
2
+ ignores?: Set<string>;
3
+ }): string[];
4
+ //# sourceMappingURL=discover.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { isSupportedSourceFile } from '../parser/treeSitter.js';
5
+ const DEFAULT_IGNORES = new Set([
6
+ '.git',
7
+ '.sling',
8
+ '.pi-subagents',
9
+ 'node_modules',
10
+ 'dist',
11
+ 'coverage',
12
+ '.next',
13
+ '.turbo',
14
+ '.cache'
15
+ ]);
16
+ export function discoverSourceFiles(root, { ignores = DEFAULT_IGNORES } = {}) {
17
+ const files = [];
18
+ walk(root, root, files, ignores);
19
+ return files.sort();
20
+ }
21
+ function walk(root, current, files, ignores) {
22
+ const entries = readdirSync(current, { withFileTypes: true });
23
+ const relativePaths = entries.map((entry) => path.relative(root, path.join(current, entry.name)));
24
+ const gitIgnored = ignoredByGit(root, relativePaths);
25
+ for (let index = 0; index < entries.length; index += 1) {
26
+ const entry = entries[index];
27
+ if (!entry)
28
+ continue;
29
+ if (ignores.has(entry.name))
30
+ continue;
31
+ const fullPath = path.join(current, entry.name);
32
+ const relativePath = relativePaths[index];
33
+ if (!relativePath || gitIgnored.has(relativePath))
34
+ continue;
35
+ if (entry.isDirectory()) {
36
+ walk(root, fullPath, files, ignores);
37
+ continue;
38
+ }
39
+ if (!entry.isFile())
40
+ continue;
41
+ if (!isSupportedSourceFile(fullPath))
42
+ continue;
43
+ const stat = statSync(fullPath);
44
+ if (stat.size > 2_000_000)
45
+ continue;
46
+ files.push(relativePath);
47
+ }
48
+ }
49
+ function ignoredByGit(root, relativePaths) {
50
+ if (relativePaths.length === 0)
51
+ return new Set();
52
+ const result = spawnSync('git', ['check-ignore', '--stdin', '-z'], {
53
+ cwd: root,
54
+ input: `${relativePaths.join('\0')}\0`,
55
+ encoding: 'utf8'
56
+ });
57
+ if (result.error || (result.status !== 0 && result.status !== 1)) {
58
+ return new Set();
59
+ }
60
+ return new Set(result.stdout.split('\0').filter(Boolean));
61
+ }
62
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1,3 @@
1
+ /** Fast metadata fingerprint shared by watch and context freshness checks. */
2
+ export declare function collectRepositoryFingerprint(root: string): string;
3
+ //# sourceMappingURL=fingerprint.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { statSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { discoverSourceFiles } from './discover.js';
5
+ /** Fast metadata fingerprint shared by watch and context freshness checks. */
6
+ export function collectRepositoryFingerprint(root) {
7
+ const files = discoverSourceFiles(root);
8
+ const hash = createHash('sha256');
9
+ for (const relativePath of files) {
10
+ const absolutePath = path.join(root, relativePath);
11
+ try {
12
+ const stat = statSync(absolutePath);
13
+ hash.update(relativePath);
14
+ hash.update('\0');
15
+ hash.update(String(stat.size));
16
+ hash.update('\0');
17
+ hash.update(String(Math.round(stat.mtimeMs)));
18
+ hash.update('\0');
19
+ }
20
+ catch {
21
+ hash.update(relativePath);
22
+ hash.update('\0missing\0');
23
+ }
24
+ }
25
+ return hash.digest('hex');
26
+ }
27
+ //# sourceMappingURL=fingerprint.js.map