@opticlm/connector 2.4.0 → 2.5.0

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 CHANGED
@@ -340,24 +340,31 @@ install(server, definitionProvider, {
340
340
 
341
341
  When the MCP server runs in a separate process from the IDE plugin (e.g., spawned via stdio transport), the Pipe IPC layer lets the two communicate over a named pipe.
342
342
 
343
- **IDE plugin side** — expose providers:
343
+ **IDE plugin side** — expose providers via a factory that receives client context:
344
344
 
345
345
  ```typescript
346
346
  import { servePipe } from '@opticlm/connector/pipe'
347
347
 
348
348
  const server = await servePipe({
349
349
  pipeName: 'my-ide-lsp',
350
- fileAccess: { /* ... */ },
351
- definition: { /* ... */ },
352
- // ...
353
- // Add only the providers your IDE supports
350
+ createProviders(context) {
351
+ // context is whatever the client sent at connect time
352
+ const { workspacePath } = context as { workspacePath: string }
353
+ return {
354
+ fileAccess: new MyFileAccess(workspacePath),
355
+ definition: new MyDefinitionProvider(workspacePath),
356
+ // Add only the providers your IDE supports
357
+ }
358
+ },
354
359
  })
355
360
  // server.pipePath — the resolved pipe path
356
361
  // server.connectionCount — number of connected clients
357
362
  // await server.close() — shut down
358
363
  ```
359
364
 
360
- **MCP server side** connect and install proxy providers:
365
+ The factory is called once per incoming connection, so providers can be tailored to each client. It may return a plain object or a `Promise` for async initialization.
366
+
367
+ **MCP server side** — connect with context and install proxy providers:
361
368
 
362
369
  ```typescript
363
370
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
@@ -367,6 +374,7 @@ import { install } from '@opticlm/connector/mcp'
367
374
  const conn = await connectPipe({
368
375
  pipeName: 'my-ide-lsp',
369
376
  connectTimeout: 5000, // optional, default 5000ms
377
+ context: { workspacePath: '/path/to/project' }, // sent to createProviders
370
378
  })
371
379
 
372
380
  // conn exposes proxy providers as named fields:
@@ -385,7 +393,29 @@ if (conn.definition && conn.fileAccess)
385
393
  conn.disconnect()
386
394
  ```
387
395
 
388
- The handshake automatically discovers which providers the server exposes and builds typed proxies. Change notifications are forwarded as push notifications to all connected clients. Multiple clients can connect to the same pipe simultaneously.
396
+ The handshake automatically discovers which providers the server exposes and builds typed proxies. Multiple clients can connect to the same pipe simultaneously, each with their own provider instances.
397
+
398
+ **Broadcasting to multiple clients** — since each connection gets its own providers, use a shared subscriber set for push notifications that should reach all clients:
399
+
400
+ ```typescript
401
+ const diagnosticsSubscribers = new Set<(uri: string) => void>()
402
+ yourIDE.onDiagnosticsChanged((uri) => {
403
+ for (const cb of diagnosticsSubscribers) cb(uri)
404
+ })
405
+
406
+ await servePipe({
407
+ pipeName: 'my-ide-lsp',
408
+ createProviders(context) {
409
+ return {
410
+ fileAccess: { /* ... */ },
411
+ diagnostics: {
412
+ provideDiagnostics: (uri) => yourIDE.getDiagnostics(uri),
413
+ onDiagnosticsChanged: (cb) => diagnosticsSubscribers.add(cb),
414
+ },
415
+ }
416
+ },
417
+ })
418
+ ```
389
419
 
390
420
  ## LSP Client (Built-in)
391
421
 
@@ -3,6 +3,7 @@ import { F as FileAccessProvider, E as EditProvider, D as DefinitionProvider, R
3
3
  interface connectPipeOptions {
4
4
  pipeName: string;
5
5
  connectTimeout?: number;
6
+ context?: unknown;
6
7
  }
7
8
  interface PipeConnection {
8
9
  readonly fileAccess?: FileAccessProvider;
@@ -20,8 +21,7 @@ interface PipeConnection {
20
21
  }
21
22
  declare function connectPipe(options: connectPipeOptions): Promise<PipeConnection>;
22
23
 
23
- interface servePipeOptions {
24
- pipeName: string;
24
+ interface ProviderSet {
25
25
  fileAccess: FileAccessProvider;
26
26
  edit?: EditProvider;
27
27
  definition?: DefinitionProvider;
@@ -33,11 +33,15 @@ interface servePipeOptions {
33
33
  graph?: GraphProvider;
34
34
  frontmatter?: FrontmatterProvider;
35
35
  }
36
+ interface ServePipeOptions {
37
+ pipeName: string;
38
+ createProviders: (context: unknown) => ProviderSet | Promise<ProviderSet>;
39
+ }
36
40
  interface PipeServer {
37
41
  readonly pipePath: string;
38
42
  readonly connectionCount: number;
39
43
  close(): Promise<void>;
40
44
  }
41
- declare function servePipe(options: servePipeOptions): Promise<PipeServer>;
45
+ declare function servePipe(options: ServePipeOptions): Promise<PipeServer>;
42
46
 
43
- export { type PipeConnection, type PipeServer, connectPipe, type connectPipeOptions, servePipe, type servePipeOptions };
47
+ export { type PipeConnection, type PipeServer, type ProviderSet, connectPipe, type connectPipeOptions, servePipe, type ServePipeOptions as servePipeOptions };
@@ -1,6 +1,6 @@
1
- import {connect,createServer}from'net';import {unlinkSync}from'fs';var v=class{socket;requestHandler;notificationHandler;nextId=1;pending=new Map;buffer="";destroyed=false;constructor(e,r){this.socket=e,this.requestHandler=r?.onRequest,this.notificationHandler=r?.onNotification,e.on("data",n=>this.handleData(n)),e.on("close",()=>this.rejectAll(new Error("Connection closed"))),e.on("error",n=>this.rejectAll(n));}sendRequest(e,r){return this.destroyed?Promise.reject(new Error("Transport destroyed")):new Promise((n,f)=>{let t=this.nextId++;this.pending.set(t,{resolve:n,reject:f});let c={type:"request",id:t,method:e,params:r};this.socket.write(`${JSON.stringify(c)}
2
- `);})}sendNotification(e,r){if(this.destroyed)return;let n={type:"notification",method:e,params:r};this.socket.write(`${JSON.stringify(n)}
1
+ import {connect,createServer}from'net';import {unlinkSync}from'fs';var y=class{socket;requestHandler;notificationHandler;nextId=1;pending=new Map;buffer="";destroyed=false;constructor(e,r){this.socket=e,this.requestHandler=r?.onRequest,this.notificationHandler=r?.onNotification,e.on("data",i=>this.handleData(i)),e.on("close",()=>this.rejectAll(new Error("Connection closed"))),e.on("error",i=>this.rejectAll(i));}sendRequest(e,r){return this.destroyed?Promise.reject(new Error("Transport destroyed")):new Promise((i,a)=>{let s=this.nextId++;this.pending.set(s,{resolve:i,reject:a});let t={type:"request",id:s,method:e,params:r};this.socket.write(`${JSON.stringify(t)}
2
+ `);})}sendNotification(e,r){if(this.destroyed)return;let i={type:"notification",method:e,params:r};this.socket.write(`${JSON.stringify(i)}
3
3
  `);}destroy(){this.destroyed||(this.destroyed=true,this.rejectAll(new Error("Transport destroyed")),this.socket.destroy());}handleData(e){this.buffer+=e.toString();let r=this.buffer.split(`
4
- `);this.buffer=r.pop()??"";for(let n of r){let f=n.replace(/\r$/,"");if(f)try{let t=JSON.parse(f);this.dispatch(t);}catch{}}}dispatch(e){switch(e.type){case "response":{let r=this.pending.get(e.id);r&&(this.pending.delete(e.id),e.error?r.reject(new Error(e.error.message)):r.resolve(e.result));break}case "request":{this.requestHandler&&this.requestHandler(e.method,e.params).then(r=>{if(this.destroyed)return;let n={type:"response",id:e.id,result:r};this.socket.write(`${JSON.stringify(n)}
5
- `);}).catch(r=>{if(this.destroyed)return;let n={type:"response",id:e.id,error:{message:r instanceof Error?r.message:String(r)}};this.socket.write(`${JSON.stringify(n)}
6
- `);});break}case "notification":{this.notificationHandler?.(e.method,e.params);break}}}rejectAll(e){for(let[,r]of this.pending)r.reject(e);this.pending.clear();}};function w(s){return process.platform==="win32"?`\\\\.\\pipe\\${s}`:`/tmp/${s}.sock`}var k=[{providerKey:"fileAccess",methods:["readFile","readDirectory"]},{providerKey:"edit",methods:["applyEdits"]},{providerKey:"definition",methods:["provideDefinition"]},{providerKey:"references",methods:["provideReferences"]},{providerKey:"hierarchy",methods:["provideCallHierarchy"]},{providerKey:"diagnostics",methods:["provideDiagnostics","getWorkspaceDiagnostics"]},{providerKey:"outline",methods:["provideDocumentSymbols"]},{providerKey:"globalFind",methods:["globalFind"]},{providerKey:"graph",methods:["getLinkStructure","resolveOutlinks","resolveBacklinks","addLink"]},{providerKey:"frontmatter",methods:["getFrontmatterStructure","getFrontmatter","setFrontmatter"]}];function E(s){let{pipeName:e,connectTimeout:r=5e3}=s,n=w(e);return new Promise((f,t)=>{let c=connect(n),i=false,d=setTimeout(()=>{i||(i=true,c.destroy(),t(new Error(`Connection timeout after ${r}ms`)));},r);c.on("error",o=>{i||(i=true,clearTimeout(d),t(o));}),c.on("connect",()=>{if(clearTimeout(d),i)return;let o,l,a=new v(c,{onNotification:(h,P)=>{h==="onDiagnosticsChanged"&&o&&o(P[0]),h==="onFileChanged"&&l&&l(P[0]);}});a.sendRequest("_handshake",[]).then(h=>{if(i)return;let m=h.methods,p={};for(let{providerKey:g,methods:R}of k){let F=R.filter(u=>m.includes(`${g}.${u}`));if(F.length===0)continue;let y={};for(let u of F)y[u]=(...D)=>a.sendRequest(`${g}.${u}`,D);g==="diagnostics"&&m.includes("onDiagnosticsChanged")&&(y.onDiagnosticsChanged=u=>{o=u;}),g==="fileAccess"&&m.includes("onFileChanged")&&(y.onFileChanged=u=>{l=u;}),p[g]=y;}i=true,f({fileAccess:p.fileAccess,edit:p.edit,definition:p.definition,references:p.references,hierarchy:p.hierarchy,diagnostics:p.diagnostics,outline:p.outline,globalFind:p.globalFind,graph:p.graph,frontmatter:p.frontmatter,availableMethods:m,disconnect(){a.destroy();}});}).catch(h=>{i||(i=true,a.destroy(),t(h instanceof Error?h:new Error(String(h))));});});})}function S(s){let{pipeName:e}=s,r=w(e),n=new Map;for(let{providerKey:i,methods:d}of k){let o=s[i];if(o)for(let l of d){let a=o[l];if(typeof a=="function"){let h=`${i}.${l}`;n.set(h,(...P)=>a.apply(o,P));}}}let f=[...n.keys()];s.diagnostics?.onDiagnosticsChanged&&f.push("onDiagnosticsChanged"),s.fileAccess.onFileChanged&&f.push("onFileChanged");let t=new Set,c=createServer(i=>{let d=new v(i,{onRequest:async(o,l)=>{if(o==="_handshake")return {methods:f};let a=n.get(o);if(!a)throw new Error(`Unknown method: ${o}`);return a(...l)}});t.add(d),i.on("close",()=>t.delete(d));});return s.diagnostics?.onDiagnosticsChanged&&s.diagnostics.onDiagnosticsChanged(i=>{for(let d of t)d.sendNotification("onDiagnosticsChanged",[i]);}),s.fileAccess.onFileChanged&&s.fileAccess.onFileChanged(i=>{for(let d of t)d.sendNotification("onFileChanged",[i]);}),new Promise((i,d)=>{if(process.platform!=="win32")try{unlinkSync(r);}catch{}c.on("error",d),c.listen(r,()=>{c.removeListener("error",d),i({get pipePath(){return r},get connectionCount(){return t.size},async close(){for(let o of t)o.destroy();if(t.clear(),await new Promise((o,l)=>{c.close(a=>{a?l(a):o();});}),process.platform!=="win32")try{unlinkSync(r);}catch{}}});});})}export{E as connectPipe,S as servePipe};
4
+ `);this.buffer=r.pop()??"";for(let i of r){let a=i.replace(/\r$/,"");if(a)try{let s=JSON.parse(a);this.dispatch(s);}catch{}}}dispatch(e){switch(e.type){case "response":{let r=this.pending.get(e.id);r&&(this.pending.delete(e.id),e.error?r.reject(new Error(e.error.message)):r.resolve(e.result));break}case "request":{this.requestHandler&&this.requestHandler(e.method,e.params).then(r=>{if(this.destroyed)return;let i={type:"response",id:e.id,result:r};this.socket.write(`${JSON.stringify(i)}
5
+ `);}).catch(r=>{if(this.destroyed)return;let i={type:"response",id:e.id,error:{message:r instanceof Error?r.message:String(r)}};this.socket.write(`${JSON.stringify(i)}
6
+ `);});break}case "notification":{this.notificationHandler?.(e.method,e.params);break}}}rejectAll(e){for(let[,r]of this.pending)r.reject(e);this.pending.clear();}};function b(f){return process.platform==="win32"?`\\\\.\\pipe\\${f}`:`/tmp/${f}.sock`}var F=[{providerKey:"fileAccess",methods:["readFile","readDirectory"]},{providerKey:"edit",methods:["applyEdits"]},{providerKey:"definition",methods:["provideDefinition"]},{providerKey:"references",methods:["provideReferences"]},{providerKey:"hierarchy",methods:["provideCallHierarchy"]},{providerKey:"diagnostics",methods:["provideDiagnostics","getWorkspaceDiagnostics"]},{providerKey:"outline",methods:["provideDocumentSymbols"]},{providerKey:"globalFind",methods:["globalFind"]},{providerKey:"graph",methods:["getLinkStructure","resolveOutlinks","resolveBacklinks","addLink"]},{providerKey:"frontmatter",methods:["getFrontmatterStructure","getFrontmatter","setFrontmatter"]}];function E(f){let{pipeName:e,connectTimeout:r=5e3}=f,i=b(e);return new Promise((a,s)=>{let t=connect(i),n=false,u=setTimeout(()=>{n||(n=true,t.destroy(),s(new Error(`Connection timeout after ${r}ms`)));},r);t.on("error",c=>{n||(n=true,clearTimeout(u),s(c));}),t.on("connect",()=>{if(clearTimeout(u),n)return;let c,P,g=new y(t,{onNotification:(o,v)=>{o==="onDiagnosticsChanged"&&c&&c(v[0]),o==="onFileChanged"&&P&&P(v[0]);}});g.sendRequest("_handshake",[f.context]).then(o=>{if(n)return;let p=o.methods,d={};for(let{providerKey:l,methods:k}of F){let w=k.filter(h=>p.includes(`${l}.${h}`));if(w.length===0)continue;let m={};for(let h of w)m[h]=(...S)=>g.sendRequest(`${l}.${h}`,S);l==="diagnostics"&&p.includes("onDiagnosticsChanged")&&(m.onDiagnosticsChanged=h=>{c=h;}),l==="fileAccess"&&p.includes("onFileChanged")&&(m.onFileChanged=h=>{P=h;}),d[l]=m;}n=true,a({fileAccess:d.fileAccess,edit:d.edit,definition:d.definition,references:d.references,hierarchy:d.hierarchy,diagnostics:d.diagnostics,outline:d.outline,globalFind:d.globalFind,graph:d.graph,frontmatter:d.frontmatter,availableMethods:p,disconnect(){g.destroy();}});}).catch(o=>{n||(n=true,g.destroy(),s(o instanceof Error?o:new Error(String(o))));});});})}function C(f){let{pipeName:e}=f,r=b(e),i=new Set,a=createServer(s=>{let t=null,n=new y(s,{onRequest:async(u,c)=>{if(u==="_handshake"){let g=c[0],o=await f.createProviders(g);t=new Map;for(let{providerKey:p,methods:d}of F){let l=o[p];if(l)for(let k of d){let w=l[k];typeof w=="function"&&t.set(`${p}.${k}`,(...m)=>w.apply(l,m));}}let v=[...t.keys()];return o.diagnostics?.onDiagnosticsChanged&&(o.diagnostics.onDiagnosticsChanged(p=>{n.sendNotification("onDiagnosticsChanged",[p]);}),v.push("onDiagnosticsChanged")),o.fileAccess.onFileChanged&&(o.fileAccess.onFileChanged(p=>{n.sendNotification("onFileChanged",[p]);}),v.push("onFileChanged")),{methods:v}}if(!t)throw new Error("Handshake not completed");let P=t.get(u);if(!P)throw new Error(`Unknown method: ${u}`);return P(...c)}});i.add(n),s.on("close",()=>i.delete(n));});return new Promise((s,t)=>{if(process.platform!=="win32")try{unlinkSync(r);}catch{}a.on("error",t),a.listen(r,()=>{a.removeListener("error",t),s({get pipePath(){return r},get connectionCount(){return i.size},async close(){for(let n of i)n.destroy();if(i.clear(),await new Promise((n,u)=>{a.close(c=>{c?u(c):n();});}),process.platform!=="win32")try{unlinkSync(r);}catch{}}});});})}export{E as connectPipe,C as servePipe};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opticlm/connector",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Provides an abstract interface that allows LLMs to connect to fact sources such as LSPs, code diagnostics, symbol definitions/references, links, and frontmatter",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",