@topodb/pi 0.0.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 +14 -0
- package/dist/extension.js +52 -0
- package/dist/mcp-client.js +110 -0
- package/dist/server-handle.js +41 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @topodb/pi
|
|
2
|
+
|
|
3
|
+
One-command [TopoDB](https://github.com/TopoDB/TopoDB) memory for the
|
|
4
|
+
[Pi](https://pi.dev) coding agent.
|
|
5
|
+
|
|
6
|
+
pi install npm:@topodb/pi
|
|
7
|
+
|
|
8
|
+
Registers a single `topodb` tool that lazily spawns the `topodb-mcp` server and
|
|
9
|
+
proxies its 16 memory tools. Call `{action:"list"}` to discover them, then
|
|
10
|
+
`{tool, args}` to use one. Config via env: `TOPODB_DB` (default
|
|
11
|
+
`.topodb/memory.redb`), `TOPODB_SCOPE` (default `shared`).
|
|
12
|
+
|
|
13
|
+
No Rust toolchain and no separate MCP adapter required — the prebuilt
|
|
14
|
+
`topodb-mcp` binary is pulled in automatically for your platform.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { TopodbServer } from "./server-handle.js";
|
|
3
|
+
export default function (pi) {
|
|
4
|
+
const server = new TopodbServer();
|
|
5
|
+
pi.registerTool({
|
|
6
|
+
name: "topodb",
|
|
7
|
+
label: "topodb memory",
|
|
8
|
+
description: "Persistent agent memory (temporal property graph). Call with " +
|
|
9
|
+
'{action:"list"} to see the available sub-tools and their schemas, then ' +
|
|
10
|
+
'{tool:"<name>", args:{...}} to invoke one.',
|
|
11
|
+
promptSnippet: "topodb: persistent memory. {action:\"list\"} to discover, then {tool,args}.",
|
|
12
|
+
promptGuidelines: [
|
|
13
|
+
"Before answering from prior context, recall with topodb search_memories/traverse.",
|
|
14
|
+
"Store durable facts (decisions, entities, relationships) with create_memory/create_entity/link.",
|
|
15
|
+
],
|
|
16
|
+
parameters: Type.Object({
|
|
17
|
+
action: Type.Optional(Type.Literal("list")),
|
|
18
|
+
tool: Type.Optional(Type.String()),
|
|
19
|
+
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
20
|
+
}),
|
|
21
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
22
|
+
try {
|
|
23
|
+
if (params.action === "list" && params.tool) {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{ type: "text", text: 'error: provide exactly one of {action:"list"} or {tool, args}' },
|
|
27
|
+
],
|
|
28
|
+
details: { error: "bad-params" },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (params.action === "list") {
|
|
32
|
+
const tools = await server.list();
|
|
33
|
+
return { content: [{ type: "text", text: JSON.stringify(tools) }], details: { action: "list" } };
|
|
34
|
+
}
|
|
35
|
+
if (!params.tool) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text", text: 'error: provide {action:"list"} or {tool, args}' }],
|
|
38
|
+
details: { error: "bad-params" },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const result = await server.call(params.tool, params.args ?? {});
|
|
42
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { tool: params.tool } };
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
return { content: [{ type: "text", text: `topodb error: ${e.message}` }], details: { error: true } };
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
pi.on("session_shutdown", async () => {
|
|
50
|
+
server.shutdown();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/mcp-client.ts
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
5
|
+
export class McpStdioClient {
|
|
6
|
+
nodeArgs;
|
|
7
|
+
child;
|
|
8
|
+
rl;
|
|
9
|
+
nextId = 1;
|
|
10
|
+
pending = new Map();
|
|
11
|
+
requestTimeoutMs;
|
|
12
|
+
command;
|
|
13
|
+
constructor(nodeArgs, opts = {}) {
|
|
14
|
+
this.nodeArgs = nodeArgs;
|
|
15
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
16
|
+
this.command = opts.command ?? process.execPath;
|
|
17
|
+
}
|
|
18
|
+
get running() {
|
|
19
|
+
return !!this.child && this.child.exitCode === null && !this.child.killed;
|
|
20
|
+
}
|
|
21
|
+
async start() {
|
|
22
|
+
const child = spawn(this.command, this.nodeArgs, {
|
|
23
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
24
|
+
});
|
|
25
|
+
this.child = child;
|
|
26
|
+
// A spawn failure (bad executable, EMFILE/EAGAIN, ...) emits 'error' instead of
|
|
27
|
+
// (or in addition to) 'exit'. Without a handler this is an uncaught exception
|
|
28
|
+
// that crashes the host process. Route it through the same failAll() path.
|
|
29
|
+
child.on("error", (e) => this.failAll(e));
|
|
30
|
+
// Writing to stdin after the child has died surfaces as EPIPE here. failAll()
|
|
31
|
+
// (via the 'exit'/'error' handlers above) already rejects pending requests, so
|
|
32
|
+
// this handler exists purely to prevent an unhandled 'error' event from
|
|
33
|
+
// throwing and crashing the host process.
|
|
34
|
+
child.stdin.on("error", () => { });
|
|
35
|
+
this.rl = createInterface({ input: child.stdout });
|
|
36
|
+
this.rl.on("line", (line) => this.onLine(line));
|
|
37
|
+
child.on("exit", () => this.failAll(new Error("topodb-mcp exited")));
|
|
38
|
+
await this.request("initialize", {
|
|
39
|
+
protocolVersion: "2025-11-25",
|
|
40
|
+
capabilities: {},
|
|
41
|
+
clientInfo: { name: "topodb-pi", version: "0.0.1" },
|
|
42
|
+
});
|
|
43
|
+
this.notify("notifications/initialized");
|
|
44
|
+
}
|
|
45
|
+
async listTools() {
|
|
46
|
+
const r = (await this.request("tools/list"));
|
|
47
|
+
return r.tools ?? [];
|
|
48
|
+
}
|
|
49
|
+
async callTool(name, args) {
|
|
50
|
+
const r = (await this.request("tools/call", { name, arguments: args }));
|
|
51
|
+
// NOTE: tool-level MCP `isError:true` results are currently surfaced as a
|
|
52
|
+
// success (the content is returned as-is). Treating that as a rejection is
|
|
53
|
+
// deferred — accepted v0 limitation, tracked as finding 5.
|
|
54
|
+
return r.structuredContent ?? r.content ?? r;
|
|
55
|
+
}
|
|
56
|
+
stop() {
|
|
57
|
+
this.rl?.close();
|
|
58
|
+
this.child?.kill();
|
|
59
|
+
this.failAll(new Error("client stopped"));
|
|
60
|
+
}
|
|
61
|
+
onLine(line) {
|
|
62
|
+
let msg;
|
|
63
|
+
try {
|
|
64
|
+
msg = JSON.parse(line);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (typeof msg?.id !== "number")
|
|
70
|
+
return; // notifications/logs
|
|
71
|
+
const p = this.pending.get(msg.id);
|
|
72
|
+
if (!p)
|
|
73
|
+
return;
|
|
74
|
+
this.pending.delete(msg.id);
|
|
75
|
+
if (msg.error)
|
|
76
|
+
p.reject(new Error(msg.error.message ?? "MCP error"));
|
|
77
|
+
else
|
|
78
|
+
p.resolve(msg.result);
|
|
79
|
+
}
|
|
80
|
+
request(method, params) {
|
|
81
|
+
const id = this.nextId++;
|
|
82
|
+
const line = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
this.pending.delete(id);
|
|
86
|
+
reject(new Error(`topodb-mcp request timed out after ${this.requestTimeoutMs}ms: ${method}`));
|
|
87
|
+
}, this.requestTimeoutMs);
|
|
88
|
+
timer.unref?.();
|
|
89
|
+
this.pending.set(id, {
|
|
90
|
+
resolve: (v) => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve(v);
|
|
93
|
+
},
|
|
94
|
+
reject: (e) => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
reject(e);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
this.child.stdin.write(line);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
notify(method, params) {
|
|
103
|
+
this.child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
|
|
104
|
+
}
|
|
105
|
+
failAll(err) {
|
|
106
|
+
for (const p of this.pending.values())
|
|
107
|
+
p.reject(err);
|
|
108
|
+
this.pending.clear();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/server-handle.ts
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { McpStdioClient } from "./mcp-client.js";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
export class TopodbServer {
|
|
6
|
+
env;
|
|
7
|
+
client;
|
|
8
|
+
toolCache;
|
|
9
|
+
constructor(env = process.env) {
|
|
10
|
+
this.env = env;
|
|
11
|
+
}
|
|
12
|
+
static resolveLauncher() {
|
|
13
|
+
return require.resolve("@topodb/topodb-mcp/bin/topodb-mcp.js");
|
|
14
|
+
}
|
|
15
|
+
args() {
|
|
16
|
+
const db = this.env.TOPODB_DB || ".topodb/memory.redb";
|
|
17
|
+
const scope = this.env.TOPODB_SCOPE || "shared";
|
|
18
|
+
return [TopodbServer.resolveLauncher(), "--db", db, "--scope", scope];
|
|
19
|
+
}
|
|
20
|
+
async ensure() {
|
|
21
|
+
if (this.client?.running)
|
|
22
|
+
return this.client;
|
|
23
|
+
this.client = new McpStdioClient(this.args());
|
|
24
|
+
await this.client.start();
|
|
25
|
+
return this.client;
|
|
26
|
+
}
|
|
27
|
+
async list() {
|
|
28
|
+
const c = await this.ensure();
|
|
29
|
+
if (!this.toolCache)
|
|
30
|
+
this.toolCache = await c.listTools();
|
|
31
|
+
return this.toolCache;
|
|
32
|
+
}
|
|
33
|
+
async call(tool, args) {
|
|
34
|
+
const c = await this.ensure();
|
|
35
|
+
return c.callTool(tool, args);
|
|
36
|
+
}
|
|
37
|
+
shutdown() {
|
|
38
|
+
this.client?.stop();
|
|
39
|
+
this.client = undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@topodb/pi",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Pi (pi.dev) extension: topodb agent memory via one install.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT OR Apache-2.0",
|
|
7
|
+
"repository": { "type": "git", "url": "https://github.com/TopoDB/TopoDB" },
|
|
8
|
+
"keywords": ["pi-package", "pi", "topodb", "memory", "mcp"],
|
|
9
|
+
"pi": { "extensions": ["./dist/extension.js"] },
|
|
10
|
+
"files": ["dist/", "README.md"],
|
|
11
|
+
"engines": { "node": ">=22.19.0" },
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"test": "node --import tsx --test \"test/*.test.ts\"",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@topodb/topodb-mcp": "0.0.3",
|
|
19
|
+
"typebox": "1.1.38"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "^5.6.0",
|
|
23
|
+
"tsx": "^4.19.0",
|
|
24
|
+
"@earendil-works/pi-coding-agent": "0.80.6"
|
|
25
|
+
}
|
|
26
|
+
}
|