@rohirik/pi-ltm 2.12.0 → 2.12.2

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 CHANGED
@@ -1,9 +1,11 @@
1
- import{spawn as w,execSync as b}from"node:child_process";import{createRequire as y}from"node:module";import{resolve as x,dirname as T,join as p}from"node:path";import{homedir as v}from"node:os";import{existsSync as u,readdirSync as k}from"node:fs";var m="LTM_BRIDGE_DEPTH";function P(){let c=[process.env.BUN_INSTALL?p(process.env.BUN_INSTALL,"bin","bun"):null,p(v(),".bun","bin","bun"),"/opt/homebrew/bin/bun","/usr/local/bin/bun","/usr/bin/bun"].filter(t=>t!==null&&u(t));if(c.length>0)return c[0];for(let t of["which bun","command -v bun"])try{let e=b(t,{encoding:"utf-8",stdio:"pipe"}).trim();if(e&&u(e)&&!/\bpi\b/.test(e))return e}catch{}return null}function E(){let c=p(v(),".claude","plugins","cache","ltm","ltm");if(u(c))try{let t=k(c).filter(e=>/^\d/.test(e)).sort().reverse();for(let e of t){let n=p(c,e,"src","mcp-server.ts");if(u(n))return n}}catch{}try{let e=y(import.meta.url).resolve("@rohirik/openltm-core/package.json"),n=x(T(e),"src","mcp-server.ts");if(u(n))return n}catch{}return null}var f=class{constructor(t,e){this.runtime=t;this.script=e}child=null;pending=new Map;buffer="";nextId=1;exited=!1;start(){if(this.child)return;let t=parseInt(process.env[m]??"0",10),e={...process.env,[m]:String(t+1)};this.child=w(this.runtime,[this.script],{stdio:["pipe","pipe","pipe"],env:e}),this.child.stdout?.on("data",n=>{this.buffer+=n.toString("utf-8");let r=this.buffer.split(`
2
- `);this.buffer=r.pop()??"";for(let o of r){let l=o.trim();if(l)try{let i=JSON.parse(l);if(i.id!==void 0){let s=this.pending.get(i.id);s&&(this.pending.delete(i.id),i.error?s.reject(new Error(i.error.message)):s.resolve(i.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(t,e,n){let r=JSON.stringify({jsonrpc:"2.0",id:n,method:t,params:e})+`
3
- `;this.child?.stdin?.write(r)}request(t,e,n=3e4){let r=this.nextId++;return new Promise((o,l)=>{let i=setTimeout(()=>{this.pending.delete(r),l(new Error(`LTM MCP timeout: ${t}`))},n);this.pending.set(r,{resolve:s=>{clearTimeout(i),o(s)},reject:s=>{clearTimeout(i),l(s)}}),this.send(t,e,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(t,e){let n=await this.request("tools/call",{name:t,arguments:e},6e4),r=(n?.content??[]).filter(o=>o?.type==="text"&&o.text).map(o=>o.text).join(`
4
- `);if(n?.isError)throw new Error(r||`${t} returned an error`);return r}};function S(c){let t=c;if(parseInt(process.env[m]??"0",10)>0)return;let e=P(),n=E();if(!e||!n)return;let r=new f(e,n);r.start();let o=(async()=>{await r.initialize();let l=await r.listTools();for(let i of l){let s=i.name;t.registerTool({name:s,label:s,description:i.description??"",parameters:i.inputSchema??{type:"object",properties:{}},async execute(g,a){return{content:[{type:"text",text:await r.callTool(s,a??{})}],details:{}}}})}})().catch(()=>{});t.on("before_agent_start",async l=>{await o;let i=l;try{let g=String(i?.cwd??process.cwd()).replace(/\/$/,"").split("/").pop()??"",a=await r.callTool("ltm_recall",{project:g,limit:8,sort_by:"relevance"});if(!a||a==="[]")return;let d=`## Prior Knowledge (LTM)
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
- ${a}
7
- `,h=String(i?.systemPrompt??"");return{systemPrompt:h?`${h}
8
+ ${m}
9
+ `;if(!f.trim())return;let w=String(i?.systemPrompt??"");return{systemPrompt:w?`${w}
8
10
 
9
- ${d}`:d}}catch{}})}export{S as default};
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.0",
3
+ "version": "2.12.2",
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.0"
41
+ "@rohirik/openltm-core": "^2.12.2"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "bun-types": ">=1.0.0"
package/src/hooks.ts CHANGED
@@ -1,16 +1,9 @@
1
- import { recall, learn } from "@rohirik/openltm-core";
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.replace(/\/$/, "").split("/").pop() ?? "";
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 memories = await recall({ project, limit: MAX_CONTEXT_MEMORIES, sort_by: "relevance" });
23
- if (memories.length === 0) return;
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 s = join(cacheBase, v, "src", "mcp-server.ts");
56
- if (existsSync(s)) return s;
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 (if mcp-server is added in a future version)
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", "mcp-server.ts");
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 script = findMcpServer();
202
- if (!bun || !script) return; // degrade gracefully — no bun or server found
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 text = await client.callTool("ltm_recall", { project, limit: 8, sort_by: "relevance" });
235
- if (!text || text === "[]") return;
236
- const block = `## Prior Knowledge (LTM)\n\n${text}\n`;
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 {