@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.
- package/README.md +158 -0
- package/package.json +47 -0
- package/packages/toji-comms/README.md +71 -0
- package/packages/toji-comms/src/cli/agents.ts +121 -0
- package/packages/toji-comms/src/cli/mmx.ts +65 -0
- package/packages/toji-comms/src/cli/subprocess.ts +47 -0
- package/packages/toji-comms/src/comms/orchestrator.ts +92 -0
- package/packages/toji-comms/src/comms/prompt.ts +84 -0
- package/packages/toji-comms/src/comms/store.ts +145 -0
- package/packages/toji-comms/src/comms/types.ts +94 -0
- package/packages/toji-comms/src/db/connection.ts +58 -0
- package/packages/toji-comms/src/db/migrations.ts +69 -0
- package/packages/toji-comms/src/index.ts +368 -0
- package/packages/toji-comms/src/mcp/client.ts +71 -0
- package/packages/toji-comms/src/mcp/server.ts +81 -0
- package/packages/toji-mem/README.md +52 -0
- package/packages/toji-mem/grammars/manifest.json +9 -0
- package/packages/toji-mem/grammars/tree-sitter-cpp.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-dart.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-java.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-javascript.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-python.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-tsx.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-typescript.wasm +0 -0
- package/packages/toji-mem/src/db/connection.ts +58 -0
- package/packages/toji-mem/src/db/migrations.ts +181 -0
- package/packages/toji-mem/src/index.ts +326 -0
- package/packages/toji-mem/src/indexer/file-walker.ts +45 -0
- package/packages/toji-mem/src/indexer/index-project.ts +277 -0
- package/packages/toji-mem/src/indexer/parsers/cpp.ts +81 -0
- package/packages/toji-mem/src/indexer/parsers/dart.ts +91 -0
- package/packages/toji-mem/src/indexer/parsers/java.ts +83 -0
- package/packages/toji-mem/src/indexer/parsers/python.ts +84 -0
- package/packages/toji-mem/src/indexer/parsers/registry.ts +28 -0
- package/packages/toji-mem/src/indexer/parsers/tree-sitter-loader.ts +39 -0
- package/packages/toji-mem/src/indexer/parsers/types.ts +48 -0
- package/packages/toji-mem/src/indexer/parsers/typescript.ts +105 -0
- package/packages/toji-mem/src/standards/store.ts +52 -0
- package/packages/toji-mem/src/tools/blast-radius.ts +98 -0
- package/packages/toji-mem/src/tools/graph-explore.ts +186 -0
- package/packages/toji-mem/src/tools/project-overview.ts +102 -0
- package/packages/toji-mem/src/tools/query-memory.ts +105 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { TojiDatabase } from "./connection";
|
|
2
|
+
|
|
3
|
+
const tables = `
|
|
4
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
5
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
|
+
root_path TEXT NOT NULL UNIQUE,
|
|
7
|
+
name TEXT NOT NULL,
|
|
8
|
+
indexed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
14
|
+
path TEXT NOT NULL,
|
|
15
|
+
name TEXT NOT NULL DEFAULT '',
|
|
16
|
+
language TEXT NOT NULL,
|
|
17
|
+
hash TEXT NOT NULL,
|
|
18
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
19
|
+
UNIQUE(project_id, path)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
25
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
26
|
+
name TEXT NOT NULL,
|
|
27
|
+
canon_name TEXT NOT NULL DEFAULT '',
|
|
28
|
+
kind TEXT NOT NULL,
|
|
29
|
+
language TEXT NOT NULL,
|
|
30
|
+
start_line INTEGER NOT NULL,
|
|
31
|
+
end_line INTEGER NOT NULL,
|
|
32
|
+
signature TEXT,
|
|
33
|
+
docstring TEXT NOT NULL DEFAULT '',
|
|
34
|
+
exported INTEGER NOT NULL DEFAULT 0
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS imports (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
40
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
41
|
+
imported_name TEXT,
|
|
42
|
+
source TEXT NOT NULL,
|
|
43
|
+
start_line INTEGER NOT NULL
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS calls (
|
|
47
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
48
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
49
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
50
|
+
caller_name TEXT,
|
|
51
|
+
callee_name TEXT NOT NULL,
|
|
52
|
+
start_line INTEGER NOT NULL
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
58
|
+
from_type TEXT NOT NULL,
|
|
59
|
+
from_id INTEGER NOT NULL,
|
|
60
|
+
to_type TEXT NOT NULL,
|
|
61
|
+
to_id INTEGER,
|
|
62
|
+
to_name TEXT,
|
|
63
|
+
edge_type TEXT NOT NULL,
|
|
64
|
+
confidence REAL NOT NULL DEFAULT 0.5
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE IF NOT EXISTS standards (
|
|
68
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
|
+
scope TEXT NOT NULL,
|
|
70
|
+
project_path TEXT,
|
|
71
|
+
language TEXT,
|
|
72
|
+
framework TEXT,
|
|
73
|
+
rule TEXT NOT NULL,
|
|
74
|
+
rationale TEXT,
|
|
75
|
+
priority TEXT NOT NULL DEFAULT 'medium',
|
|
76
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
77
|
+
);
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const searchTables = `
|
|
81
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS symbol_fts USING fts5(
|
|
82
|
+
symbol_id UNINDEXED,
|
|
83
|
+
project_id UNINDEXED,
|
|
84
|
+
file_id UNINDEXED,
|
|
85
|
+
name,
|
|
86
|
+
canon_name,
|
|
87
|
+
kind,
|
|
88
|
+
language,
|
|
89
|
+
path,
|
|
90
|
+
signature,
|
|
91
|
+
docstring,
|
|
92
|
+
tokenize='porter'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts5(
|
|
96
|
+
file_id UNINDEXED,
|
|
97
|
+
project_id UNINDEXED,
|
|
98
|
+
name,
|
|
99
|
+
path,
|
|
100
|
+
language,
|
|
101
|
+
docstring,
|
|
102
|
+
tokenize='porter'
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS standards_fts USING fts5(
|
|
106
|
+
standard_id UNINDEXED,
|
|
107
|
+
rule,
|
|
108
|
+
rationale,
|
|
109
|
+
language,
|
|
110
|
+
framework,
|
|
111
|
+
tokenize='porter'
|
|
112
|
+
);
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const indexes = `
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_files_project_path ON files(project_id, path);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_files_project_name ON files(project_id, name);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_files_lower_path ON files(lower(path));
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_files_lower_name ON files(lower(name));
|
|
120
|
+
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_project_name ON symbols(project_id, name);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_project_canon_name ON symbols(project_id, canon_name);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_lower_name ON symbols(lower(name));
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_lower_canon_name ON symbols(lower(canon_name));
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_project_file ON symbols(project_id, file_id);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_edges_project_from ON edges(project_id, from_type, from_id, edge_type);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_edges_project_to ON edges(project_id, to_type, to_id, edge_type);
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_edges_project_to_name ON edges(project_id, to_name);
|
|
130
|
+
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_standards_lang_framework ON standards(language, framework);
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
export function runMigrations(database: TojiDatabase): void {
|
|
135
|
+
database.exec(tables);
|
|
136
|
+
migrateExisting(database);
|
|
137
|
+
resetOldSearchTables(database);
|
|
138
|
+
database.exec(searchTables);
|
|
139
|
+
database.exec(indexes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function migrateExisting(database: TojiDatabase): void {
|
|
143
|
+
for (const sql of [
|
|
144
|
+
"ALTER TABLE files ADD COLUMN name TEXT NOT NULL DEFAULT ''",
|
|
145
|
+
"ALTER TABLE symbols ADD COLUMN canon_name TEXT NOT NULL DEFAULT ''",
|
|
146
|
+
"ALTER TABLE symbols ADD COLUMN docstring TEXT NOT NULL DEFAULT ''",
|
|
147
|
+
]) {
|
|
148
|
+
try {
|
|
149
|
+
database.exec(sql);
|
|
150
|
+
} catch {
|
|
151
|
+
// Existing column. SQLite has no IF NOT EXISTS for ADD COLUMN.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resetOldSearchTables(database: TojiDatabase): void {
|
|
157
|
+
for (const [table, column] of [
|
|
158
|
+
["symbol_fts", "canon_name"],
|
|
159
|
+
["file_fts", "file_id"],
|
|
160
|
+
["standards_fts", "standard_id"],
|
|
161
|
+
] as const) {
|
|
162
|
+
if (hasTable(database, table) && !hasColumn(database, table, column)) {
|
|
163
|
+
database.exec(`DROP TABLE ${table}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function hasTable(database: TojiDatabase, table: string): boolean {
|
|
169
|
+
return Boolean(
|
|
170
|
+
database
|
|
171
|
+
.query<{ name: string }, [string]>(`SELECT name FROM sqlite_master WHERE name = ?`)
|
|
172
|
+
.get(table),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function hasColumn(database: TojiDatabase, table: string, column: string): boolean {
|
|
177
|
+
return database
|
|
178
|
+
.query<{ name: string }, []>(`PRAGMA table_info(${table})`)
|
|
179
|
+
.all()
|
|
180
|
+
.some((row) => row.name === column);
|
|
181
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
// @ts-expect-error pi provides this package at extension runtime.
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
|
|
6
|
+
import { openTojiDatabase } from "./db/connection";
|
|
7
|
+
import { indexProject } from "./indexer/index-project";
|
|
8
|
+
import { addStandard, getStandards } from "./standards/store";
|
|
9
|
+
import { getBlastRadius } from "./tools/blast-radius";
|
|
10
|
+
import { graphExplore } from "./tools/graph-explore";
|
|
11
|
+
import { getProjectOverview } from "./tools/project-overview";
|
|
12
|
+
import { queryMemory } from "./tools/query-memory";
|
|
13
|
+
|
|
14
|
+
export default function (pi: ExtensionAPI) {
|
|
15
|
+
pi.registerCommand("toji-index", {
|
|
16
|
+
description: "Index current or specified project into Toji memory",
|
|
17
|
+
handler: async (args, ctx) => {
|
|
18
|
+
const database = openTojiDatabase();
|
|
19
|
+
const rootPath = args.trim() || ctx.cwd;
|
|
20
|
+
const result = await indexProject(database, rootPath);
|
|
21
|
+
database.close();
|
|
22
|
+
ctx.ui.notify(`Toji indexed ${result.indexedFiles} files and ${result.symbols} symbols.`, "info");
|
|
23
|
+
pi.sendMessage({ customType: "toji-index", content: formatJson(result), display: true });
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
pi.registerCommand("toji-overview", {
|
|
28
|
+
description: "Show indexed project overview. Usage: /toji-overview [path]",
|
|
29
|
+
handler: async (args, ctx) => {
|
|
30
|
+
const database = openTojiDatabase();
|
|
31
|
+
const overview = getProjectOverview(database, { projectPath: args.trim() || ctx.cwd, includeSymbols: true });
|
|
32
|
+
database.close();
|
|
33
|
+
ctx.ui.notify("Showing compact Toji overview. Use the tool output and Ctrl+O for full JSON.", "info");
|
|
34
|
+
pi.sendMessage({ customType: "toji-overview", content: formatJson(compactValue(overview)), display: true });
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
pi.registerCommand("toji-query", {
|
|
39
|
+
description: "Search Toji memory. Usage: /toji-query <query>",
|
|
40
|
+
handler: async (args, ctx) => {
|
|
41
|
+
const query = args.trim();
|
|
42
|
+
if (!query) {
|
|
43
|
+
ctx.ui.notify("Usage: /toji-query <query>", "error");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const database = openTojiDatabase();
|
|
48
|
+
const result = queryMemory(database, query, "all");
|
|
49
|
+
database.close();
|
|
50
|
+
pi.sendMessage({ customType: "toji-query", content: formatJson(result), display: true });
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
pi.registerCommand("toji-blast", {
|
|
55
|
+
description: "Analyze symbol blast radius. Usage: /toji-blast <symbol> [--file path] [--depth n] [--direction incoming|outgoing|both]",
|
|
56
|
+
handler: async (args, ctx) => {
|
|
57
|
+
const options = parseBlastArgs(args);
|
|
58
|
+
if (!options.target) {
|
|
59
|
+
ctx.ui.notify("Usage: /toji-blast <symbol> [--file path] [--depth n] [--direction incoming|outgoing|both]", "error");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const database = openTojiDatabase();
|
|
64
|
+
const result = getBlastRadius(database, options);
|
|
65
|
+
database.close();
|
|
66
|
+
pi.sendMessage({ customType: "toji-blast", content: formatJson(result), display: true });
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
pi.registerCommand("toji-graph", {
|
|
71
|
+
description: "Explore graph from an intent. Usage: /toji-graph <intent>",
|
|
72
|
+
handler: async (args, ctx) => {
|
|
73
|
+
const intent = args.trim();
|
|
74
|
+
if (!intent) {
|
|
75
|
+
ctx.ui.notify("Usage: /toji-graph <intent>", "error");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const database = openTojiDatabase();
|
|
80
|
+
const result = graphExplore(database, { intent, direction: "both", depth: 2, projectPath: ctx.cwd });
|
|
81
|
+
database.close();
|
|
82
|
+
pi.sendMessage({ customType: "toji-graph", content: formatJson(result), display: true });
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
pi.registerCommand("toji-standards", {
|
|
87
|
+
description: "Show Toji standards. Usage: /toji-standards [language]",
|
|
88
|
+
handler: async (args) => {
|
|
89
|
+
const language = args.trim() || undefined;
|
|
90
|
+
const database = openTojiDatabase();
|
|
91
|
+
const standards = getStandards(database, language);
|
|
92
|
+
database.close();
|
|
93
|
+
pi.sendMessage({ customType: "toji-standards", content: formatJson(standards), display: true });
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
pi.registerTool({
|
|
98
|
+
name: "toji_index_project",
|
|
99
|
+
label: "Index Project",
|
|
100
|
+
description: "Index the current project into Toji graph memory using SQLite, FTS5, and tree-sitter.",
|
|
101
|
+
parameters: Type.Object({
|
|
102
|
+
rootPath: Type.Optional(Type.String({ description: "Project root path. Defaults to current pi cwd." })),
|
|
103
|
+
}),
|
|
104
|
+
async execute(_toolCallId, params, _signal, onUpdate, ctx) {
|
|
105
|
+
const database = openTojiDatabase();
|
|
106
|
+
const rootPath = params.rootPath ?? ctx.cwd;
|
|
107
|
+
onUpdate?.({ content: [{ type: "text", text: `Indexing ${rootPath}...` }], details: {} });
|
|
108
|
+
const result = await indexProject(database, rootPath);
|
|
109
|
+
database.close();
|
|
110
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
pi.registerTool({
|
|
115
|
+
name: "toji_project_overview",
|
|
116
|
+
label: "Project Overview",
|
|
117
|
+
description: "Return an overview of an indexed project: files by directory, languages, symbols, tests, and graph edge counts. Use this when asked to read or summarize an indexed codebase.",
|
|
118
|
+
parameters: Type.Object({
|
|
119
|
+
projectPath: Type.Optional(Type.String({ description: "Project root path. Defaults to current pi cwd." })),
|
|
120
|
+
projectId: Type.Optional(Type.Number({ description: "Indexed project id." })),
|
|
121
|
+
includeSymbols: Type.Optional(Type.Boolean({ description: "Include symbols grouped by file. Defaults to true." })),
|
|
122
|
+
}),
|
|
123
|
+
async execute(_toolCallId, params, _signal, onUpdate, ctx) {
|
|
124
|
+
const database = openTojiDatabase();
|
|
125
|
+
onUpdate?.({ content: [{ type: "text", text: "Loading indexed project overview..." }], details: {} });
|
|
126
|
+
const overview = getProjectOverview(database, {
|
|
127
|
+
projectPath: params.projectPath ?? ctx.cwd,
|
|
128
|
+
projectId: params.projectId,
|
|
129
|
+
includeSymbols: params.includeSymbols ?? true,
|
|
130
|
+
});
|
|
131
|
+
database.close();
|
|
132
|
+
return { content: [{ type: "text", text: JSON.stringify(compactValue(overview), null, 2) }], details: overview };
|
|
133
|
+
},
|
|
134
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
135
|
+
if (isPartial) return new Text(theme.fg("warning", "Loading Toji overview..."), 0, 0);
|
|
136
|
+
const full = formatPlainJson(result.details);
|
|
137
|
+
const compact = formatPlainJson(compactValue(result.details));
|
|
138
|
+
const text = expanded ? full : `${compact}\n${theme.fg("dim", "Ctrl+O to expand full JSON")}`;
|
|
139
|
+
return new Text(text, 0, 0);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.registerTool({
|
|
144
|
+
name: "toji_query_memory",
|
|
145
|
+
label: "Query Memory",
|
|
146
|
+
description: "Search Toji symbol, file, and standards memory through SQLite FTS5.",
|
|
147
|
+
parameters: Type.Object({
|
|
148
|
+
query: Type.String(),
|
|
149
|
+
kind: Type.Optional(Type.Union([
|
|
150
|
+
Type.Literal("symbols"),
|
|
151
|
+
Type.Literal("files"),
|
|
152
|
+
Type.Literal("standards"),
|
|
153
|
+
Type.Literal("all"),
|
|
154
|
+
])),
|
|
155
|
+
}),
|
|
156
|
+
async execute(_toolCallId, params) {
|
|
157
|
+
const database = openTojiDatabase();
|
|
158
|
+
const result = queryMemory(database, params.query, params.kind ?? "all");
|
|
159
|
+
database.close();
|
|
160
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
pi.registerTool({
|
|
165
|
+
name: "toji_graph_explore",
|
|
166
|
+
label: "Graph Explore",
|
|
167
|
+
description: "Explore Toji graph memory from a natural-language intent. Use this for questions like 'what files would change?', 'what is affected?', or 'add something like X'. It finds seed symbols/files with FTS, traverses blast radius, and returns likely affected files.",
|
|
168
|
+
parameters: Type.Object({
|
|
169
|
+
intent: Type.String({ description: "Natural-language exploration goal, such as 'add a CLI adapter like hermes'." }),
|
|
170
|
+
seedQueries: Type.Optional(Type.Array(Type.String({ description: "Concrete terms to seed graph traversal, such as hermes, adapter, registry, cli." }))),
|
|
171
|
+
maxSeeds: Type.Optional(Type.Number({ description: "Max seed symbols. Defaults to 8." })),
|
|
172
|
+
depth: Type.Optional(Type.Number({ description: "Traversal depth. Defaults to 2." })),
|
|
173
|
+
direction: Type.Optional(Type.Union([Type.Literal("incoming"), Type.Literal("outgoing"), Type.Literal("both")])),
|
|
174
|
+
projectPath: Type.Optional(Type.String({ description: "Project root path. Defaults to current pi cwd." })),
|
|
175
|
+
}),
|
|
176
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
177
|
+
const database = openTojiDatabase();
|
|
178
|
+
const result = graphExplore(database, {
|
|
179
|
+
intent: params.intent,
|
|
180
|
+
seedQueries: params.seedQueries,
|
|
181
|
+
maxSeeds: params.maxSeeds,
|
|
182
|
+
depth: params.depth,
|
|
183
|
+
direction: params.direction,
|
|
184
|
+
projectPath: params.projectPath ?? ctx.cwd,
|
|
185
|
+
});
|
|
186
|
+
database.close();
|
|
187
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
pi.registerTool({
|
|
192
|
+
name: "toji_blast_radius",
|
|
193
|
+
label: "Blast Radius",
|
|
194
|
+
description: "Find likely impacted files and symbols by traversing Toji graph memory.",
|
|
195
|
+
parameters: Type.Object({
|
|
196
|
+
target: Type.String({ description: "Symbol name to analyze." }),
|
|
197
|
+
filePath: Type.Optional(Type.String({ description: "Optional file path to disambiguate symbols with the same name." })),
|
|
198
|
+
depth: Type.Optional(Type.Number({ description: "Traversal depth. Defaults to 2." })),
|
|
199
|
+
direction: Type.Optional(Type.Union([Type.Literal("incoming"), Type.Literal("outgoing"), Type.Literal("both")])),
|
|
200
|
+
edgeTypes: Type.Optional(Type.Array(Type.String({ description: "Edge type to traverse, such as symbol_calls_symbol." }))),
|
|
201
|
+
}),
|
|
202
|
+
async execute(_toolCallId, params) {
|
|
203
|
+
const database = openTojiDatabase();
|
|
204
|
+
const result = getBlastRadius(database, {
|
|
205
|
+
target: params.target,
|
|
206
|
+
filePath: params.filePath,
|
|
207
|
+
depth: params.depth,
|
|
208
|
+
direction: params.direction,
|
|
209
|
+
edgeTypes: params.edgeTypes,
|
|
210
|
+
});
|
|
211
|
+
database.close();
|
|
212
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: { result } };
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
pi.registerTool({
|
|
217
|
+
name: "toji_add_standard",
|
|
218
|
+
label: "Add Standard",
|
|
219
|
+
description: "Persist a global or project coding standard for future AI agent retrieval.",
|
|
220
|
+
parameters: Type.Object({
|
|
221
|
+
scope: Type.Union([Type.Literal("global"), Type.Literal("project")]),
|
|
222
|
+
projectPath: Type.Optional(Type.String()),
|
|
223
|
+
language: Type.Optional(Type.String()),
|
|
224
|
+
framework: Type.Optional(Type.String()),
|
|
225
|
+
rule: Type.String(),
|
|
226
|
+
rationale: Type.Optional(Type.String()),
|
|
227
|
+
priority: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")])),
|
|
228
|
+
}),
|
|
229
|
+
async execute(_toolCallId, params) {
|
|
230
|
+
const database = openTojiDatabase();
|
|
231
|
+
const id = addStandard(database, params);
|
|
232
|
+
database.close();
|
|
233
|
+
return { content: [{ type: "text", text: `Added standard ${id}.` }], details: { id } };
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
pi.registerTool({
|
|
238
|
+
name: "toji_get_standards",
|
|
239
|
+
label: "Get Standards",
|
|
240
|
+
description: "Retrieve applicable coding standards from Toji memory.",
|
|
241
|
+
parameters: Type.Object({
|
|
242
|
+
language: Type.Optional(Type.String()),
|
|
243
|
+
framework: Type.Optional(Type.String()),
|
|
244
|
+
}),
|
|
245
|
+
async execute(_toolCallId, params) {
|
|
246
|
+
const database = openTojiDatabase();
|
|
247
|
+
const standards = getStandards(database, params.language, params.framework);
|
|
248
|
+
database.close();
|
|
249
|
+
return { content: [{ type: "text", text: JSON.stringify(standards, null, 2) }], details: { standards } };
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function formatJson(value: unknown): string {
|
|
255
|
+
return `\n\`\`\`json\n${formatPlainJson(value)}\n\`\`\``;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatPlainJson(value: unknown): string {
|
|
259
|
+
return JSON.stringify(value, null, 2);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function compactValue(value: unknown, maxItems = 20): unknown {
|
|
263
|
+
if (Array.isArray(value)) return value.slice(0, maxItems).map((item) => compactValue(item, maxItems));
|
|
264
|
+
if (!value || typeof value !== "object") return value;
|
|
265
|
+
|
|
266
|
+
const input = value as Record<string, unknown>;
|
|
267
|
+
const output: Record<string, unknown> = {};
|
|
268
|
+
for (const [key, item] of Object.entries(input)) {
|
|
269
|
+
if (Array.isArray(item)) {
|
|
270
|
+
output[key] = item.slice(0, maxItems).map((entry) => compactValue(entry, maxItems));
|
|
271
|
+
if (item.length > maxItems) output[`${key}_truncated`] = item.length - maxItems;
|
|
272
|
+
} else if (item && typeof item === "object") {
|
|
273
|
+
output[key] = compactValue(item, maxItems);
|
|
274
|
+
} else {
|
|
275
|
+
output[key] = item;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return output;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function parseBlastArgs(args: string): {
|
|
282
|
+
target: string;
|
|
283
|
+
filePath?: string;
|
|
284
|
+
depth?: number;
|
|
285
|
+
direction?: "incoming" | "outgoing" | "both";
|
|
286
|
+
} {
|
|
287
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
288
|
+
const targetParts: string[] = [];
|
|
289
|
+
let filePath: string | undefined;
|
|
290
|
+
let depth: number | undefined;
|
|
291
|
+
let direction: "incoming" | "outgoing" | "both" | undefined;
|
|
292
|
+
|
|
293
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
294
|
+
const token = tokens[index];
|
|
295
|
+
if (!token) continue;
|
|
296
|
+
|
|
297
|
+
const next = tokens[index + 1];
|
|
298
|
+
|
|
299
|
+
if (token === "--file" && next) {
|
|
300
|
+
filePath = next;
|
|
301
|
+
index += 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (token === "--depth" && next) {
|
|
306
|
+
const parsedDepth = Number(next);
|
|
307
|
+
if (Number.isFinite(parsedDepth)) depth = parsedDepth;
|
|
308
|
+
index += 1;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (token === "--direction" && isBlastDirection(next)) {
|
|
313
|
+
direction = next;
|
|
314
|
+
index += 1;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
targetParts.push(token);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { target: targetParts.join(" "), filePath, depth, direction };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isBlastDirection(value: string | undefined): value is "incoming" | "outgoing" | "both" {
|
|
325
|
+
return value === "incoming" || value === "outgoing" || value === "both";
|
|
326
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
|
|
5
|
+
const ignoredDirectories = new Set([
|
|
6
|
+
".git",
|
|
7
|
+
".pi",
|
|
8
|
+
".zenin",
|
|
9
|
+
".dart_tool",
|
|
10
|
+
".gradle",
|
|
11
|
+
"Pods",
|
|
12
|
+
"ephemeral",
|
|
13
|
+
"node_modules",
|
|
14
|
+
"dist",
|
|
15
|
+
"build",
|
|
16
|
+
"coverage",
|
|
17
|
+
".next",
|
|
18
|
+
".turbo",
|
|
19
|
+
".venv",
|
|
20
|
+
"__pycache__",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export async function walkProjectFiles(rootPath: string, supportedExtensions: string[]): Promise<string[]> {
|
|
24
|
+
const results: string[] = [];
|
|
25
|
+
const extensionSet = new Set(supportedExtensions);
|
|
26
|
+
|
|
27
|
+
async function walk(directory: string): Promise<void> {
|
|
28
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
29
|
+
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const absolutePath = join(directory, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
if (!ignoredDirectories.has(entry.name) && !existsSync(join(absolutePath, ".git"))) await walk(absolutePath);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!entry.isFile()) continue;
|
|
38
|
+
if (!extensionSet.has(absolutePath.slice(absolutePath.lastIndexOf(".")))) continue;
|
|
39
|
+
results.push(relative(rootPath, absolutePath));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await walk(rootPath);
|
|
44
|
+
return results.sort();
|
|
45
|
+
}
|