@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,368 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+
4
+ import { addThreadContext, clearThreadContext, deleteThreadContext, getOrCreateThread, listThreadContext } from "./comms/store";
5
+ import type { CommsContextType, CommsMode, SourceModelInfo, TargetProvider } from "./comms/types";
6
+ import { openTojiCommsDatabase } from "./db/connection";
7
+ import { sendCommsThroughMcp } from "./mcp/client";
8
+
9
+ const modes = ["discuss", "ask", "critique", "decide", "verify", "plan"] as const;
10
+ const providers = ["mmx", "claude", "codex"] as const;
11
+ const contextTypes = ["project_overview", "graph_result", "file_snippet", "decision", "constraint", "research_summary", "benchmark_result", "user_preference", "note"] as const;
12
+
13
+ export default function (pi: ExtensionAPI) {
14
+ registerCommsRenderer(pi);
15
+
16
+ let currentModel: SourceModelInfo = {};
17
+ let currentThreadId: number | undefined;
18
+ let defaultMode: CommsMode = "discuss";
19
+ let defaultTargetProvider: TargetProvider = "mmx";
20
+ let defaultTargetModel: string | undefined;
21
+
22
+ pi.on("model_select", (event) => {
23
+ currentModel = readModelInfo(event.model);
24
+ });
25
+
26
+ pi.registerCommand("toji-comms", {
27
+ description: "Raw Toji Comms command. Prefer asking the active AI to use toji_comms_send so it can compose context first. Usage: /toji-comms <message>",
28
+ handler: async (args, ctx) => {
29
+ const sourceMessage = args.trim();
30
+ if (!sourceMessage) {
31
+ ctx.ui.notify("Usage: /toji-comms <message>", "error");
32
+ return;
33
+ }
34
+
35
+ ctx.ui.notify(`Toji Comms raw mode sends text directly. Prefer toji_comms_send for true AI-to-AI context. Sending ${defaultMode} message to ${defaultTargetProvider}...`, "info");
36
+ pi.sendMessage({ customType: "toji-comms-user", content: formatVisibleUserPrompt(sourceMessage), display: true });
37
+
38
+ const result = await sendCommsThroughMcp({
39
+ cwd: ctx.cwd,
40
+ threadId: currentThreadId,
41
+ title: sourceMessage,
42
+ mode: defaultMode,
43
+ sourceModel: currentModel,
44
+ sessionId: ctx.sessionManager.getSessionFile(),
45
+ targetProvider: defaultTargetProvider,
46
+ targetModel: defaultTargetModel,
47
+ context: {
48
+ userRequest: sourceMessage,
49
+ sourceMessage,
50
+ contextMode: ["brief", "thread", "recentTurns"],
51
+ },
52
+ }, ctx.signal);
53
+
54
+ currentThreadId = result.threadId;
55
+ sendResultMessages(pi, result);
56
+ },
57
+ });
58
+
59
+ pi.registerCommand("toji-comms-model", {
60
+ description: "Set default Toji Comms target model. Usage: /toji-comms-model <model>",
61
+ handler: async (args, ctx) => {
62
+ defaultTargetModel = args.trim() || undefined;
63
+ ctx.ui.notify(defaultTargetModel ? `Toji Comms model set to ${defaultTargetModel}` : "Toji Comms model reset to provider default", "info");
64
+ },
65
+ });
66
+
67
+ pi.registerCommand("toji-comms-provider", {
68
+ description: "Set default Toji Comms provider. Usage: /toji-comms-provider mmx|claude|codex",
69
+ handler: async (args, ctx) => {
70
+ const provider = args.trim() as TargetProvider;
71
+ if (!isProvider(provider)) {
72
+ ctx.ui.notify(`Usage: /toji-comms-provider ${providers.join("|")}`, "error");
73
+ return;
74
+ }
75
+ defaultTargetProvider = provider;
76
+ ctx.ui.notify(`Toji Comms provider set to ${defaultTargetProvider}`, "info");
77
+ },
78
+ });
79
+
80
+ pi.registerCommand("toji-comms-mode", {
81
+ description: "Set default Toji Comms mode. Usage: /toji-comms-mode discuss|ask|critique|decide|verify|plan",
82
+ handler: async (args, ctx) => {
83
+ const mode = args.trim() as CommsMode;
84
+ if (!isMode(mode)) {
85
+ ctx.ui.notify(`Usage: /toji-comms-mode ${modes.join("|")}`, "error");
86
+ return;
87
+ }
88
+ defaultMode = mode;
89
+ ctx.ui.notify(`Toji Comms mode set to ${defaultMode}`, "info");
90
+ },
91
+ });
92
+
93
+ pi.registerCommand("toji-comms-new", {
94
+ description: "Start a new Toji Comms thread. Usage: /toji-comms-new [title]",
95
+ handler: async (args, ctx) => {
96
+ currentThreadId = undefined;
97
+ ctx.ui.notify(args.trim() ? `Next Toji Comms message will start a new thread: ${args.trim()}` : "Next Toji Comms message will start a new thread", "info");
98
+ },
99
+ });
100
+
101
+ pi.registerCommand("toji-plan", {
102
+ description: "Compatibility alias for /toji-comms in plan mode. Usage: /toji-plan <message>",
103
+ handler: async (args, ctx) => {
104
+ const sourceMessage = args.trim();
105
+ if (!sourceMessage) {
106
+ ctx.ui.notify("Usage: /toji-plan <message>", "error");
107
+ return;
108
+ }
109
+
110
+ ctx.ui.notify(`Toji Plan alias sending plan-mode message to ${defaultTargetProvider}...`, "info");
111
+ pi.sendMessage({ customType: "toji-comms-user", content: formatVisibleUserPrompt(sourceMessage), display: true });
112
+ const result = await sendCommsThroughMcp({
113
+ cwd: ctx.cwd,
114
+ threadId: currentThreadId,
115
+ title: sourceMessage,
116
+ mode: "plan",
117
+ sourceModel: currentModel,
118
+ sessionId: ctx.sessionManager.getSessionFile(),
119
+ targetProvider: defaultTargetProvider,
120
+ targetModel: defaultTargetModel,
121
+ context: {
122
+ userRequest: sourceMessage,
123
+ sourceMessage,
124
+ contextMode: ["brief", "thread", "recentTurns"],
125
+ },
126
+ }, ctx.signal);
127
+
128
+ currentThreadId = result.threadId;
129
+ sendResultMessages(pi, result);
130
+ },
131
+ });
132
+
133
+ pi.registerTool({
134
+ name: "toji_comms_send",
135
+ label: "Send Toji Comms",
136
+ description: "Send an AI-authored message to a peer AI through MCP. Store raw user text in userRequest, but sourceMessage must be the active model's composed message with interpretation, context, constraints, findings, and requested peer response. Stores user/source/peer turns in SQLite and returns the rendered prompt plus peer reply.",
137
+ parameters: Type.Object({
138
+ userRequest: Type.Optional(Type.String({ description: "The user's original request, if relevant." })),
139
+ sourceMessage: Type.String({ description: "The active AI's full composed message to the peer AI. Do not copy the user's raw prompt. Include interpretation, relevant context, constraints, current findings, and what response you need from the peer." }),
140
+ knownContext: Type.Optional(Type.Array(Type.String(), { description: "Concise context packet facts for the peer AI, such as relevant files, symbols, decisions, constraints, tool results, or session facts." })),
141
+ questionsForPeer: Type.Optional(Type.Array(Type.String(), { description: "Specific questions the peer AI should answer." })),
142
+ mode: Type.Optional(Type.Union(modes.map((mode) => Type.Literal(mode)), { description: "Conversation mode. Defaults to discuss." })),
143
+ threadId: Type.Optional(Type.Number({ description: "Existing Toji Comms thread id. Defaults to active thread for cwd." })),
144
+ title: Type.Optional(Type.String({ description: "New thread title when no thread id exists." })),
145
+ targetProvider: Type.Optional(Type.Union(providers.map((provider) => Type.Literal(provider)), { description: "Optional CLI provider. Defaults to /toji-comms-provider or mmx." })),
146
+ targetModel: Type.Optional(Type.String({ description: "Optional provider model id." })),
147
+ includeThreadContext: Type.Optional(Type.Boolean({ description: "Include reusable context rows for the thread. Defaults to true." })),
148
+ contextIds: Type.Optional(Type.Array(Type.Number({ description: "Specific reusable context row IDs to include." }))),
149
+ contextMode: Type.Optional(Type.Array(Type.Union([Type.Literal("brief"), Type.Literal("thread"), Type.Literal("recentTurns"), Type.Literal("threadContext")]))),
150
+ }),
151
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
152
+ if (isProbablyRawUserPrompt(params.userRequest, params.sourceMessage)) {
153
+ return {
154
+ content: [{ type: "text", text: "toji_comms_send rejected this call because sourceMessage appears to copy the raw userRequest. Compose an AI-authored sourceMessage with interpretation, context, findings, constraints, and questions for the peer." }],
155
+ details: { userRequest: params.userRequest, sourceMessage: params.sourceMessage },
156
+ isError: true,
157
+ };
158
+ }
159
+
160
+ onUpdate?.({ content: [{ type: "text", text: "Sending AI-to-AI message through Toji Comms..." }], details: {} });
161
+ const result = await sendCommsThroughMcp({
162
+ cwd: ctx.cwd,
163
+ threadId: params.threadId ?? currentThreadId,
164
+ title: params.title ?? params.userRequest ?? params.sourceMessage,
165
+ mode: params.mode ?? defaultMode,
166
+ sourceModel: currentModel,
167
+ sessionId: ctx.sessionManager.getSessionFile(),
168
+ targetProvider: params.targetProvider ?? defaultTargetProvider,
169
+ targetModel: params.targetModel ?? defaultTargetModel,
170
+ includeThreadContext: params.includeThreadContext,
171
+ contextIds: params.contextIds,
172
+ context: {
173
+ userRequest: params.userRequest,
174
+ sourceMessage: params.sourceMessage,
175
+ knownContext: params.knownContext,
176
+ questionsForPeer: params.questionsForPeer,
177
+ contextMode: params.contextMode ?? ["brief", "thread", "threadContext", "recentTurns"],
178
+ },
179
+ }, signal);
180
+
181
+ currentThreadId = result.threadId;
182
+ pi.sendMessage({ customType: "toji-comms-source", content: formatVisibleRenderedPrompt(result.prompt), display: true });
183
+ return {
184
+ content: [{ type: "text", text: result.reply }],
185
+ details: result,
186
+ isError: result.status === "failed",
187
+ };
188
+ },
189
+ });
190
+
191
+ pi.registerTool({
192
+ name: "toji_comms_context",
193
+ label: "Toji Comms Context",
194
+ description: "Manage reusable context rows for the active Toji Comms thread. Use this to store project overviews, graph results, decisions, constraints, file snippets, research summaries, benchmark results, and user preferences for later peer prompts.",
195
+ parameters: Type.Object({
196
+ action: Type.Union([Type.Literal("add"), Type.Literal("list"), Type.Literal("delete"), Type.Literal("clear")]),
197
+ threadId: Type.Optional(Type.Number({ description: "Thread id. Defaults to active thread for cwd." })),
198
+ type: Type.Optional(Type.Union(contextTypes.map((type) => Type.Literal(type)), { description: "Context row type for add." })),
199
+ title: Type.Optional(Type.String({ description: "Short context title for add." })),
200
+ content: Type.Optional(Type.String({ description: "Context content for add." })),
201
+ source: Type.Optional(Type.String({ description: "Where this context came from, such as toji_project_overview." })),
202
+ id: Type.Optional(Type.Number({ description: "Context row id for delete." })),
203
+ }),
204
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
205
+ const database = openTojiCommsDatabase();
206
+ try {
207
+ const thread = params.threadId === undefined
208
+ ? getOrCreateThread(database, {
209
+ cwd: ctx.cwd,
210
+ title: params.title ?? "Toji Comms context",
211
+ sourceModel: currentModel,
212
+ sessionId: ctx.sessionManager.getSessionFile(),
213
+ context: { sourceMessage: params.content ?? params.title ?? "Context management" },
214
+ })
215
+ : undefined;
216
+ const threadId = params.threadId ?? thread?.id;
217
+ if (!threadId) throw new Error("No Toji Comms thread available.");
218
+ currentThreadId = threadId;
219
+
220
+ if (params.action === "add") {
221
+ if (!params.type || !params.title || !params.content) throw new Error("action=add requires type, title, and content.");
222
+ const id = addThreadContext(database, {
223
+ threadId,
224
+ type: params.type as CommsContextType,
225
+ title: params.title,
226
+ content: params.content,
227
+ source: params.source,
228
+ });
229
+ return { content: [{ type: "text", text: `Added Toji Comms context #${id} to thread #${threadId}.` }], details: { id, threadId } };
230
+ }
231
+
232
+ if (params.action === "list") {
233
+ const contexts = listThreadContext(database, threadId, 20);
234
+ return { content: [{ type: "text", text: JSON.stringify(contexts, null, 2) }], details: { threadId, contexts } };
235
+ }
236
+
237
+ if (params.action === "delete") {
238
+ if (params.id === undefined) throw new Error("action=delete requires id.");
239
+ const deleted = deleteThreadContext(database, params.id);
240
+ return { content: [{ type: "text", text: deleted ? `Deleted Toji Comms context #${params.id}.` : `No context row found for #${params.id}.` }], details: { id: params.id, deleted } };
241
+ }
242
+
243
+ clearThreadContext(database, threadId);
244
+ return { content: [{ type: "text", text: `Cleared Toji Comms context for thread #${threadId}.` }], details: { threadId } };
245
+ } finally {
246
+ database.close();
247
+ }
248
+ },
249
+ });
250
+
251
+ pi.registerTool({
252
+ name: "toji_plan_run",
253
+ label: "Run Toji Plan",
254
+ description: "Compatibility alias for toji_comms_send with mode=plan.",
255
+ parameters: Type.Object({
256
+ userRequest: Type.String({ description: "The user's original request or goal." }),
257
+ localAssessment: Type.Optional(Type.String({ description: "The current model's interpretation and context." })),
258
+ knownContext: Type.Optional(Type.Array(Type.String())),
259
+ proposedPlan: Type.Optional(Type.Array(Type.String())),
260
+ questionsForPeer: Type.Optional(Type.Array(Type.String())),
261
+ provider: Type.Optional(Type.Union(providers.map((provider) => Type.Literal(provider)))),
262
+ model: Type.Optional(Type.String()),
263
+ }),
264
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
265
+ onUpdate?.({ content: [{ type: "text", text: "Sending plan-mode message through Toji Comms..." }], details: {} });
266
+ const sourceMessage = [
267
+ params.localAssessment ?? `User request: ${params.userRequest}`,
268
+ params.proposedPlan?.length ? `Proposed plan:\n${params.proposedPlan.map((item, index) => `${index + 1}. ${item}`).join("\n")}` : undefined,
269
+ ].filter(Boolean).join("\n\n");
270
+ const result = await sendCommsThroughMcp({
271
+ cwd: ctx.cwd,
272
+ threadId: currentThreadId,
273
+ title: params.userRequest,
274
+ mode: "plan",
275
+ sourceModel: currentModel,
276
+ sessionId: ctx.sessionManager.getSessionFile(),
277
+ targetProvider: params.provider ?? defaultTargetProvider,
278
+ targetModel: params.model ?? defaultTargetModel,
279
+ context: {
280
+ userRequest: params.userRequest,
281
+ sourceMessage,
282
+ knownContext: params.knownContext,
283
+ questionsForPeer: params.questionsForPeer,
284
+ contextMode: ["brief", "thread", "recentTurns"],
285
+ },
286
+ }, signal);
287
+
288
+ currentThreadId = result.threadId;
289
+ pi.sendMessage({ customType: "toji-comms-source", content: formatVisibleRenderedPrompt(result.prompt), display: true });
290
+ return { content: [{ type: "text", text: result.reply }], details: result, isError: result.status === "failed" };
291
+ },
292
+ });
293
+ }
294
+
295
+ function sendResultMessages(pi: ExtensionAPI, result: { threadId: number; prompt: string; reply: string }): void {
296
+ pi.sendMessage({ customType: "toji-comms-source", content: formatVisibleRenderedPrompt(result.prompt), display: true });
297
+ pi.sendMessage({ customType: "toji-comms", content: `Toji Comms thread #${result.threadId}\n\n${result.reply}`, display: true, details: result });
298
+ }
299
+
300
+ function registerCommsRenderer(pi: ExtensionAPI): void {
301
+ pi.registerMessageRenderer("toji-comms", (message, { expanded }) => {
302
+ const details = message.details as { threadId?: number; reply?: string; status?: string } | undefined;
303
+ const threadId = details?.threadId;
304
+ const status = details?.status ?? "completed";
305
+ const reply = details?.reply ?? String(message.content);
306
+ const heading = `Toji Comms${threadId ? ` thread #${threadId}` : ""}${status === "failed" ? " failed" : ""}`;
307
+ const body = expanded ? reply : clipLines(reply, 18);
308
+ const hint = expanded ? "" : "\n… clipped. Press Ctrl+O to expand peer reply.";
309
+ return {
310
+ render: (width: number) => `${heading}\n\n${body}${hint}`.split("\n").map((line) => truncateLine(line, width)),
311
+ invalidate: () => {},
312
+ };
313
+ });
314
+ }
315
+
316
+ function clipLines(value: string, maxLines: number): string {
317
+ const lines = value.trim().split("\n");
318
+ if (lines.length <= maxLines) return value.trim();
319
+ return lines.slice(0, maxLines).join("\n");
320
+ }
321
+
322
+ function truncateLine(line: string, width: number): string {
323
+ if (width <= 1) return "";
324
+ if (line.length <= width) return line;
325
+ return `${line.slice(0, width - 1)}…`;
326
+ }
327
+
328
+ function isProbablyRawUserPrompt(userRequest: string | undefined, sourceMessage: string): boolean {
329
+ if (!userRequest) return false;
330
+ const normalizedUser = normalizePromptText(userRequest);
331
+ const normalizedSource = normalizePromptText(sourceMessage);
332
+ if (!normalizedUser || !normalizedSource) return false;
333
+ return normalizedUser === normalizedSource;
334
+ }
335
+
336
+ function normalizePromptText(value: string): string {
337
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
338
+ }
339
+
340
+ function formatVisibleUserPrompt(prompt: string): string {
341
+ return `User prompt sent to Toji Comms:\n\n${prompt}`;
342
+ }
343
+
344
+ function formatVisibleRenderedPrompt(prompt: string): string {
345
+ return `Rendered prompt sent to peer CLI:\n\n${prompt}`;
346
+ }
347
+
348
+ function formatStatus(enabled: boolean): string {
349
+ return `Toji Plan mode: ${enabled ? "on" : "off"}`;
350
+ }
351
+
352
+ function isMode(value: string): value is CommsMode {
353
+ return modes.includes(value as CommsMode);
354
+ }
355
+
356
+ function isProvider(value: string): value is TargetProvider {
357
+ return providers.includes(value as TargetProvider);
358
+ }
359
+
360
+ function readModelInfo(model: unknown): SourceModelInfo {
361
+ if (!model || typeof model !== "object") return {};
362
+
363
+ const record = model as Record<string, unknown>;
364
+ return {
365
+ provider: typeof record.provider === "string" ? record.provider : undefined,
366
+ id: typeof record.id === "string" ? record.id : undefined,
367
+ };
368
+ }
@@ -0,0 +1,71 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3
+
4
+ import type { CommsRequest, CommsResult } from "../comms/types";
5
+ import { createTojiCommsMcpServer } from "./server";
6
+
7
+ const TOJI_COMMS_MCP_TIMEOUT_MS = 180_000;
8
+
9
+ export async function sendCommsThroughMcp(request: CommsRequest, signal?: AbortSignal): Promise<CommsResult> {
10
+ const server = createTojiCommsMcpServer();
11
+ const client = new Client({ name: "toji-comms-pi-extension", version: "0.1.0" });
12
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
13
+
14
+ try {
15
+ await Promise.all([
16
+ client.connect(clientTransport),
17
+ server.connect(serverTransport),
18
+ ]);
19
+
20
+ const result = await client.callTool({
21
+ name: "toji_comms_send",
22
+ arguments: {
23
+ cwd: request.cwd,
24
+ threadId: request.threadId,
25
+ title: request.title,
26
+ mode: request.mode,
27
+ sessionId: request.sessionId,
28
+ sourceModelProvider: request.sourceModel?.provider,
29
+ sourceModelId: request.sourceModel?.id,
30
+ targetProvider: request.targetProvider,
31
+ targetModel: request.targetModel,
32
+ system: request.system,
33
+ userRequest: request.context.userRequest,
34
+ sourceMessage: request.context.sourceMessage,
35
+ knownContext: request.context.knownContext,
36
+ questionsForPeer: request.context.questionsForPeer,
37
+ contextMode: request.context.contextMode,
38
+ includeThreadContext: request.includeThreadContext,
39
+ contextIds: request.contextIds,
40
+ },
41
+ }, undefined, { signal, timeout: TOJI_COMMS_MCP_TIMEOUT_MS });
42
+
43
+ return parseCommsResult(result.structuredContent ?? readTextContent(result.content));
44
+ } finally {
45
+ await client.close();
46
+ await server.close();
47
+ }
48
+ }
49
+
50
+ function parseCommsResult(value: unknown): CommsResult {
51
+ if (typeof value === "string") {
52
+ try {
53
+ return parseCommsResult(JSON.parse(value));
54
+ } catch {
55
+ throw new Error(`MCP server returned invalid Toji Comms JSON: ${value.slice(0, 200)}`);
56
+ }
57
+ }
58
+
59
+ if (!value || typeof value !== "object") {
60
+ throw new Error(`MCP server returned an invalid Toji Comms result: ${String(value)}`);
61
+ }
62
+
63
+ return value as CommsResult;
64
+ }
65
+
66
+ function readTextContent(content: unknown): string | undefined {
67
+ if (!Array.isArray(content)) return undefined;
68
+ const firstText = content.find((item) => item && typeof item === "object" && "type" in item && item.type === "text" && "text" in item);
69
+ if (!firstText || typeof firstText !== "object" || !("text" in firstText) || typeof firstText.text !== "string") return undefined;
70
+ return firstText.text;
71
+ }
@@ -0,0 +1,81 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+
4
+ import { sendComms } from "../comms/orchestrator";
5
+ import type { CommsMode, CommsRequest } from "../comms/types";
6
+ import { openTojiCommsDatabase } from "../db/connection";
7
+
8
+ const modeSchema = z.enum(["discuss", "ask", "critique", "decide", "verify", "plan"]);
9
+ const providerSchema = z.enum(["mmx", "claude", "codex"]);
10
+
11
+ export function createTojiCommsMcpServer(): McpServer {
12
+ const database = openTojiCommsDatabase();
13
+ const server = new McpServer({ name: "toji-comms", version: "0.1.0" });
14
+
15
+ server.registerTool(
16
+ "toji_comms_send",
17
+ {
18
+ title: "Send Toji Comms Message",
19
+ description: "Store an AI-to-AI communication turn, send the rendered context packet to a CLI agent, persist the peer reply, and return the result.",
20
+ inputSchema: {
21
+ cwd: z.string(),
22
+ threadId: z.number().optional(),
23
+ title: z.string().optional(),
24
+ mode: modeSchema.optional(),
25
+ sessionId: z.string().optional(),
26
+ sourceModelProvider: z.string().optional(),
27
+ sourceModelId: z.string().optional(),
28
+ targetProvider: providerSchema.optional(),
29
+ targetModel: z.string().optional(),
30
+ system: z.string().optional(),
31
+ userRequest: z.string().optional(),
32
+ sourceMessage: z.string(),
33
+ knownContext: z.array(z.string()).optional(),
34
+ questionsForPeer: z.array(z.string()).optional(),
35
+ contextMode: z.array(z.enum(["brief", "thread", "recentTurns", "threadContext"])).optional(),
36
+ includeThreadContext: z.boolean().optional(),
37
+ contextIds: z.array(z.number()).optional(),
38
+ },
39
+ },
40
+ async (params, extra) => {
41
+ const request: CommsRequest = {
42
+ cwd: params.cwd,
43
+ threadId: params.threadId,
44
+ title: params.title,
45
+ mode: params.mode as CommsMode | undefined,
46
+ sessionId: params.sessionId,
47
+ sourceModel: {
48
+ provider: params.sourceModelProvider,
49
+ id: params.sourceModelId,
50
+ },
51
+ targetProvider: params.targetProvider ?? "mmx",
52
+ targetModel: params.targetModel,
53
+ system: params.system,
54
+ includeThreadContext: params.includeThreadContext,
55
+ contextIds: params.contextIds,
56
+ context: {
57
+ userRequest: params.userRequest,
58
+ sourceMessage: params.sourceMessage,
59
+ knownContext: params.knownContext,
60
+ questionsForPeer: params.questionsForPeer,
61
+ contextMode: params.contextMode,
62
+ },
63
+ };
64
+ const result = await sendComms(database, request, extra.signal);
65
+
66
+ return {
67
+ content: [{ type: "text", text: JSON.stringify(result) }],
68
+ structuredContent: result as unknown as Record<string, unknown>,
69
+ isError: result.status === "failed",
70
+ };
71
+ },
72
+ );
73
+
74
+ const originalClose = server.close.bind(server);
75
+ server.close = async () => {
76
+ database.close();
77
+ await originalClose();
78
+ };
79
+
80
+ return server;
81
+ }
@@ -0,0 +1,52 @@
1
+ # toji-mem
2
+
3
+ Graph memory for pi agents backed by SQLite, FTS5, and tree-sitter.
4
+
5
+ ## Goals
6
+
7
+ - Index codebases as queryable graph memory.
8
+ - Replace repeated ripgrep/find usage with symbol, file, and standards queries.
9
+ - Estimate blast impact radius through graph traversal.
10
+ - Store global and project coding standards for agent retrieval.
11
+
12
+ ## Tools
13
+
14
+ - `toji_index_project`: index the current project.
15
+ - `toji_query_memory`: search symbols, files, and standards.
16
+ - `toji_blast_radius`: inspect likely impacted symbols/files.
17
+ - `toji_add_standard`: persist coding standards.
18
+ - `toji_get_standards`: retrieve applicable standards.
19
+
20
+ ## Setup
21
+
22
+ From the monorepo root:
23
+
24
+ ```bash
25
+ bun install
26
+ bun run toji-mem:download-grammars
27
+ bun run toji-mem:typecheck
28
+ ```
29
+
30
+ Test in pi:
31
+
32
+ ```bash
33
+ pi -e ./packages/toji-mem
34
+ ```
35
+
36
+ ## Storage
37
+
38
+ The SQLite database is stored at:
39
+
40
+ ```txt
41
+ ~/.pi/agent/toji-mem/toji.sqlite
42
+ ```
43
+
44
+ ## Supported languages in the first scaffold
45
+
46
+ - TypeScript
47
+ - TSX
48
+ - JavaScript
49
+ - JSX
50
+ - Python
51
+
52
+ More grammars can be added by extending `scripts/download-grammars.ts` and `src/indexer/parsers/registry.ts`.
@@ -0,0 +1,9 @@
1
+ {
2
+ "typescript": "tree-sitter-typescript.wasm",
3
+ "tsx": "tree-sitter-tsx.wasm",
4
+ "javascript": "tree-sitter-javascript.wasm",
5
+ "python": "tree-sitter-python.wasm",
6
+ "dart": "tree-sitter-dart.wasm",
7
+ "cpp": "tree-sitter-cpp.wasm",
8
+ "java": "tree-sitter-java.wasm"
9
+ }
@@ -0,0 +1,58 @@
1
+ import { DatabaseSync, type SQLInputValue, type StatementSync } from "node:sqlite";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { mkdirSync } from "node:fs";
5
+
6
+ import { runMigrations } from "./migrations";
7
+
8
+ export interface TojiStatement<Result = unknown, Params extends SQLInputValue[] = SQLInputValue[]> {
9
+ run(...params: Params): unknown;
10
+ get(...params: Params): Result | undefined;
11
+ all(...params: Params): Result[];
12
+ }
13
+
14
+ export interface TojiDatabase {
15
+ exec(sql: string): void;
16
+ query<Result = unknown, Params extends SQLInputValue[] = SQLInputValue[]>(sql: string): TojiStatement<Result, Params>;
17
+ close(): void;
18
+ }
19
+
20
+ class NodeSqliteDatabase implements TojiDatabase {
21
+ readonly #database: DatabaseSync;
22
+
23
+ constructor(databasePath: string) {
24
+ this.#database = new DatabaseSync(databasePath, { open: true });
25
+ }
26
+
27
+ exec(sql: string): void {
28
+ this.#database.exec(sql);
29
+ }
30
+
31
+ query<Result = unknown, Params extends SQLInputValue[] = SQLInputValue[]>(sql: string): TojiStatement<Result, Params> {
32
+ const statement = this.#database.prepare(sql) as StatementSync;
33
+ return {
34
+ run: (...params: Params) => statement.run(...params),
35
+ get: (...params: Params) => statement.get(...params) as Result | undefined,
36
+ all: (...params: Params) => statement.all(...params) as Result[],
37
+ };
38
+ }
39
+
40
+ close(): void {
41
+ this.#database.close();
42
+ }
43
+ }
44
+
45
+ export function getDefaultDatabasePath(): string {
46
+ return join(homedir(), ".pi", "agent", "toji-mem", "toji.sqlite");
47
+ }
48
+
49
+ export function openTojiDatabase(databasePath = getDefaultDatabasePath()): TojiDatabase {
50
+ mkdirSync(dirname(databasePath), { recursive: true });
51
+
52
+ const database = new NodeSqliteDatabase(databasePath);
53
+ database.exec("PRAGMA foreign_keys = ON;");
54
+ database.exec("PRAGMA journal_mode = WAL;");
55
+ runMigrations(database);
56
+
57
+ return database;
58
+ }