@mr-jones123/toji 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 +158 -0
  2. package/package.json +47 -0
  3. package/packages/toji-comms/README.md +71 -0
  4. package/packages/toji-comms/src/cli/agents.ts +121 -0
  5. package/packages/toji-comms/src/cli/mmx.ts +65 -0
  6. package/packages/toji-comms/src/cli/subprocess.ts +47 -0
  7. package/packages/toji-comms/src/comms/orchestrator.ts +92 -0
  8. package/packages/toji-comms/src/comms/prompt.ts +84 -0
  9. package/packages/toji-comms/src/comms/store.ts +145 -0
  10. package/packages/toji-comms/src/comms/types.ts +94 -0
  11. package/packages/toji-comms/src/db/connection.ts +58 -0
  12. package/packages/toji-comms/src/db/migrations.ts +69 -0
  13. package/packages/toji-comms/src/index.ts +368 -0
  14. package/packages/toji-comms/src/mcp/client.ts +71 -0
  15. package/packages/toji-comms/src/mcp/server.ts +81 -0
  16. package/packages/toji-mem/README.md +52 -0
  17. package/packages/toji-mem/grammars/manifest.json +9 -0
  18. package/packages/toji-mem/grammars/tree-sitter-cpp.wasm +0 -0
  19. package/packages/toji-mem/grammars/tree-sitter-dart.wasm +0 -0
  20. package/packages/toji-mem/grammars/tree-sitter-java.wasm +0 -0
  21. package/packages/toji-mem/grammars/tree-sitter-javascript.wasm +0 -0
  22. package/packages/toji-mem/grammars/tree-sitter-python.wasm +0 -0
  23. package/packages/toji-mem/grammars/tree-sitter-tsx.wasm +0 -0
  24. package/packages/toji-mem/grammars/tree-sitter-typescript.wasm +0 -0
  25. package/packages/toji-mem/src/db/connection.ts +58 -0
  26. package/packages/toji-mem/src/db/migrations.ts +181 -0
  27. package/packages/toji-mem/src/index.ts +326 -0
  28. package/packages/toji-mem/src/indexer/file-walker.ts +45 -0
  29. package/packages/toji-mem/src/indexer/index-project.ts +277 -0
  30. package/packages/toji-mem/src/indexer/parsers/cpp.ts +81 -0
  31. package/packages/toji-mem/src/indexer/parsers/dart.ts +91 -0
  32. package/packages/toji-mem/src/indexer/parsers/java.ts +83 -0
  33. package/packages/toji-mem/src/indexer/parsers/python.ts +84 -0
  34. package/packages/toji-mem/src/indexer/parsers/registry.ts +28 -0
  35. package/packages/toji-mem/src/indexer/parsers/tree-sitter-loader.ts +39 -0
  36. package/packages/toji-mem/src/indexer/parsers/types.ts +48 -0
  37. package/packages/toji-mem/src/indexer/parsers/typescript.ts +105 -0
  38. package/packages/toji-mem/src/standards/store.ts +52 -0
  39. package/packages/toji-mem/src/tools/blast-radius.ts +98 -0
  40. package/packages/toji-mem/src/tools/graph-explore.ts +186 -0
  41. package/packages/toji-mem/src/tools/project-overview.ts +102 -0
  42. package/packages/toji-mem/src/tools/query-memory.ts +105 -0
@@ -0,0 +1,39 @@
1
+ import { Language, Parser, type Tree } from "web-tree-sitter";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ let initialized = false;
7
+ const languageCache = new Map<string, Language>();
8
+
9
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
10
+
11
+ export async function loadTreeSitterLanguage(grammarFile: string): Promise<Language> {
12
+ if (!initialized) {
13
+ await Parser.init();
14
+ initialized = true;
15
+ }
16
+
17
+ const grammarPath = join(packageRoot, "grammars", grammarFile);
18
+ const cached = languageCache.get(grammarPath);
19
+ if (cached) return cached;
20
+
21
+ if (!existsSync(grammarPath)) {
22
+ throw new Error(
23
+ `Missing tree-sitter grammar ${grammarFile}. Run: bun --cwd packages/toji-mem run download-grammars`,
24
+ );
25
+ }
26
+
27
+ const language = await Language.load(grammarPath);
28
+ languageCache.set(grammarPath, language);
29
+ return language;
30
+ }
31
+
32
+ export async function parseWithGrammar(grammarFile: string, content: string): Promise<Tree> {
33
+ const language = await loadTreeSitterLanguage(grammarFile);
34
+ const parser = new Parser();
35
+ parser.setLanguage(language);
36
+ const tree = parser.parse(content);
37
+ if (!tree) throw new Error(`Failed to parse content with ${grammarFile}`);
38
+ return tree;
39
+ }
@@ -0,0 +1,48 @@
1
+ export type SymbolKind = "function" | "class" | "method" | "interface" | "type" | "variable";
2
+
3
+ export interface ParseInput {
4
+ projectId: number;
5
+ filePath: string;
6
+ content: string;
7
+ }
8
+
9
+ export interface ParsedSymbol {
10
+ name: string;
11
+ kind: SymbolKind;
12
+ startLine: number;
13
+ endLine: number;
14
+ exported: boolean;
15
+ signature?: string;
16
+ }
17
+
18
+ export interface ParsedImport {
19
+ importedName?: string;
20
+ source: string;
21
+ startLine: number;
22
+ }
23
+
24
+ export interface ParsedCall {
25
+ callerName?: string;
26
+ calleeName: string;
27
+ startLine: number;
28
+ }
29
+
30
+ export interface ParsedRelation {
31
+ fromName: string;
32
+ toName: string;
33
+ edgeType: "symbol_extends_symbol" | "symbol_implements_symbol";
34
+ startLine: number;
35
+ }
36
+
37
+ export interface ParsedFile {
38
+ symbols: ParsedSymbol[];
39
+ imports: ParsedImport[];
40
+ calls: ParsedCall[];
41
+ relations: ParsedRelation[];
42
+ }
43
+
44
+ export interface LanguageParser {
45
+ language: string;
46
+ extensions: string[];
47
+ parseFile(input: ParseInput): Promise<ParsedFile>;
48
+ }
@@ -0,0 +1,105 @@
1
+ import { Query } from "web-tree-sitter";
2
+
3
+ import { loadTreeSitterLanguage, parseWithGrammar } from "./tree-sitter-loader";
4
+ import type { LanguageParser, ParsedCall, ParsedFile, ParsedImport, ParsedRelation, ParsedSymbol, SymbolKind } from "./types";
5
+
6
+ const typeScriptQuerySource = `
7
+ (function_declaration name: (identifier) @function.name) @function.definition
8
+ (class_declaration name: (type_identifier) @class.name) @class.definition
9
+ (method_definition name: (property_identifier) @method.name) @method.definition
10
+ (interface_declaration name: (type_identifier) @interface.name) @interface.definition
11
+ (type_alias_declaration name: (type_identifier) @type.name) @type.definition
12
+ (lexical_declaration (variable_declarator name: (identifier) @variable.name value: [(arrow_function) (function_expression)])) @variable.definition
13
+ (import_statement source: (string) @import.source) @import.statement
14
+ (call_expression function: (identifier) @call.name) @call.expression
15
+ (call_expression function: (member_expression property: (property_identifier) @call.member_name)) @call.expression
16
+ (class_declaration name: (type_identifier) @heritage.from (class_heritage (extends_clause value: (identifier) @extends.to)))
17
+ (class_declaration name: (type_identifier) @heritage.from (class_heritage (implements_clause (type_identifier) @implements.to)))
18
+ (interface_declaration name: (type_identifier) @heritage.from (extends_type_clause (type_identifier) @extends.to))
19
+ `;
20
+
21
+ const javaScriptQuerySource = `
22
+ (function_declaration name: (identifier) @function.name) @function.definition
23
+ (class_declaration name: (identifier) @class.name) @class.definition
24
+ (method_definition name: (property_identifier) @method.name) @method.definition
25
+ (lexical_declaration (variable_declarator name: (identifier) @variable.name value: [(arrow_function) (function_expression)])) @variable.definition
26
+ (import_statement source: (string) @import.source) @import.statement
27
+ (call_expression function: (identifier) @call.name) @call.expression
28
+ (call_expression function: (member_expression property: (property_identifier) @call.member_name)) @call.expression
29
+ `;
30
+
31
+ const symbolKinds = new Map<string, SymbolKind>([
32
+ ["function.name", "function"],
33
+ ["class.name", "class"],
34
+ ["method.name", "method"],
35
+ ["interface.name", "interface"],
36
+ ["type.name", "type"],
37
+ ["variable.name", "variable"],
38
+ ]);
39
+
40
+ export function createTypeScriptParser(language: string, grammarFile: string, extensions: string[]): LanguageParser {
41
+ return {
42
+ language,
43
+ extensions,
44
+ async parseFile(input): Promise<ParsedFile> {
45
+ const tree = await parseWithGrammar(grammarFile, input.content);
46
+ const grammar = await loadTreeSitterLanguage(grammarFile);
47
+ const query = new Query(grammar, language === "javascript" || language === "jsx" ? javaScriptQuerySource : typeScriptQuerySource);
48
+ const captures = query.captures(tree.rootNode);
49
+
50
+ const symbols: ParsedSymbol[] = [];
51
+ const imports: ParsedImport[] = [];
52
+ const calls: ParsedCall[] = [];
53
+ const relations: ParsedRelation[] = [];
54
+ let heritageFrom: string | undefined;
55
+
56
+ for (const capture of captures) {
57
+ const kind = symbolKinds.get(capture.name);
58
+ if (kind) {
59
+ const parent = capture.node.parent;
60
+ symbols.push({
61
+ name: capture.node.text,
62
+ kind,
63
+ startLine: capture.node.startPosition.row + 1,
64
+ endLine: parent ? parent.endPosition.row + 1 : capture.node.endPosition.row + 1,
65
+ exported: Boolean(parent?.parent?.type.includes("export")),
66
+ signature: parent?.text.split("\n")[0]?.trim(),
67
+ });
68
+ continue;
69
+ }
70
+
71
+ if (capture.name === "import.source") {
72
+ imports.push({
73
+ source: capture.node.text.replaceAll("'", "").replaceAll('"', ""),
74
+ startLine: capture.node.startPosition.row + 1,
75
+ });
76
+ continue;
77
+ }
78
+
79
+ if (capture.name === "call.name" || capture.name === "call.member_name") {
80
+ calls.push({
81
+ calleeName: capture.node.text,
82
+ startLine: capture.node.startPosition.row + 1,
83
+ });
84
+ continue;
85
+ }
86
+
87
+ if (capture.name === "heritage.from") {
88
+ heritageFrom = capture.node.text;
89
+ continue;
90
+ }
91
+
92
+ if ((capture.name === "extends.to" || capture.name === "implements.to") && heritageFrom) {
93
+ relations.push({
94
+ fromName: heritageFrom,
95
+ toName: capture.node.text,
96
+ edgeType: capture.name === "extends.to" ? "symbol_extends_symbol" : "symbol_implements_symbol",
97
+ startLine: capture.node.startPosition.row + 1,
98
+ });
99
+ }
100
+ }
101
+
102
+ return { symbols, imports, calls, relations };
103
+ },
104
+ };
105
+ }
@@ -0,0 +1,52 @@
1
+ import type { TojiDatabase } from "../db/connection";
2
+
3
+ export interface AddStandardInput {
4
+ scope: "global" | "project";
5
+ projectPath?: string;
6
+ language?: string;
7
+ framework?: string;
8
+ rule: string;
9
+ rationale?: string;
10
+ priority?: "low" | "medium" | "high";
11
+ }
12
+
13
+ export function addStandard(database: TojiDatabase, input: AddStandardInput): number {
14
+ const result = database
15
+ .query<{ id: number }, [string, string | null, string | null, string | null, string, string | null, string]>(
16
+ `INSERT INTO standards (scope, project_path, language, framework, rule, rationale, priority)
17
+ VALUES (?, ?, ?, ?, ?, ?, ?)
18
+ RETURNING id`,
19
+ )
20
+ .get(
21
+ input.scope,
22
+ input.projectPath ?? null,
23
+ input.language ?? null,
24
+ input.framework ?? null,
25
+ input.rule,
26
+ input.rationale ?? null,
27
+ input.priority ?? "medium",
28
+ );
29
+
30
+ if (!result) throw new Error("Failed to add standard");
31
+
32
+ database
33
+ .query<unknown, [number, string, string, string, string]>(
34
+ `INSERT INTO standards_fts (standard_id, rule, rationale, language, framework) VALUES (?, ?, ?, ?, ?)`,
35
+ )
36
+ .run(result.id, input.rule, input.rationale ?? "", input.language ?? "", input.framework ?? "");
37
+
38
+ return result.id;
39
+ }
40
+
41
+ export function getStandards(database: TojiDatabase, language?: string, framework?: string): unknown[] {
42
+ return database
43
+ .query(
44
+ `SELECT id, scope, project_path, language, framework, rule, rationale, priority
45
+ FROM standards
46
+ WHERE (? IS NULL OR language IS NULL OR language = ?)
47
+ AND (? IS NULL OR framework IS NULL OR framework = ?)
48
+ ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, created_at DESC
49
+ LIMIT 20`,
50
+ )
51
+ .all(language ?? null, language ?? null, framework ?? null, framework ?? null);
52
+ }
@@ -0,0 +1,98 @@
1
+ import type { TojiDatabase } from "../db/connection";
2
+
3
+ export type BlastRadiusDirection = "incoming" | "outgoing" | "both";
4
+
5
+ export interface BlastRadiusOptions {
6
+ target: string;
7
+ filePath?: string;
8
+ depth?: number;
9
+ direction?: BlastRadiusDirection;
10
+ edgeTypes?: string[];
11
+ }
12
+
13
+ export function getBlastRadius(database: TojiDatabase, options: BlastRadiusOptions): unknown[] {
14
+ const depth = options.depth ?? 2;
15
+ const direction = options.direction ?? "both";
16
+ const edgeTypeFilter = buildEdgeTypeFilter(options.edgeTypes);
17
+ const outgoingJoin = direction === "incoming" ? "" : outgoingTraversal(edgeTypeFilter.sql);
18
+ const incomingJoin = direction === "outgoing" ? "" : incomingTraversal(edgeTypeFilter.sql);
19
+ const startFileFilter = options.filePath ? "AND f.path = ?" : "";
20
+
21
+ const sql = `WITH RECURSIVE start_nodes(node_type, node_id, node_name) AS (
22
+ SELECT 'symbol', s.id, s.name
23
+ FROM symbols s
24
+ JOIN files f ON f.id = s.file_id
25
+ WHERE (s.name = ? OR s.canon_name = ?)
26
+ ${startFileFilter}
27
+ UNION
28
+ SELECT 'file', f.id, f.path
29
+ FROM files f
30
+ WHERE f.path = ? OR f.name = ?
31
+ ),
32
+ traversed(node_type, node_id, node_name, distance, via_edge, direction) AS (
33
+ SELECT node_type, node_id, node_name, 0, 'target', 'target' FROM start_nodes
34
+ ${outgoingJoin}
35
+ ${incomingJoin}
36
+ )
37
+ SELECT DISTINCT
38
+ t.node_type,
39
+ t.node_id,
40
+ COALESCE(s.name, f.name, t.node_name) AS name,
41
+ COALESCE(s.kind, 'file') AS kind,
42
+ t.distance,
43
+ t.via_edge,
44
+ t.direction,
45
+ COALESCE(sf.path, f.path, t.node_name) AS path,
46
+ COALESCE(s.language, f.language, '') AS language,
47
+ CASE WHEN t.via_edge = 'file_imports_file' THEN 'conservative_file' ELSE 'precise_symbol' END AS scope
48
+ FROM traversed t
49
+ LEFT JOIN symbols s ON t.node_type = 'symbol' AND s.id = t.node_id
50
+ LEFT JOIN files sf ON sf.id = s.file_id
51
+ LEFT JOIN files f ON t.node_type = 'file' AND f.id = t.node_id
52
+ ORDER BY t.distance, path, name
53
+ LIMIT 100`;
54
+
55
+ return database.query(sql).all(...buildParams(options, depth, direction, edgeTypeFilter.params));
56
+ }
57
+
58
+ function outgoingTraversal(edgeTypeSql: string): string {
59
+ return `UNION
60
+ SELECT e.to_type, COALESCE(e.to_id, -e.id), COALESCE(e.to_name, ''), traversed.distance + 1, e.edge_type, 'outgoing'
61
+ FROM traversed
62
+ JOIN edges e ON e.from_type = traversed.node_type AND e.from_id = traversed.node_id
63
+ WHERE traversed.distance < ?
64
+ ${edgeTypeSql}`;
65
+ }
66
+
67
+ function incomingTraversal(edgeTypeSql: string): string {
68
+ return `UNION
69
+ SELECT e.from_type, e.from_id, '', traversed.distance + 1, e.edge_type, 'incoming'
70
+ FROM traversed
71
+ JOIN edges e ON e.to_type = traversed.node_type AND e.to_id = traversed.node_id
72
+ WHERE traversed.distance < ?
73
+ ${edgeTypeSql}`;
74
+ }
75
+
76
+ function buildEdgeTypeFilter(edgeTypes?: string[]): { sql: string; params: string[] } {
77
+ if (!edgeTypes || edgeTypes.length === 0) return { sql: "", params: [] };
78
+ return {
79
+ sql: `AND e.edge_type IN (${edgeTypes.map(() => "?").join(", ")})`,
80
+ params: edgeTypes,
81
+ };
82
+ }
83
+
84
+ function buildParams(
85
+ options: BlastRadiusOptions,
86
+ depth: number,
87
+ direction: BlastRadiusDirection,
88
+ edgeTypeParams: string[],
89
+ ): Array<string | number> {
90
+ const params: Array<string | number> = [options.target, options.target];
91
+ if (options.filePath) params.push(options.filePath);
92
+ params.push(options.target, options.target);
93
+
94
+ if (direction !== "incoming") params.push(depth, ...edgeTypeParams);
95
+ if (direction !== "outgoing") params.push(depth, ...edgeTypeParams);
96
+
97
+ return params;
98
+ }
@@ -0,0 +1,186 @@
1
+ import { resolve } from "node:path";
2
+
3
+ import type { TojiDatabase } from "../db/connection";
4
+ import { getBlastRadius, type BlastRadiusDirection } from "./blast-radius";
5
+
6
+ export interface GraphExploreOptions {
7
+ intent: string;
8
+ seedQueries?: string[];
9
+ maxSeeds?: number;
10
+ depth?: number;
11
+ direction?: BlastRadiusDirection;
12
+ projectPath?: string;
13
+ }
14
+
15
+ interface SeedSymbol {
16
+ name: string;
17
+ kind: string;
18
+ language: string;
19
+ path: string;
20
+ signature?: string;
21
+ score: number;
22
+ }
23
+
24
+ export function graphExplore(database: TojiDatabase, options: GraphExploreOptions): Record<string, unknown> {
25
+ const maxSeeds = options.maxSeeds ?? 8;
26
+ const queries = buildQueries(options.intent, options.seedQueries);
27
+ const projectId = options.projectPath ? findProjectId(database, options.projectPath) : undefined;
28
+ const seeds = findSeeds(database, queries, maxSeeds, projectId);
29
+
30
+ if (seeds.length === 0) {
31
+ return {
32
+ intent: options.intent,
33
+ queries,
34
+ projectId,
35
+ seeds: [],
36
+ blastRadius: [],
37
+ hint: "No seed symbols found. Try concrete seedQueries such as adapter, registry, runner, or run toji_project_overview first.",
38
+ };
39
+ }
40
+
41
+ const seen = new Set<string>();
42
+ const blastRadius = [];
43
+ for (const seed of seeds) {
44
+ const rows = getBlastRadius(database, {
45
+ target: seed.name,
46
+ filePath: seed.path,
47
+ depth: options.depth ?? 2,
48
+ direction: options.direction ?? "both",
49
+ }) as Array<Record<string, unknown>>;
50
+
51
+ for (const row of rows) {
52
+ const key = `${String(row.path)}:${String(row.kind)}:${String(row.name)}:${String(row.distance)}:${String(row.direction)}`;
53
+ if (seen.has(key)) continue;
54
+ seen.add(key);
55
+ blastRadius.push({ ...row, seed: `${seed.name} (${seed.path})` });
56
+ }
57
+ }
58
+
59
+ return {
60
+ intent: options.intent,
61
+ queries,
62
+ projectId,
63
+ seeds,
64
+ blastRadius,
65
+ likelyFiles: summarizeFiles(blastRadius),
66
+ };
67
+ }
68
+
69
+ function buildQueries(intent: string, seedQueries?: string[]): string[] {
70
+ const explicit = (seedQueries ?? []).map((item) => item.trim()).filter(Boolean);
71
+ const tokens = intent
72
+ .split(/[^A-Za-z0-9_]+/)
73
+ .map((item) => item.trim())
74
+ .filter((item) => item.length >= 3 && !stopWords.has(item.toLowerCase()));
75
+ return [...new Set([...explicit, ...tokens])].slice(0, 16);
76
+ }
77
+
78
+ function findSeeds(database: TojiDatabase, queries: string[], maxSeeds: number, projectId?: number): SeedSymbol[] {
79
+ const scores = new Map<string, SeedSymbol>();
80
+
81
+ for (const [queryIndex, query] of queries.entries()) {
82
+ const rows = findSeedRows(database, query, projectId);
83
+
84
+ for (const row of rows) {
85
+ const key = `${row.path}:${row.kind}:${row.name}`;
86
+ const previous = scores.get(key);
87
+ const score = (previous?.score ?? 0) + 100 - queryIndex;
88
+ scores.set(key, { ...row, score });
89
+ }
90
+ }
91
+
92
+ return [...scores.values()]
93
+ .sort((left, right) => right.score - left.score || rankKind(left.kind) - rankKind(right.kind) || left.path.localeCompare(right.path))
94
+ .slice(0, maxSeeds);
95
+ }
96
+
97
+ function findSeedRows(database: TojiDatabase, query: string, projectId?: number): SeedSymbol[] {
98
+ if (projectId !== undefined) return findSeedRowsByLike(database, query, projectId);
99
+
100
+ const ftsRows = database
101
+ .query<SeedSymbol, [string]>(
102
+ `SELECT name, kind, language, path, signature
103
+ FROM symbol_fts
104
+ WHERE symbol_fts MATCH ?
105
+ LIMIT 20`,
106
+ )
107
+ .all(query);
108
+ if (ftsRows.length > 0) return ftsRows;
109
+
110
+ return findSeedRowsByLike(database, query);
111
+ }
112
+
113
+ function findSeedRowsByLike(database: TojiDatabase, query: string, projectId?: number): SeedSymbol[] {
114
+ const like = `%${query}%`;
115
+ const projectFilter = projectId === undefined ? "" : "AND s.project_id = ?";
116
+ const params = projectId === undefined ? [like, like, query, like, like] : [like, like, projectId, query, like, like];
117
+ return database
118
+ .query<SeedSymbol, Array<string | number>>(
119
+ `SELECT s.name, s.kind, s.language, f.path, s.signature
120
+ FROM symbols s
121
+ JOIN files f ON f.id = s.file_id
122
+ WHERE (s.name LIKE ? OR f.path LIKE ?)
123
+ ${projectFilter}
124
+ ORDER BY CASE
125
+ WHEN s.name = ? THEN 0
126
+ WHEN s.name LIKE ? THEN 1
127
+ WHEN f.path LIKE ? THEN 2
128
+ ELSE 3
129
+ END,
130
+ f.path,
131
+ s.name
132
+ LIMIT 20`,
133
+ )
134
+ .all(...params);
135
+ }
136
+
137
+ function findProjectId(database: TojiDatabase, projectPath: string): number | undefined {
138
+ return database
139
+ .query<{ id: number }, [string]>(`SELECT id FROM projects WHERE root_path = ?`)
140
+ .get(resolve(projectPath))?.id;
141
+ }
142
+
143
+ function summarizeFiles(rows: Array<Record<string, unknown>>): Array<{ path: string; hits: number; minDistance: number }> {
144
+ const files = new Map<string, { path: string; hits: number; minDistance: number }>();
145
+ for (const row of rows) {
146
+ const path = String(row.path ?? "");
147
+ if (!path) continue;
148
+ const distance = typeof row.distance === "number" ? row.distance : Number(row.distance ?? 99);
149
+ const current = files.get(path) ?? { path, hits: 0, minDistance: distance };
150
+ current.hits += 1;
151
+ current.minDistance = Math.min(current.minDistance, distance);
152
+ files.set(path, current);
153
+ }
154
+ return [...files.values()].sort((left, right) => left.minDistance - right.minDistance || right.hits - left.hits || left.path.localeCompare(right.path));
155
+ }
156
+
157
+ function rankKind(kind: string): number {
158
+ if (kind === "class") return 0;
159
+ if (kind === "function") return 1;
160
+ if (kind === "method") return 2;
161
+ return 3;
162
+ }
163
+
164
+ const stopWords = new Set([
165
+ "the",
166
+ "and",
167
+ "for",
168
+ "with",
169
+ "what",
170
+ "where",
171
+ "when",
172
+ "would",
173
+ "could",
174
+ "should",
175
+ "like",
176
+ "add",
177
+ "new",
178
+ "need",
179
+ "needed",
180
+ "files",
181
+ "change",
182
+ "changes",
183
+ "affected",
184
+ "impact",
185
+ "impacted",
186
+ ]);
@@ -0,0 +1,102 @@
1
+ import { resolve } from "node:path";
2
+
3
+ import type { TojiDatabase } from "../db/connection";
4
+
5
+ export interface ProjectOverviewOptions {
6
+ projectPath?: string;
7
+ projectId?: number;
8
+ includeSymbols?: boolean;
9
+ }
10
+
11
+ export function getProjectOverview(database: TojiDatabase, options: ProjectOverviewOptions = {}): Record<string, unknown> {
12
+ const project = findProject(database, options);
13
+ if (!project) {
14
+ return {
15
+ error: "No indexed project found. Run toji_index_project or /toji-index first.",
16
+ projectPath: options.projectPath ? resolve(options.projectPath) : undefined,
17
+ };
18
+ }
19
+
20
+ const counts = {
21
+ files: scalarCount(database, "files", project.id),
22
+ symbols: scalarCount(database, "symbols", project.id),
23
+ imports: scalarCount(database, "imports", project.id),
24
+ calls: scalarCount(database, "calls", project.id),
25
+ edges: scalarCount(database, "edges", project.id),
26
+ };
27
+
28
+ const overview: Record<string, unknown> = {
29
+ project,
30
+ counts,
31
+ languages: database
32
+ .query(`SELECT language, count(*) AS files FROM files WHERE project_id = ? GROUP BY language ORDER BY files DESC`)
33
+ .all(project.id),
34
+ directories: database
35
+ .query(
36
+ `SELECT CASE WHEN instr(path, '/') = 0 THEN '(root)' ELSE substr(path, 1, instr(path, '/') - 1) END AS directory,
37
+ count(*) AS files
38
+ FROM files
39
+ WHERE project_id = ?
40
+ GROUP BY directory
41
+ ORDER BY files DESC, directory
42
+ LIMIT 50`,
43
+ )
44
+ .all(project.id),
45
+ edgeTypes: database
46
+ .query(`SELECT edge_type, count(*) AS count FROM edges WHERE project_id = ? GROUP BY edge_type ORDER BY count DESC`)
47
+ .all(project.id),
48
+ files: database.query(`SELECT path, language FROM files WHERE project_id = ? ORDER BY path LIMIT 200`).all(project.id),
49
+ tests: database
50
+ .query(`SELECT path, language FROM files WHERE project_id = ? AND (path LIKE 'test/%' OR path LIKE 'tests/%' OR path LIKE '%test%') ORDER BY path LIMIT 100`)
51
+ .all(project.id),
52
+ topSymbols: database
53
+ .query(
54
+ `SELECT s.name, s.kind, s.language, f.path, s.signature
55
+ FROM symbols s
56
+ JOIN files f ON f.id = s.file_id
57
+ WHERE s.project_id = ? AND s.kind IN ('class', 'function', 'method', 'interface', 'type')
58
+ ORDER BY CASE s.kind WHEN 'class' THEN 0 WHEN 'interface' THEN 1 WHEN 'function' THEN 2 WHEN 'method' THEN 3 ELSE 4 END,
59
+ f.path,
60
+ s.start_line
61
+ LIMIT 120`,
62
+ )
63
+ .all(project.id),
64
+ };
65
+
66
+ if (options.includeSymbols) {
67
+ overview.symbolsByFile = database
68
+ .query(
69
+ `SELECT f.path,
70
+ group_concat(s.kind || ':' || s.name, ', ') AS symbols
71
+ FROM files f
72
+ LEFT JOIN symbols s ON s.file_id = f.id
73
+ WHERE f.project_id = ?
74
+ GROUP BY f.id
75
+ ORDER BY f.path
76
+ LIMIT 200`,
77
+ )
78
+ .all(project.id);
79
+ }
80
+
81
+ return overview;
82
+ }
83
+
84
+ function findProject(database: TojiDatabase, options: ProjectOverviewOptions): { id: number; root_path: string; name: string; indexed_at: string } | undefined {
85
+ if (options.projectId !== undefined) {
86
+ return database.query<{ id: number; root_path: string; name: string; indexed_at: string }, [number]>(`SELECT id, root_path, name, indexed_at FROM projects WHERE id = ?`).get(options.projectId);
87
+ }
88
+
89
+ if (options.projectPath) {
90
+ return database
91
+ .query<{ id: number; root_path: string; name: string; indexed_at: string }, [string]>(`SELECT id, root_path, name, indexed_at FROM projects WHERE root_path = ?`)
92
+ .get(resolve(options.projectPath));
93
+ }
94
+
95
+ return database
96
+ .query<{ id: number; root_path: string; name: string; indexed_at: string }, []>(`SELECT id, root_path, name, indexed_at FROM projects ORDER BY indexed_at DESC, id DESC LIMIT 1`)
97
+ .get();
98
+ }
99
+
100
+ function scalarCount(database: TojiDatabase, table: string, projectId: number): number {
101
+ return database.query<{ count: number }, [number]>(`SELECT count(*) AS count FROM ${table} WHERE project_id = ?`).get(projectId)?.count ?? 0;
102
+ }