@rohirik/pi-ltm 2.12.0 → 2.12.3
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/dist/index.js +9 -7
- package/package.json +2 -2
- package/src/hooks.ts +4 -15
- package/src/index.ts +49 -13
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import{spawn as
|
|
2
|
-
`);this.buffer=r.pop()??"";for(let
|
|
3
|
-
`;this.child?.stdin?.write(r)}request(t,
|
|
4
|
-
`);if(n?.isError)throw new Error(r||`${
|
|
1
|
+
import{spawn as y,execSync as x}from"node:child_process";import{createRequire as T}from"node:module";import{resolve as P,dirname as k,join as g}from"node:path";import{homedir as b}from"node:os";import{existsSync as p,readdirSync as S}from"node:fs";var h="LTM_BRIDGE_DEPTH";function E(){let a=[process.env.BUN_INSTALL?g(process.env.BUN_INSTALL,"bin","bun"):null,g(b(),".bun","bin","bun"),"/opt/homebrew/bin/bun","/usr/local/bin/bun","/usr/bin/bun"].filter(e=>e!==null&&p(e));if(a.length>0)return a[0];for(let e of["which bun","command -v bun"])try{let t=x(e,{encoding:"utf-8",stdio:"pipe"}).trim();if(t&&p(t)&&!/\bpi\b/.test(t))return t}catch{}return null}function M(){let a=g(b(),".claude","plugins","cache","ltm","ltm");if(p(a))try{let e=S(a).filter(t=>/^\d/.test(t)).sort().reverse();for(let t of e){let n=g(a,t,"src","mcp-server.ts");if(p(n))return{script:n,args:[]}}}catch{}try{let t=T(import.meta.url).resolve("@rohirik/openltm-core/package.json"),n=P(k(t),"src","cli","bin.ts");if(p(n))return{script:n,args:["mcp-serve"]}}catch{}return null}var v=class{constructor(e,t,n=[]){this.runtime=e;this.script=t;this.args=n}child=null;pending=new Map;buffer="";nextId=1;exited=!1;start(){if(this.child)return;let e=parseInt(process.env[h]??"0",10),t={...process.env,[h]:String(e+1)};this.child=y(this.runtime,[this.script,...this.args],{stdio:["pipe","pipe","pipe"],env:t}),this.child.stdout?.on("data",n=>{this.buffer+=n.toString("utf-8");let r=this.buffer.split(`
|
|
2
|
+
`);this.buffer=r.pop()??"";for(let s of r){let o=s.trim();if(o)try{let c=JSON.parse(o);if(c.id!==void 0){let i=this.pending.get(c.id);i&&(this.pending.delete(c.id),c.error?i.reject(new Error(c.error.message)):i.resolve(c.result))}}catch{}}}),this.child.on("exit",()=>{this.exited=!0;for(let n of this.pending.values())n.reject(new Error("LTM MCP server exited"));this.pending.clear()}),this.child.on("error",()=>{this.exited=!0;for(let n of this.pending.values())n.reject(new Error("LTM MCP server error"));this.pending.clear()})}send(e,t,n){let r=JSON.stringify({jsonrpc:"2.0",id:n,method:e,params:t})+`
|
|
3
|
+
`;this.child?.stdin?.write(r)}request(e,t,n=3e4){let r=this.nextId++;return new Promise((s,o)=>{let c=setTimeout(()=>{this.pending.delete(r),o(new Error(`LTM MCP timeout: ${e}`))},n);this.pending.set(r,{resolve:i=>{clearTimeout(c),s(i)},reject:i=>{clearTimeout(c),o(i)}}),this.send(e,t,r)})}async initialize(){await this.request("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"pi-ltm-bridge",version:"1.0.0"}}),this.send("notifications/initialized")}async listTools(){return(await this.request("tools/list"))?.tools??[]}async callTool(e,t){let n=await this.request("tools/call",{name:e,arguments:t},6e4),r=(n?.content??[]).filter(s=>s?.type==="text"&&s.text).map(s=>s.text).join(`
|
|
4
|
+
`);if(n?.isError)throw new Error(r||`${e} returned an error`);return r}};function j(a,e){try{let t=JSON.parse(e),n=Array.isArray(t.globals)?t.globals:[],r=Array.isArray(t.scoped)?t.scoped:[];if(n.length===0&&r.length===0)return"";let s=["## Prior Knowledge (LTM)",""];if(n.length>0){s.push("Global:");for(let o of n.slice(0,3))s.push(`- [${o.id}] ${String(o.content).replace(/\s+/g," ").trim()}`);s.push("")}if(r.length>0){s.push(`Project (${a}):`);for(let o of r.slice(0,7))s.push(`- [${o.id}] ${String(o.content).replace(/\s+/g," ").trim()}`);s.push("")}return s.join(`
|
|
5
|
+
`).trimEnd()+`
|
|
6
|
+
`}catch{return""}}function _(a){let e=a;if(parseInt(process.env[h]??"0",10)>0)return;let t=E(),n=M();if(!t||!n)return;let r=new v(t,n.script,n.args);r.start();let s=new Set,o=(async()=>{await r.initialize();let c=await r.listTools();for(let i of c){let l=i.name;s.add(l),e.registerTool({name:l,label:l,description:i.description??"",parameters:i.inputSchema??{type:"object",properties:{}},async execute(d,u){return{content:[{type:"text",text:await r.callTool(l,u??{})}],details:{}}}})}})().catch(()=>{});e.on("before_agent_start",async c=>{await o;let i=c;try{let d=String(i?.cwd??process.cwd()).replace(/\/$/,"").split("/").pop()??"",u=s.has("context")?"context":s.has("recall")?"recall":"";if(!u)return;let m=u==="context"?await r.callTool("context",{project:d}):await r.callTool("recall",{project:d,limit:8,sort_by:"relevance"}),f=u==="context"?j(d,m):`## Prior Knowledge (LTM)
|
|
5
7
|
|
|
6
|
-
${
|
|
7
|
-
|
|
8
|
+
${m}
|
|
9
|
+
`;if(!f.trim())return;let w=String(i?.systemPrompt??"");return{systemPrompt:w?`${w}
|
|
8
10
|
|
|
9
|
-
${
|
|
11
|
+
${f}`:f}}catch{}})}export{_ as default};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rohirik/pi-ltm",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.3",
|
|
4
4
|
"description": "LTM (Long-Term Memory) native extension for Pi — share memories with Claude Code and OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@rohirik/openltm-core": "^2.12.
|
|
41
|
+
"@rohirik/openltm-core": "^2.12.3"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"bun-types": ">=1.0.0"
|
package/src/hooks.ts
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildPrefillContext, deriveProjectFromCwd, learn } from "@rohirik/openltm-core";
|
|
2
2
|
|
|
3
3
|
type PiAny = any;
|
|
4
4
|
|
|
5
|
-
const MAX_CONTEXT_MEMORIES = 10;
|
|
6
|
-
|
|
7
|
-
function formatContextBlock(memories: Array<{ id: number; content: string; category: string }>): string {
|
|
8
|
-
const lines = memories.map((m) => `- [${m.id}] (${m.category}) ${m.content}`);
|
|
9
|
-
return "## Prior Knowledge (LTM)\n\n" + lines.join("\n") + "\n";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
5
|
function projectFromCwd(cwd: string): string {
|
|
13
|
-
return cwd
|
|
6
|
+
return deriveProjectFromCwd(cwd);
|
|
14
7
|
}
|
|
15
8
|
|
|
16
9
|
export function registerHooks(pi: PiAny): void {
|
|
@@ -19,12 +12,8 @@ export function registerHooks(pi: PiAny): void {
|
|
|
19
12
|
try {
|
|
20
13
|
const cwd = String(event?.cwd ?? process.cwd());
|
|
21
14
|
const project = projectFromCwd(cwd);
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
const block = formatContextBlock(
|
|
26
|
-
memories.map((m) => ({ id: m.id, content: m.content, category: m.category })),
|
|
27
|
-
);
|
|
15
|
+
const block = buildPrefillContext({ project, maxMemories: 10, maxLines: 18 });
|
|
16
|
+
if (!block) return;
|
|
28
17
|
const existing = String(event?.systemPrompt ?? "");
|
|
29
18
|
const parts = existing ? [existing, block] : [block];
|
|
30
19
|
return { systemPrompt: parts.join("\n\n") };
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ function findBun(): string | null {
|
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function findMcpServer(): string | null {
|
|
45
|
+
function findMcpServer(): { script: string; args: string[] } | null {
|
|
46
46
|
// 1. Plugin cache — newest version first (Claude Code users)
|
|
47
47
|
const cacheBase = join(homedir(), ".claude", "plugins", "cache", "ltm", "ltm");
|
|
48
48
|
if (existsSync(cacheBase)) {
|
|
@@ -52,19 +52,19 @@ function findMcpServer(): string | null {
|
|
|
52
52
|
.sort()
|
|
53
53
|
.reverse();
|
|
54
54
|
for (const v of versions) {
|
|
55
|
-
const
|
|
56
|
-
if (existsSync(
|
|
55
|
+
const script = join(cacheBase, v, "src", "mcp-server.ts");
|
|
56
|
+
if (existsSync(script)) return { script, args: [] };
|
|
57
57
|
}
|
|
58
58
|
} catch {
|
|
59
59
|
// continue to next strategy
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
// 2. openltm-core package
|
|
62
|
+
// 2. openltm-core package — run the packaged CLI entrypoint with mcp-serve
|
|
63
63
|
try {
|
|
64
64
|
const req = createRequire(import.meta.url);
|
|
65
65
|
const pkgJson = req.resolve("@rohirik/openltm-core/package.json");
|
|
66
|
-
const script = resolve(dirname(pkgJson), "src", "
|
|
67
|
-
if (existsSync(script)) return script;
|
|
66
|
+
const script = resolve(dirname(pkgJson), "src", "cli", "bin.ts");
|
|
67
|
+
if (existsSync(script)) return { script, args: ["mcp-serve"] };
|
|
68
68
|
} catch {
|
|
69
69
|
// not available
|
|
70
70
|
}
|
|
@@ -89,6 +89,7 @@ class LtmMcpClient {
|
|
|
89
89
|
constructor(
|
|
90
90
|
private readonly runtime: string,
|
|
91
91
|
private readonly script: string,
|
|
92
|
+
private readonly args: string[] = [],
|
|
92
93
|
) {}
|
|
93
94
|
|
|
94
95
|
start(): void {
|
|
@@ -96,7 +97,7 @@ class LtmMcpClient {
|
|
|
96
97
|
const depth = parseInt(process.env[BRIDGE_DEPTH_ENV] ?? "0", 10);
|
|
97
98
|
const env = { ...process.env, [BRIDGE_DEPTH_ENV]: String(depth + 1) };
|
|
98
99
|
|
|
99
|
-
this.child = spawn(this.runtime, [this.script], { stdio: ["pipe", "pipe", "pipe"], env });
|
|
100
|
+
this.child = spawn(this.runtime, [this.script, ...this.args], { stdio: ["pipe", "pipe", "pipe"], env });
|
|
100
101
|
|
|
101
102
|
this.child.stdout?.on("data", (chunk: Buffer) => {
|
|
102
103
|
this.buffer += chunk.toString("utf-8");
|
|
@@ -184,6 +185,33 @@ class LtmMcpClient {
|
|
|
184
185
|
|
|
185
186
|
// ── Extension entry point ─────────────────────────────────────────────────────
|
|
186
187
|
|
|
188
|
+
function formatContextPayload(project: string, raw: string): string {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(raw) as {
|
|
191
|
+
globals?: Array<{ id: number; content: string }>;
|
|
192
|
+
scoped?: Array<{ id: number; content: string }>;
|
|
193
|
+
};
|
|
194
|
+
const globals = Array.isArray(parsed.globals) ? parsed.globals : [];
|
|
195
|
+
const scoped = Array.isArray(parsed.scoped) ? parsed.scoped : [];
|
|
196
|
+
if (globals.length === 0 && scoped.length === 0) return "";
|
|
197
|
+
|
|
198
|
+
const lines: string[] = ["## Prior Knowledge (LTM)", ""];
|
|
199
|
+
if (globals.length > 0) {
|
|
200
|
+
lines.push("Global:");
|
|
201
|
+
for (const m of globals.slice(0, 3)) lines.push(`- [${m.id}] ${String(m.content).replace(/\s+/g, " ").trim()}`);
|
|
202
|
+
lines.push("");
|
|
203
|
+
}
|
|
204
|
+
if (scoped.length > 0) {
|
|
205
|
+
lines.push(`Project (${project}):`);
|
|
206
|
+
for (const m of scoped.slice(0, 7)) lines.push(`- [${m.id}] ${String(m.content).replace(/\s+/g, " ").trim()}`);
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
209
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
210
|
+
} catch {
|
|
211
|
+
return "";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
187
215
|
export default function ltmExtension(pi: unknown): void {
|
|
188
216
|
const p = pi as {
|
|
189
217
|
registerTool: (def: {
|
|
@@ -198,19 +226,21 @@ export default function ltmExtension(pi: unknown): void {
|
|
|
198
226
|
if (parseInt(process.env[BRIDGE_DEPTH_ENV] ?? "0", 10) > 0) return;
|
|
199
227
|
|
|
200
228
|
const bun = findBun();
|
|
201
|
-
const
|
|
202
|
-
if (!bun || !
|
|
229
|
+
const server = findMcpServer();
|
|
230
|
+
if (!bun || !server) return; // degrade gracefully — no bun or server found
|
|
203
231
|
|
|
204
|
-
const client = new LtmMcpClient(bun, script);
|
|
232
|
+
const client = new LtmMcpClient(bun, server.script, server.args);
|
|
205
233
|
client.start();
|
|
206
234
|
|
|
207
235
|
// Bootstrap runs async — tools are registered once handshake completes.
|
|
208
236
|
// Pi allows registerTool after extension load; before_agent_start awaits ready.
|
|
237
|
+
const toolNames = new Set<string>();
|
|
209
238
|
const ready = (async () => {
|
|
210
239
|
await client.initialize();
|
|
211
240
|
const tools = await client.listTools();
|
|
212
241
|
for (const tool of tools) {
|
|
213
242
|
const name = tool.name;
|
|
243
|
+
toolNames.add(name);
|
|
214
244
|
p.registerTool({
|
|
215
245
|
name,
|
|
216
246
|
label: name,
|
|
@@ -231,9 +261,15 @@ export default function ltmExtension(pi: unknown): void {
|
|
|
231
261
|
try {
|
|
232
262
|
const cwd = String(ev?.cwd ?? process.cwd());
|
|
233
263
|
const project = cwd.replace(/\/$/, "").split("/").pop() ?? "";
|
|
234
|
-
const
|
|
235
|
-
if (!
|
|
236
|
-
const
|
|
264
|
+
const toolName = toolNames.has("context") ? "context" : (toolNames.has("recall") ? "recall" : "");
|
|
265
|
+
if (!toolName) return;
|
|
266
|
+
const text = toolName === "context"
|
|
267
|
+
? await client.callTool("context", { project })
|
|
268
|
+
: await client.callTool("recall", { project, limit: 8, sort_by: "relevance" });
|
|
269
|
+
const block = toolName === "context"
|
|
270
|
+
? formatContextPayload(project, text)
|
|
271
|
+
: `## Prior Knowledge (LTM)\n\n${text}\n`;
|
|
272
|
+
if (!block.trim()) return;
|
|
237
273
|
const existing = String(ev?.systemPrompt ?? "");
|
|
238
274
|
return { systemPrompt: existing ? `${existing}\n\n${block}` : block };
|
|
239
275
|
} catch {
|