@nimblebrain/synapse 0.1.1 → 0.1.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.
@@ -1,48 +1,280 @@
1
1
  'use strict';
2
2
 
3
+ var child_process = require('child_process');
4
+ var fs = require('fs');
5
+ var path = require('path');
6
+
3
7
  // src/vite/plugin.ts
4
- function synapseVite(options) {
5
- const { appName, platformUrl = "http://localhost:4321", injectBridge = true } = options;
8
+ function synapseVite(options = {}) {
9
+ const enablePreview = options.preview !== false;
10
+ let manifest = null;
11
+ let appName = options.appName ?? "app";
12
+ let serverProcess = null;
13
+ const pendingRequests = /* @__PURE__ */ new Map();
14
+ let serverBuffer = "";
15
+ function loadManifest(root) {
16
+ const manifestPath = options.manifest ? path.resolve(options.manifest) : path.resolve(root, "..", "manifest.json");
17
+ if (!fs.existsSync(manifestPath)) return null;
18
+ try {
19
+ return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ function deriveServerCmd(m, root) {
25
+ if (options.serverCmd) return options.serverCmd;
26
+ const cfg = m.server?.mcp_config;
27
+ if (!cfg?.command) return null;
28
+ const serverDir = path.resolve(root, "..");
29
+ let cmd = cfg.command;
30
+ const args = cfg.args ?? [];
31
+ if (cmd === "python" && fs.existsSync(path.join(serverDir, "pyproject.toml"))) {
32
+ cmd = "uv run python";
33
+ }
34
+ return `cd ${JSON.stringify(serverDir)} && ${cmd} ${args.join(" ")}`;
35
+ }
36
+ function startServer(cmd) {
37
+ serverProcess = child_process.spawn(cmd, {
38
+ shell: true,
39
+ stdio: ["pipe", "pipe", "pipe"]
40
+ });
41
+ serverProcess.stderr?.on("data", (d) => {
42
+ process.stderr.write(` [mcp] ${d}`);
43
+ });
44
+ serverProcess.stdout?.on("data", (d) => {
45
+ serverBuffer += d.toString();
46
+ const lines = serverBuffer.split("\n");
47
+ serverBuffer = lines.pop() ?? "";
48
+ for (const line of lines) {
49
+ if (!line.trim()) continue;
50
+ try {
51
+ const msg = JSON.parse(line);
52
+ if (msg.id && pendingRequests.has(msg.id)) {
53
+ const p = pendingRequests.get(msg.id);
54
+ pendingRequests.delete(msg.id);
55
+ p?.resolve(msg);
56
+ }
57
+ } catch {
58
+ process.stderr.write(` [mcp] ${line}
59
+ `);
60
+ }
61
+ }
62
+ });
63
+ serverProcess.on("exit", (code) => {
64
+ if (code !== null && code !== 0) {
65
+ console.error(` [mcp] Server exited with code ${code}`);
66
+ }
67
+ serverProcess = null;
68
+ });
69
+ sendToServer({
70
+ jsonrpc: "2.0",
71
+ id: "init-1",
72
+ method: "initialize",
73
+ params: {
74
+ protocolVersion: "2024-11-05",
75
+ capabilities: {},
76
+ clientInfo: { name: "synapse-preview", version: "0.1.0" }
77
+ }
78
+ });
79
+ }
80
+ function sendToServer(msg) {
81
+ if (!serverProcess?.stdin?.writable) return;
82
+ serverProcess.stdin.write(`${JSON.stringify(msg)}
83
+ `);
84
+ }
85
+ function callServerTool(name, args) {
86
+ return new Promise((resolve2, reject) => {
87
+ const id = `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
88
+ pendingRequests.set(id, { resolve: resolve2, reject });
89
+ sendToServer({
90
+ jsonrpc: "2.0",
91
+ id,
92
+ method: "tools/call",
93
+ params: { name, arguments: args }
94
+ });
95
+ setTimeout(() => {
96
+ if (pendingRequests.has(id)) {
97
+ pendingRequests.delete(id);
98
+ reject(new Error("Tool call timed out (10s)"));
99
+ }
100
+ }, 1e4);
101
+ });
102
+ }
6
103
  return {
7
104
  name: "synapse",
8
105
  config() {
9
106
  return {
10
107
  define: {
11
- "import.meta.env.SYNAPSE_PLATFORM_URL": JSON.stringify(platformUrl),
12
108
  "import.meta.env.SYNAPSE_APP_NAME": JSON.stringify(appName)
13
109
  },
14
110
  server: {
15
111
  hmr: {
16
- // Required for HMR inside sandboxed iframe
17
112
  protocol: "ws",
18
113
  host: "localhost"
19
114
  }
20
115
  }
21
116
  };
22
117
  },
118
+ configResolved(config) {
119
+ manifest = loadManifest(config.root);
120
+ if (manifest?.name) {
121
+ appName = options.appName ?? manifest.name;
122
+ }
123
+ },
23
124
  configureServer(server) {
24
- server.middlewares.use((_req, res, next) => {
125
+ if (enablePreview && manifest) {
126
+ const cmd = deriveServerCmd(manifest, server.config.root);
127
+ if (cmd) {
128
+ console.log(`
129
+ [synapse] Starting MCP server: ${cmd}
130
+ `);
131
+ startServer(cmd);
132
+ }
133
+ }
134
+ server.middlewares.use((req, res, next) => {
25
135
  res.setHeader("Access-Control-Allow-Origin", "*");
26
136
  res.setHeader("Access-Control-Allow-Methods", "*");
27
137
  res.setHeader("Access-Control-Allow-Headers", "*");
138
+ if (req.url === "/__preview" || req.url === "/__preview/") {
139
+ res.writeHead(200, { "Content-Type": "text/html" });
140
+ res.end(previewHostHtml(appName));
141
+ return;
142
+ }
143
+ if (req.method === "POST" && req.url === "/__mcp") {
144
+ let body = "";
145
+ req.on("data", (chunk) => {
146
+ body += chunk.toString();
147
+ });
148
+ req.on("end", async () => {
149
+ try {
150
+ const msg = JSON.parse(body);
151
+ const result = await callServerTool(msg.params.name, msg.params.arguments || {});
152
+ res.writeHead(200, { "Content-Type": "application/json" });
153
+ res.end(JSON.stringify(result));
154
+ } catch (err) {
155
+ res.writeHead(200, { "Content-Type": "application/json" });
156
+ res.end(
157
+ JSON.stringify({
158
+ jsonrpc: "2.0",
159
+ id: JSON.parse(body).id,
160
+ error: { code: -32e3, message: err.message }
161
+ })
162
+ );
163
+ }
164
+ });
165
+ return;
166
+ }
28
167
  next();
29
168
  });
30
169
  },
31
- transformIndexHtml(html) {
32
- if (!injectBridge) return html;
33
- const bridgeScript = `
34
- <script type="module">
35
- import { createSynapse } from "@nimblebrain/synapse";
36
- window.__synapse = createSynapse({
37
- name: ${JSON.stringify(appName)},
38
- version: "0.0.0-dev",
39
- });
40
- </script>`;
41
- return html.replace("</head>", `${bridgeScript}
42
- </head>`);
170
+ buildEnd() {
171
+ if (serverProcess) {
172
+ serverProcess.kill("SIGTERM");
173
+ serverProcess = null;
174
+ }
43
175
  }
44
176
  };
45
177
  }
178
+ function previewHostHtml(appName) {
179
+ return `<!DOCTYPE html>
180
+ <html>
181
+ <head>
182
+ <meta charset="utf-8" />
183
+ <title>${appName} \u2014 Synapse Preview</title>
184
+ <style>
185
+ * { margin: 0; padding: 0; box-sizing: border-box; }
186
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #0f172a; color: #e2e8f0; }
187
+ header { padding: 10px 16px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; align-items: center; gap: 10px; font-size: 13px; }
188
+ header .dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }
189
+ header .name { font-weight: 600; }
190
+ header .spacer { flex: 1; }
191
+ header button { background: #334155; border: none; color: #e2e8f0; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; }
192
+ header .url { color: #64748b; font-size: 11px; font-family: monospace; }
193
+ iframe { width: 100%; height: calc(100vh - 41px); border: none; }
194
+ </style>
195
+ </head>
196
+ <body>
197
+ <header>
198
+ <span class="dot"></span>
199
+ <span class="name">${appName}</span>
200
+ <span class="spacer"></span>
201
+ <button id="toggle">Toggle Theme</button>
202
+ <span class="url">Synapse Preview</span>
203
+ </header>
204
+ <iframe id="app" src="/"></iframe>
205
+
206
+ <script>
207
+ var iframe = document.getElementById("app");
208
+ var dark = true;
209
+
210
+ function getTokens(d) {
211
+ return d ? {
212
+ "--nb-background":"#0f172a","--nb-foreground":"#e2e8f0",
213
+ "--nb-card":"#1e293b","--nb-card-foreground":"#e2e8f0",
214
+ "--nb-primary":"#6366f1","--nb-primary-foreground":"#fff",
215
+ "--nb-muted-foreground":"#94a3b8","--nb-border":"#334155",
216
+ "--nb-ring":"#6366f1","--nb-destructive":"#ef4444",
217
+ "--nb-radius":"0.5rem","--nb-font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
218
+ } : {
219
+ "--nb-background":"#ffffff","--nb-foreground":"#0f172a",
220
+ "--nb-card":"#f8fafc","--nb-card-foreground":"#0f172a",
221
+ "--nb-primary":"#6366f1","--nb-primary-foreground":"#fff",
222
+ "--nb-muted-foreground":"#64748b","--nb-border":"#e2e8f0",
223
+ "--nb-ring":"#6366f1","--nb-destructive":"#ef4444",
224
+ "--nb-radius":"0.5rem","--nb-font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
225
+ };
226
+ }
227
+
228
+ function post(msg) { iframe.contentWindow.postMessage(msg, "*"); }
229
+
230
+ window.addEventListener("message", async function(e) {
231
+ if (e.source !== iframe.contentWindow) return;
232
+ var msg = e.data;
233
+ if (!msg || typeof msg !== "object") return;
234
+
235
+ // ext-apps handshake
236
+ if (msg.method === "ui/initialize" && msg.id) {
237
+ post({ jsonrpc:"2.0", id:msg.id, result: {
238
+ protocolVersion:"2026-01-26",
239
+ serverInfo:{name:"nimblebrain",version:"preview"},
240
+ capabilities:{openLinks:{},serverTools:{}},
241
+ hostContext:{theme:dark?"dark":"light",primaryColor:"#6366f1",tokens:getTokens(dark)}
242
+ }});
243
+ return;
244
+ }
245
+ if (msg.method === "ui/notifications/initialized") return;
246
+
247
+ // Tool calls \u2014 proxy via Vite middleware
248
+ if (msg.method === "tools/call" && msg.id) {
249
+ try {
250
+ var r = await fetch("/__mcp", {
251
+ method:"POST", headers:{"Content-Type":"application/json"},
252
+ body: JSON.stringify({jsonrpc:"2.0",id:msg.id,method:"tools/call",params:{name:msg.params.name,arguments:msg.params.arguments||{}}})
253
+ });
254
+ post(await r.json());
255
+ } catch(err) {
256
+ post({jsonrpc:"2.0",id:msg.id,error:{code:-32000,message:err.message}});
257
+ }
258
+ return;
259
+ }
260
+
261
+ // Log other messages
262
+ if (msg.method === "ui/chat") console.log("[chat]", msg.params?.message);
263
+ else if (msg.method === "ui/action") console.log("[action]", msg.params?.action, msg.params);
264
+ else if (msg.method === "ui/stateChanged") { console.log("[state]", msg.params?.state); post({jsonrpc:"2.0",method:"ui/stateAcknowledged",params:{truncated:false}}); }
265
+ else if (msg.method === "ui/keydown") { /* ignore */ }
266
+ else if (msg.method) console.log("[bridge]", msg.method, msg);
267
+ });
268
+
269
+ document.getElementById("toggle").onclick = function() {
270
+ dark = !dark;
271
+ document.body.style.background = dark ? "#0f172a" : "#f1f5f9";
272
+ post({jsonrpc:"2.0",method:"ui/themeChanged",params:{mode:dark?"dark":"light",tokens:getTokens(dark)}});
273
+ };
274
+ </script>
275
+ </body>
276
+ </html>`;
277
+ }
46
278
 
47
279
  exports.synapseVite = synapseVite;
48
280
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/vite/plugin.ts"],"names":[],"mappings":";;;AAmBO,SAAS,YAAY,OAAA,EAA2C;AACrE,EAAA,MAAM,EAAE,OAAA,EAAS,WAAA,GAAc,uBAAA,EAAyB,YAAA,GAAe,MAAK,GAAI,OAAA;AAEhF,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,SAAA;AAAA,IAEN,MAAA,GAAS;AACP,MAAA,OAAO;AAAA,QACL,MAAA,EAAQ;AAAA,UACN,sCAAA,EAAwC,IAAA,CAAK,SAAA,CAAU,WAAW,CAAA;AAAA,UAClE,kCAAA,EAAoC,IAAA,CAAK,SAAA,CAAU,OAAO;AAAA,SAC5D;AAAA,QACA,MAAA,EAAQ;AAAA,UACN,GAAA,EAAK;AAAA;AAAA,YAEH,QAAA,EAAU,IAAA;AAAA,YACV,IAAA,EAAM;AAAA;AACR;AACF,OACF;AAAA,IACF,CAAA;AAAA,IAEA,gBAAgB,MAAA,EAAuB;AAErC,MAAA,MAAA,CAAO,WAAA,CAAY,GAAA,CAAI,CAAC,IAAA,EAAM,KAAK,IAAA,KAAS;AAC1C,QAAA,GAAA,CAAI,SAAA,CAAU,+BAA+B,GAAG,CAAA;AAChD,QAAA,GAAA,CAAI,SAAA,CAAU,gCAAgC,GAAG,CAAA;AACjD,QAAA,GAAA,CAAI,SAAA,CAAU,gCAAgC,GAAG,CAAA;AACjD,QAAA,IAAA,EAAK;AAAA,MACP,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IAEA,mBAAmB,IAAA,EAAc;AAC/B,MAAA,IAAI,CAAC,cAAc,OAAO,IAAA;AAE1B,MAAA,MAAM,YAAA,GAAe;AAAA;AAAA;AAAA;AAAA,UAAA,EAIf,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA;AAAA;AAAA,SAAA,CAAA;AAM7B,MAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,SAAA,EAAW,CAAA,EAAG,YAAY;AAAA,OAAA,CAAW,CAAA;AAAA,IAC3D;AAAA,GACF;AACF","file":"index.cjs","sourcesContent":["import type { Plugin, ViteDevServer } from \"vite\";\n\nexport interface SynapseVitePluginOptions {\n /** App name (must match manifest) */\n appName: string;\n /** Platform API URL (default: http://localhost:4321) */\n platformUrl?: string;\n /** Auto-inject bridge runtime into HTML entry (default: true) */\n injectBridge?: boolean;\n}\n\n/**\n * Vite plugin for Synapse app development.\n *\n * - Configures CORS for cross-origin iframe communication\n * - Injects ext-apps bridge runtime if `injectBridge` is true\n * - Sets up HMR WebSocket to work inside iframe sandbox\n * - Exposes platform URL as `import.meta.env.SYNAPSE_PLATFORM_URL`\n */\nexport function synapseVite(options: SynapseVitePluginOptions): Plugin {\n const { appName, platformUrl = \"http://localhost:4321\", injectBridge = true } = options;\n\n return {\n name: \"synapse\",\n\n config() {\n return {\n define: {\n \"import.meta.env.SYNAPSE_PLATFORM_URL\": JSON.stringify(platformUrl),\n \"import.meta.env.SYNAPSE_APP_NAME\": JSON.stringify(appName),\n },\n server: {\n hmr: {\n // Required for HMR inside sandboxed iframe\n protocol: \"ws\",\n host: \"localhost\",\n },\n },\n };\n },\n\n configureServer(server: ViteDevServer) {\n // Add CORS headers for cross-origin iframe communication\n server.middlewares.use((_req, res, next) => {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"*\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n next();\n });\n },\n\n transformIndexHtml(html: string) {\n if (!injectBridge) return html;\n\n const bridgeScript = `\n<script type=\"module\">\n import { createSynapse } from \"@nimblebrain/synapse\";\n window.__synapse = createSynapse({\n name: ${JSON.stringify(appName)},\n version: \"0.0.0-dev\",\n });\n</script>`;\n\n // Inject before closing </head> tag\n return html.replace(\"</head>\", `${bridgeScript}\\n</head>`);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/vite/plugin.ts"],"names":["resolve","existsSync","readFileSync","join","spawn"],"mappings":";;;;;;;AAiDO,SAAS,WAAA,CAAY,OAAA,GAAoC,EAAC,EAAW;AAC1E,EAAA,MAAM,aAAA,GAAgB,QAAQ,OAAA,KAAY,KAAA;AAC1C,EAAA,IAAI,QAAA,GAA4B,IAAA;AAChC,EAAA,IAAI,OAAA,GAAU,QAAQ,OAAA,IAAW,KAAA;AACjC,EAAA,IAAI,aAAA,GAAqC,IAAA;AACzC,EAAA,MAAM,eAAA,uBAAsB,GAAA,EAG1B;AACF,EAAA,IAAI,YAAA,GAAe,EAAA;AAEnB,EAAA,SAAS,aAAa,IAAA,EAA+B;AACnD,IAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,QAAA,GACzBA,YAAA,CAAQ,OAAA,CAAQ,QAAQ,CAAA,GACxBA,YAAA,CAAQ,IAAA,EAAM,IAAA,EAAM,eAAe,CAAA;AAEvC,IAAA,IAAI,CAACC,aAAA,CAAW,YAAY,CAAA,EAAG,OAAO,IAAA;AACtC,IAAA,IAAI;AACF,MAAA,OAAO,IAAA,CAAK,KAAA,CAAMC,eAAA,CAAa,YAAA,EAAc,OAAO,CAAC,CAAA;AAAA,IACvD,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,SAAS,eAAA,CAAgB,GAAa,IAAA,EAA6B;AACjE,IAAA,IAAI,OAAA,CAAQ,SAAA,EAAW,OAAO,OAAA,CAAQ,SAAA;AACtC,IAAA,MAAM,GAAA,GAAM,EAAE,MAAA,EAAQ,UAAA;AACtB,IAAA,IAAI,CAAC,GAAA,EAAK,OAAA,EAAS,OAAO,IAAA;AAE1B,IAAA,MAAM,SAAA,GAAYF,YAAA,CAAQ,IAAA,EAAM,IAAI,CAAA;AACpC,IAAA,IAAI,MAAM,GAAA,CAAI,OAAA;AACd,IAAA,MAAM,IAAA,GAAO,GAAA,CAAI,IAAA,IAAQ,EAAC;AAG1B,IAAA,IAAI,QAAQ,QAAA,IAAYC,aAAA,CAAWE,UAAK,SAAA,EAAW,gBAAgB,CAAC,CAAA,EAAG;AACrE,MAAA,GAAA,GAAM,eAAA;AAAA,IACR;AAEA,IAAA,OAAO,CAAA,GAAA,EAAM,IAAA,CAAK,SAAA,CAAU,SAAS,CAAC,CAAA,IAAA,EAAO,GAAG,CAAA,CAAA,EAAI,IAAA,CAAK,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA;AAAA,EACpE;AAEA,EAAA,SAAS,YAAY,GAAA,EAAmB;AACtC,IAAA,aAAA,GAAgBC,oBAAM,GAAA,EAAK;AAAA,MACzB,KAAA,EAAO,IAAA;AAAA,MACP,KAAA,EAAO,CAAC,MAAA,EAAQ,MAAA,EAAQ,MAAM;AAAA,KAC/B,CAAA;AAED,IAAA,aAAA,CAAc,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,CAAA,KAAc;AAC9C,MAAA,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,QAAA,EAAW,CAAC,CAAA,CAAE,CAAA;AAAA,IACrC,CAAC,CAAA;AAED,IAAA,aAAA,CAAc,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,CAAA,KAAc;AAC9C,MAAA,YAAA,IAAgB,EAAE,QAAA,EAAS;AAE3B,MAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,KAAA,CAAM,IAAI,CAAA;AACrC,MAAA,YAAA,GAAe,KAAA,CAAM,KAAI,IAAK,EAAA;AAC9B,MAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,QAAA,IAAI,CAAC,IAAA,CAAK,IAAA,EAAK,EAAG;AAClB,QAAA,IAAI;AACF,UAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC3B,UAAA,IAAI,IAAI,EAAA,IAAM,eAAA,CAAgB,GAAA,CAAI,GAAA,CAAI,EAAE,CAAA,EAAG;AACzC,YAAA,MAAM,CAAA,GAAI,eAAA,CAAgB,GAAA,CAAI,GAAA,CAAI,EAAE,CAAA;AACpC,YAAA,eAAA,CAAgB,MAAA,CAAO,IAAI,EAAE,CAAA;AAC7B,YAAA,CAAA,EAAG,QAAQ,GAAG,CAAA;AAAA,UAChB;AAAA,QACF,CAAA,CAAA,MAAQ;AAEN,UAAA,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,QAAA,EAAW,IAAI;AAAA,CAAI,CAAA;AAAA,QAC1C;AAAA,MACF;AAAA,IACF,CAAC,CAAA;AAED,IAAA,aAAA,CAAc,EAAA,CAAG,MAAA,EAAQ,CAAC,IAAA,KAAS;AACjC,MAAA,IAAI,IAAA,KAAS,IAAA,IAAQ,IAAA,KAAS,CAAA,EAAG;AAC/B,QAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,gCAAA,EAAmC,IAAI,CAAA,CAAE,CAAA;AAAA,MACzD;AACA,MAAA,aAAA,GAAgB,IAAA;AAAA,IAClB,CAAC,CAAA;AAGD,IAAA,YAAA,CAAa;AAAA,MACX,OAAA,EAAS,KAAA;AAAA,MACT,EAAA,EAAI,QAAA;AAAA,MACJ,MAAA,EAAQ,YAAA;AAAA,MACR,MAAA,EAAQ;AAAA,QACN,eAAA,EAAiB,YAAA;AAAA,QACjB,cAAc,EAAC;AAAA,QACf,UAAA,EAAY,EAAE,IAAA,EAAM,iBAAA,EAAmB,SAAS,OAAA;AAAQ;AAC1D,KACD,CAAA;AAAA,EACH;AAEA,EAAA,SAAS,aAAa,GAAA,EAAoC;AACxD,IAAA,IAAI,CAAC,aAAA,EAAe,KAAA,EAAO,QAAA,EAAU;AACrC,IAAA,aAAA,CAAc,MAAM,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,SAAA,CAAU,GAAG,CAAC;AAAA,CAAI,CAAA;AAAA,EACtD;AAEA,EAAA,SAAS,cAAA,CAAe,MAAc,IAAA,EAAiD;AACrF,IAAA,OAAO,IAAI,OAAA,CAAQ,CAACJ,QAAAA,EAAS,MAAA,KAAW;AACtC,MAAA,MAAM,EAAA,GAAK,CAAA,QAAA,EAAW,IAAA,CAAK,GAAA,EAAK,CAAA,CAAA,EAAI,IAAA,CAAK,MAAA,EAAO,CAAE,SAAS,EAAE,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AAC1E,MAAA,eAAA,CAAgB,IAAI,EAAA,EAAI,EAAE,OAAA,EAAAA,QAAAA,EAAS,QAAQ,CAAA;AAC3C,MAAA,YAAA,CAAa;AAAA,QACX,OAAA,EAAS,KAAA;AAAA,QACT,EAAA;AAAA,QACA,MAAA,EAAQ,YAAA;AAAA,QACR,MAAA,EAAQ,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA;AAAK,OACjC,CAAA;AACD,MAAA,UAAA,CAAW,MAAM;AACf,QAAA,IAAI,eAAA,CAAgB,GAAA,CAAI,EAAE,CAAA,EAAG;AAC3B,UAAA,eAAA,CAAgB,OAAO,EAAE,CAAA;AACzB,UAAA,MAAA,CAAO,IAAI,KAAA,CAAM,2BAA2B,CAAC,CAAA;AAAA,QAC/C;AAAA,MACF,GAAG,GAAK,CAAA;AAAA,IACV,CAAC,CAAA;AAAA,EACH;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,SAAA;AAAA,IAEN,MAAA,GAAS;AACP,MAAA,OAAO;AAAA,QACL,MAAA,EAAQ;AAAA,UACN,kCAAA,EAAoC,IAAA,CAAK,SAAA,CAAU,OAAO;AAAA,SAC5D;AAAA,QACA,MAAA,EAAQ;AAAA,UACN,GAAA,EAAK;AAAA,YACH,QAAA,EAAU,IAAA;AAAA,YACV,IAAA,EAAM;AAAA;AACR;AACF,OACF;AAAA,IACF,CAAA;AAAA,IAEA,eAAe,MAAA,EAAQ;AACrB,MAAA,QAAA,GAAW,YAAA,CAAa,OAAO,IAAI,CAAA;AACnC,MAAA,IAAI,UAAU,IAAA,EAAM;AAClB,QAAA,OAAA,GAAU,OAAA,CAAQ,WAAW,QAAA,CAAS,IAAA;AAAA,MACxC;AAAA,IACF,CAAA;AAAA,IAEA,gBAAgB,MAAA,EAAuB;AAErC,MAAA,IAAI,iBAAiB,QAAA,EAAU;AAC7B,QAAA,MAAM,GAAA,GAAM,eAAA,CAAgB,QAAA,EAAU,MAAA,CAAO,OAAO,IAAI,CAAA;AACxD,QAAA,IAAI,GAAA,EAAK;AACP,UAAA,OAAA,CAAQ,GAAA,CAAI;AAAA,iCAAA,EAAsC,GAAG;AAAA,CAAI,CAAA;AACzD,UAAA,WAAA,CAAY,GAAG,CAAA;AAAA,QACjB;AAAA,MACF;AAEA,MAAA,MAAA,CAAO,WAAA,CAAY,GAAA,CAAI,CAAC,GAAA,EAAK,KAAK,IAAA,KAAS;AAEzC,QAAA,GAAA,CAAI,SAAA,CAAU,+BAA+B,GAAG,CAAA;AAChD,QAAA,GAAA,CAAI,SAAA,CAAU,gCAAgC,GAAG,CAAA;AACjD,QAAA,GAAA,CAAI,SAAA,CAAU,gCAAgC,GAAG,CAAA;AAGjD,QAAA,IAAI,GAAA,CAAI,GAAA,KAAQ,YAAA,IAAgB,GAAA,CAAI,QAAQ,aAAA,EAAe;AACzD,UAAA,GAAA,CAAI,SAAA,CAAU,GAAA,EAAK,EAAE,cAAA,EAAgB,aAAa,CAAA;AAClD,UAAA,GAAA,CAAI,GAAA,CAAI,eAAA,CAAgB,OAAO,CAAC,CAAA;AAChC,UAAA;AAAA,QACF;AAGA,QAAA,IAAI,GAAA,CAAI,MAAA,KAAW,MAAA,IAAU,GAAA,CAAI,QAAQ,QAAA,EAAU;AACjD,UAAA,IAAI,IAAA,GAAO,EAAA;AACX,UAAA,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAkB;AAChC,YAAA,IAAA,IAAQ,MAAM,QAAA,EAAS;AAAA,UACzB,CAAC,CAAA;AACD,UAAA,GAAA,CAAI,EAAA,CAAG,OAAO,YAAY;AACxB,YAAA,IAAI;AACF,cAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC3B,cAAA,MAAM,MAAA,GAAS,MAAM,cAAA,CAAe,GAAA,CAAI,MAAA,CAAO,MAAM,GAAA,CAAI,MAAA,CAAO,SAAA,IAAa,EAAE,CAAA;AAC/E,cAAA,GAAA,CAAI,SAAA,CAAU,GAAA,EAAK,EAAE,cAAA,EAAgB,oBAAoB,CAAA;AACzD,cAAA,GAAA,CAAI,GAAA,CAAI,IAAA,CAAK,SAAA,CAAU,MAAM,CAAC,CAAA;AAAA,YAChC,SAAS,GAAA,EAAK;AACZ,cAAA,GAAA,CAAI,SAAA,CAAU,GAAA,EAAK,EAAE,cAAA,EAAgB,oBAAoB,CAAA;AACzD,cAAA,GAAA,CAAI,GAAA;AAAA,gBACF,KAAK,SAAA,CAAU;AAAA,kBACb,OAAA,EAAS,KAAA;AAAA,kBACT,EAAA,EAAI,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA,CAAE,EAAA;AAAA,kBACrB,OAAO,EAAE,IAAA,EAAM,KAAA,EAAQ,OAAA,EAAU,IAAc,OAAA;AAAQ,iBACxD;AAAA,eACH;AAAA,YACF;AAAA,UACF,CAAC,CAAA;AACD,UAAA;AAAA,QACF;AAEA,QAAA,IAAA,EAAK;AAAA,MACP,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IAEA,QAAA,GAAW;AAET,MAAA,IAAI,aAAA,EAAe;AACjB,QAAA,aAAA,CAAc,KAAK,SAAS,CAAA;AAC5B,QAAA,aAAA,GAAgB,IAAA;AAAA,MAClB;AAAA,IACF;AAAA,GACF;AACF;AAEA,SAAS,gBAAgB,OAAA,EAAyB;AAChD,EAAA,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA,SAAA,EAIE,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAAA,EAgBO,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;AA8EhC","file":"index.cjs","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport type { Plugin, ViteDevServer } from \"vite\";\n\nexport interface SynapseVitePluginOptions {\n /** App name. If omitted, reads from ../manifest.json */\n appName?: string;\n /** Path to manifest.json. Default: ../manifest.json (relative to ui/) */\n manifest?: string;\n /**\n * Shell command to start the MCP server. If omitted, derived from manifest.\n * The server runs in stdio mode — stdin/stdout JSON-RPC.\n */\n serverCmd?: string;\n /** Set to false to disable the preview host page at /__preview */\n preview?: boolean;\n}\n\ninterface Manifest {\n name: string;\n version?: string;\n server?: {\n type?: string;\n entry_point?: string;\n mcp_config?: {\n command?: string;\n args?: string[];\n };\n };\n}\n\n/**\n * Synapse Vite plugin — full local dev experience for MCP apps.\n *\n * What it does:\n * - Reads ../manifest.json to get app name and server config\n * - Spawns the MCP server as a child process (stdio mode)\n * - Serves a preview host page at /__preview that iframes your app\n * - Proxies tool calls from the iframe through POST /__mcp to the server\n * - Handles the ext-apps handshake so Synapse hooks work\n * - HMR works inside the iframe — edit .tsx, see changes instantly\n *\n * Usage in vite.config.ts:\n * import { synapseVite } from \"@nimblebrain/synapse/vite\";\n * export default { plugins: [react(), viteSingleFile(), synapseVite()] };\n *\n * Then: cd ui && npm run dev && open http://localhost:5173/__preview\n */\nexport function synapseVite(options: SynapseVitePluginOptions = {}): Plugin {\n const enablePreview = options.preview !== false;\n let manifest: Manifest | null = null;\n let appName = options.appName ?? \"app\";\n let serverProcess: ChildProcess | null = null;\n const pendingRequests = new Map<\n string,\n { resolve: (v: unknown) => void; reject: (e: Error) => void }\n >();\n let serverBuffer = \"\";\n\n function loadManifest(root: string): Manifest | null {\n const manifestPath = options.manifest\n ? resolve(options.manifest)\n : resolve(root, \"..\", \"manifest.json\");\n\n if (!existsSync(manifestPath)) return null;\n try {\n return JSON.parse(readFileSync(manifestPath, \"utf-8\"));\n } catch {\n return null;\n }\n }\n\n function deriveServerCmd(m: Manifest, root: string): string | null {\n if (options.serverCmd) return options.serverCmd;\n const cfg = m.server?.mcp_config;\n if (!cfg?.command) return null;\n\n const serverDir = resolve(root, \"..\");\n let cmd = cfg.command;\n const args = cfg.args ?? [];\n\n // Python projects: use `uv run` if pyproject.toml exists\n if (cmd === \"python\" && existsSync(join(serverDir, \"pyproject.toml\"))) {\n cmd = \"uv run python\";\n }\n\n return `cd ${JSON.stringify(serverDir)} && ${cmd} ${args.join(\" \")}`;\n }\n\n function startServer(cmd: string): void {\n serverProcess = spawn(cmd, {\n shell: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n serverProcess.stderr?.on(\"data\", (d: Buffer) => {\n process.stderr.write(` [mcp] ${d}`);\n });\n\n serverProcess.stdout?.on(\"data\", (d: Buffer) => {\n serverBuffer += d.toString();\n // Parse line-delimited JSON-RPC responses\n const lines = serverBuffer.split(\"\\n\");\n serverBuffer = lines.pop() ?? \"\"; // keep incomplete line\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const msg = JSON.parse(line);\n if (msg.id && pendingRequests.has(msg.id)) {\n const p = pendingRequests.get(msg.id);\n pendingRequests.delete(msg.id);\n p?.resolve(msg);\n }\n } catch {\n // Not JSON — log it\n process.stderr.write(` [mcp] ${line}\\n`);\n }\n }\n });\n\n serverProcess.on(\"exit\", (code) => {\n if (code !== null && code !== 0) {\n console.error(` [mcp] Server exited with code ${code}`);\n }\n serverProcess = null;\n });\n\n // Send initialize\n sendToServer({\n jsonrpc: \"2.0\",\n id: \"init-1\",\n method: \"initialize\",\n params: {\n protocolVersion: \"2024-11-05\",\n capabilities: {},\n clientInfo: { name: \"synapse-preview\", version: \"0.1.0\" },\n },\n });\n }\n\n function sendToServer(msg: Record<string, unknown>): void {\n if (!serverProcess?.stdin?.writable) return;\n serverProcess.stdin.write(`${JSON.stringify(msg)}\\n`);\n }\n\n function callServerTool(name: string, args: Record<string, unknown>): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const id = `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n pendingRequests.set(id, { resolve, reject });\n sendToServer({\n jsonrpc: \"2.0\",\n id,\n method: \"tools/call\",\n params: { name, arguments: args },\n });\n setTimeout(() => {\n if (pendingRequests.has(id)) {\n pendingRequests.delete(id);\n reject(new Error(\"Tool call timed out (10s)\"));\n }\n }, 10000);\n });\n }\n\n return {\n name: \"synapse\",\n\n config() {\n return {\n define: {\n \"import.meta.env.SYNAPSE_APP_NAME\": JSON.stringify(appName),\n },\n server: {\n hmr: {\n protocol: \"ws\",\n host: \"localhost\",\n },\n },\n };\n },\n\n configResolved(config) {\n manifest = loadManifest(config.root);\n if (manifest?.name) {\n appName = options.appName ?? manifest.name;\n }\n },\n\n configureServer(server: ViteDevServer) {\n // Start MCP server\n if (enablePreview && manifest) {\n const cmd = deriveServerCmd(manifest, server.config.root);\n if (cmd) {\n console.log(`\\n [synapse] Starting MCP server: ${cmd}\\n`);\n startServer(cmd);\n }\n }\n\n server.middlewares.use((req, res, next) => {\n // CORS for iframe communication\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"*\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n // /__preview — bridge host page\n if (req.url === \"/__preview\" || req.url === \"/__preview/\") {\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(previewHostHtml(appName));\n return;\n }\n\n // POST /__mcp — tool call proxy\n if (req.method === \"POST\" && req.url === \"/__mcp\") {\n let body = \"\";\n req.on(\"data\", (chunk: Buffer) => {\n body += chunk.toString();\n });\n req.on(\"end\", async () => {\n try {\n const msg = JSON.parse(body);\n const result = await callServerTool(msg.params.name, msg.params.arguments || {});\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(result));\n } catch (err) {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n id: JSON.parse(body).id,\n error: { code: -32000, message: (err as Error).message },\n }),\n );\n }\n });\n return;\n }\n\n next();\n });\n },\n\n buildEnd() {\n // Kill server on build end (for production builds)\n if (serverProcess) {\n serverProcess.kill(\"SIGTERM\");\n serverProcess = null;\n }\n },\n };\n}\n\nfunction previewHostHtml(appName: string): string {\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\" />\n <title>${appName} — Synapse Preview</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; background: #0f172a; color: #e2e8f0; }\n header { padding: 10px 16px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; align-items: center; gap: 10px; font-size: 13px; }\n header .dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }\n header .name { font-weight: 600; }\n header .spacer { flex: 1; }\n header button { background: #334155; border: none; color: #e2e8f0; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; }\n header .url { color: #64748b; font-size: 11px; font-family: monospace; }\n iframe { width: 100%; height: calc(100vh - 41px); border: none; }\n </style>\n</head>\n<body>\n <header>\n <span class=\"dot\"></span>\n <span class=\"name\">${appName}</span>\n <span class=\"spacer\"></span>\n <button id=\"toggle\">Toggle Theme</button>\n <span class=\"url\">Synapse Preview</span>\n </header>\n <iframe id=\"app\" src=\"/\"></iframe>\n\n <script>\n var iframe = document.getElementById(\"app\");\n var dark = true;\n\n function getTokens(d) {\n return d ? {\n \"--nb-background\":\"#0f172a\",\"--nb-foreground\":\"#e2e8f0\",\n \"--nb-card\":\"#1e293b\",\"--nb-card-foreground\":\"#e2e8f0\",\n \"--nb-primary\":\"#6366f1\",\"--nb-primary-foreground\":\"#fff\",\n \"--nb-muted-foreground\":\"#94a3b8\",\"--nb-border\":\"#334155\",\n \"--nb-ring\":\"#6366f1\",\"--nb-destructive\":\"#ef4444\",\n \"--nb-radius\":\"0.5rem\",\"--nb-font-sans\":\"-apple-system,BlinkMacSystemFont,sans-serif\"\n } : {\n \"--nb-background\":\"#ffffff\",\"--nb-foreground\":\"#0f172a\",\n \"--nb-card\":\"#f8fafc\",\"--nb-card-foreground\":\"#0f172a\",\n \"--nb-primary\":\"#6366f1\",\"--nb-primary-foreground\":\"#fff\",\n \"--nb-muted-foreground\":\"#64748b\",\"--nb-border\":\"#e2e8f0\",\n \"--nb-ring\":\"#6366f1\",\"--nb-destructive\":\"#ef4444\",\n \"--nb-radius\":\"0.5rem\",\"--nb-font-sans\":\"-apple-system,BlinkMacSystemFont,sans-serif\"\n };\n }\n\n function post(msg) { iframe.contentWindow.postMessage(msg, \"*\"); }\n\n window.addEventListener(\"message\", async function(e) {\n if (e.source !== iframe.contentWindow) return;\n var msg = e.data;\n if (!msg || typeof msg !== \"object\") return;\n\n // ext-apps handshake\n if (msg.method === \"ui/initialize\" && msg.id) {\n post({ jsonrpc:\"2.0\", id:msg.id, result: {\n protocolVersion:\"2026-01-26\",\n serverInfo:{name:\"nimblebrain\",version:\"preview\"},\n capabilities:{openLinks:{},serverTools:{}},\n hostContext:{theme:dark?\"dark\":\"light\",primaryColor:\"#6366f1\",tokens:getTokens(dark)}\n }});\n return;\n }\n if (msg.method === \"ui/notifications/initialized\") return;\n\n // Tool calls — proxy via Vite middleware\n if (msg.method === \"tools/call\" && msg.id) {\n try {\n var r = await fetch(\"/__mcp\", {\n method:\"POST\", headers:{\"Content-Type\":\"application/json\"},\n body: JSON.stringify({jsonrpc:\"2.0\",id:msg.id,method:\"tools/call\",params:{name:msg.params.name,arguments:msg.params.arguments||{}}})\n });\n post(await r.json());\n } catch(err) {\n post({jsonrpc:\"2.0\",id:msg.id,error:{code:-32000,message:err.message}});\n }\n return;\n }\n\n // Log other messages\n if (msg.method === \"ui/chat\") console.log(\"[chat]\", msg.params?.message);\n else if (msg.method === \"ui/action\") console.log(\"[action]\", msg.params?.action, msg.params);\n else if (msg.method === \"ui/stateChanged\") { console.log(\"[state]\", msg.params?.state); post({jsonrpc:\"2.0\",method:\"ui/stateAcknowledged\",params:{truncated:false}}); }\n else if (msg.method === \"ui/keydown\") { /* ignore */ }\n else if (msg.method) console.log(\"[bridge]\", msg.method, msg);\n });\n\n document.getElementById(\"toggle\").onclick = function() {\n dark = !dark;\n document.body.style.background = dark ? \"#0f172a\" : \"#f1f5f9\";\n post({jsonrpc:\"2.0\",method:\"ui/themeChanged\",params:{mode:dark?\"dark\":\"light\",tokens:getTokens(dark)}});\n };\n </script>\n</body>\n</html>`;\n}\n"]}
@@ -1,21 +1,35 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
3
  interface SynapseVitePluginOptions {
4
- /** App name (must match manifest) */
5
- appName: string;
6
- /** Platform API URL (default: http://localhost:4321) */
7
- platformUrl?: string;
8
- /** Auto-inject bridge runtime into HTML entry (default: true) */
9
- injectBridge?: boolean;
4
+ /** App name. If omitted, reads from ../manifest.json */
5
+ appName?: string;
6
+ /** Path to manifest.json. Default: ../manifest.json (relative to ui/) */
7
+ manifest?: string;
8
+ /**
9
+ * Shell command to start the MCP server. If omitted, derived from manifest.
10
+ * The server runs in stdio mode — stdin/stdout JSON-RPC.
11
+ */
12
+ serverCmd?: string;
13
+ /** Set to false to disable the preview host page at /__preview */
14
+ preview?: boolean;
10
15
  }
11
16
  /**
12
- * Vite plugin for Synapse app development.
17
+ * Synapse Vite plugin — full local dev experience for MCP apps.
13
18
  *
14
- * - Configures CORS for cross-origin iframe communication
15
- * - Injects ext-apps bridge runtime if `injectBridge` is true
16
- * - Sets up HMR WebSocket to work inside iframe sandbox
17
- * - Exposes platform URL as `import.meta.env.SYNAPSE_PLATFORM_URL`
19
+ * What it does:
20
+ * - Reads ../manifest.json to get app name and server config
21
+ * - Spawns the MCP server as a child process (stdio mode)
22
+ * - Serves a preview host page at /__preview that iframes your app
23
+ * - Proxies tool calls from the iframe through POST /__mcp to the server
24
+ * - Handles the ext-apps handshake so Synapse hooks work
25
+ * - HMR works inside the iframe — edit .tsx, see changes instantly
26
+ *
27
+ * Usage in vite.config.ts:
28
+ * import { synapseVite } from "@nimblebrain/synapse/vite";
29
+ * export default { plugins: [react(), viteSingleFile(), synapseVite()] };
30
+ *
31
+ * Then: cd ui && npm run dev && open http://localhost:5173/__preview
18
32
  */
19
- declare function synapseVite(options: SynapseVitePluginOptions): Plugin;
33
+ declare function synapseVite(options?: SynapseVitePluginOptions): Plugin;
20
34
 
21
35
  export { type SynapseVitePluginOptions, synapseVite };
@@ -1,21 +1,35 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
3
  interface SynapseVitePluginOptions {
4
- /** App name (must match manifest) */
5
- appName: string;
6
- /** Platform API URL (default: http://localhost:4321) */
7
- platformUrl?: string;
8
- /** Auto-inject bridge runtime into HTML entry (default: true) */
9
- injectBridge?: boolean;
4
+ /** App name. If omitted, reads from ../manifest.json */
5
+ appName?: string;
6
+ /** Path to manifest.json. Default: ../manifest.json (relative to ui/) */
7
+ manifest?: string;
8
+ /**
9
+ * Shell command to start the MCP server. If omitted, derived from manifest.
10
+ * The server runs in stdio mode — stdin/stdout JSON-RPC.
11
+ */
12
+ serverCmd?: string;
13
+ /** Set to false to disable the preview host page at /__preview */
14
+ preview?: boolean;
10
15
  }
11
16
  /**
12
- * Vite plugin for Synapse app development.
17
+ * Synapse Vite plugin — full local dev experience for MCP apps.
13
18
  *
14
- * - Configures CORS for cross-origin iframe communication
15
- * - Injects ext-apps bridge runtime if `injectBridge` is true
16
- * - Sets up HMR WebSocket to work inside iframe sandbox
17
- * - Exposes platform URL as `import.meta.env.SYNAPSE_PLATFORM_URL`
19
+ * What it does:
20
+ * - Reads ../manifest.json to get app name and server config
21
+ * - Spawns the MCP server as a child process (stdio mode)
22
+ * - Serves a preview host page at /__preview that iframes your app
23
+ * - Proxies tool calls from the iframe through POST /__mcp to the server
24
+ * - Handles the ext-apps handshake so Synapse hooks work
25
+ * - HMR works inside the iframe — edit .tsx, see changes instantly
26
+ *
27
+ * Usage in vite.config.ts:
28
+ * import { synapseVite } from "@nimblebrain/synapse/vite";
29
+ * export default { plugins: [react(), viteSingleFile(), synapseVite()] };
30
+ *
31
+ * Then: cd ui && npm run dev && open http://localhost:5173/__preview
18
32
  */
19
- declare function synapseVite(options: SynapseVitePluginOptions): Plugin;
33
+ declare function synapseVite(options?: SynapseVitePluginOptions): Plugin;
20
34
 
21
35
  export { type SynapseVitePluginOptions, synapseVite };
@@ -1,46 +1,278 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { resolve, join } from 'path';
4
+
1
5
  // src/vite/plugin.ts
2
- function synapseVite(options) {
3
- const { appName, platformUrl = "http://localhost:4321", injectBridge = true } = options;
6
+ function synapseVite(options = {}) {
7
+ const enablePreview = options.preview !== false;
8
+ let manifest = null;
9
+ let appName = options.appName ?? "app";
10
+ let serverProcess = null;
11
+ const pendingRequests = /* @__PURE__ */ new Map();
12
+ let serverBuffer = "";
13
+ function loadManifest(root) {
14
+ const manifestPath = options.manifest ? resolve(options.manifest) : resolve(root, "..", "manifest.json");
15
+ if (!existsSync(manifestPath)) return null;
16
+ try {
17
+ return JSON.parse(readFileSync(manifestPath, "utf-8"));
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ function deriveServerCmd(m, root) {
23
+ if (options.serverCmd) return options.serverCmd;
24
+ const cfg = m.server?.mcp_config;
25
+ if (!cfg?.command) return null;
26
+ const serverDir = resolve(root, "..");
27
+ let cmd = cfg.command;
28
+ const args = cfg.args ?? [];
29
+ if (cmd === "python" && existsSync(join(serverDir, "pyproject.toml"))) {
30
+ cmd = "uv run python";
31
+ }
32
+ return `cd ${JSON.stringify(serverDir)} && ${cmd} ${args.join(" ")}`;
33
+ }
34
+ function startServer(cmd) {
35
+ serverProcess = spawn(cmd, {
36
+ shell: true,
37
+ stdio: ["pipe", "pipe", "pipe"]
38
+ });
39
+ serverProcess.stderr?.on("data", (d) => {
40
+ process.stderr.write(` [mcp] ${d}`);
41
+ });
42
+ serverProcess.stdout?.on("data", (d) => {
43
+ serverBuffer += d.toString();
44
+ const lines = serverBuffer.split("\n");
45
+ serverBuffer = lines.pop() ?? "";
46
+ for (const line of lines) {
47
+ if (!line.trim()) continue;
48
+ try {
49
+ const msg = JSON.parse(line);
50
+ if (msg.id && pendingRequests.has(msg.id)) {
51
+ const p = pendingRequests.get(msg.id);
52
+ pendingRequests.delete(msg.id);
53
+ p?.resolve(msg);
54
+ }
55
+ } catch {
56
+ process.stderr.write(` [mcp] ${line}
57
+ `);
58
+ }
59
+ }
60
+ });
61
+ serverProcess.on("exit", (code) => {
62
+ if (code !== null && code !== 0) {
63
+ console.error(` [mcp] Server exited with code ${code}`);
64
+ }
65
+ serverProcess = null;
66
+ });
67
+ sendToServer({
68
+ jsonrpc: "2.0",
69
+ id: "init-1",
70
+ method: "initialize",
71
+ params: {
72
+ protocolVersion: "2024-11-05",
73
+ capabilities: {},
74
+ clientInfo: { name: "synapse-preview", version: "0.1.0" }
75
+ }
76
+ });
77
+ }
78
+ function sendToServer(msg) {
79
+ if (!serverProcess?.stdin?.writable) return;
80
+ serverProcess.stdin.write(`${JSON.stringify(msg)}
81
+ `);
82
+ }
83
+ function callServerTool(name, args) {
84
+ return new Promise((resolve2, reject) => {
85
+ const id = `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
86
+ pendingRequests.set(id, { resolve: resolve2, reject });
87
+ sendToServer({
88
+ jsonrpc: "2.0",
89
+ id,
90
+ method: "tools/call",
91
+ params: { name, arguments: args }
92
+ });
93
+ setTimeout(() => {
94
+ if (pendingRequests.has(id)) {
95
+ pendingRequests.delete(id);
96
+ reject(new Error("Tool call timed out (10s)"));
97
+ }
98
+ }, 1e4);
99
+ });
100
+ }
4
101
  return {
5
102
  name: "synapse",
6
103
  config() {
7
104
  return {
8
105
  define: {
9
- "import.meta.env.SYNAPSE_PLATFORM_URL": JSON.stringify(platformUrl),
10
106
  "import.meta.env.SYNAPSE_APP_NAME": JSON.stringify(appName)
11
107
  },
12
108
  server: {
13
109
  hmr: {
14
- // Required for HMR inside sandboxed iframe
15
110
  protocol: "ws",
16
111
  host: "localhost"
17
112
  }
18
113
  }
19
114
  };
20
115
  },
116
+ configResolved(config) {
117
+ manifest = loadManifest(config.root);
118
+ if (manifest?.name) {
119
+ appName = options.appName ?? manifest.name;
120
+ }
121
+ },
21
122
  configureServer(server) {
22
- server.middlewares.use((_req, res, next) => {
123
+ if (enablePreview && manifest) {
124
+ const cmd = deriveServerCmd(manifest, server.config.root);
125
+ if (cmd) {
126
+ console.log(`
127
+ [synapse] Starting MCP server: ${cmd}
128
+ `);
129
+ startServer(cmd);
130
+ }
131
+ }
132
+ server.middlewares.use((req, res, next) => {
23
133
  res.setHeader("Access-Control-Allow-Origin", "*");
24
134
  res.setHeader("Access-Control-Allow-Methods", "*");
25
135
  res.setHeader("Access-Control-Allow-Headers", "*");
136
+ if (req.url === "/__preview" || req.url === "/__preview/") {
137
+ res.writeHead(200, { "Content-Type": "text/html" });
138
+ res.end(previewHostHtml(appName));
139
+ return;
140
+ }
141
+ if (req.method === "POST" && req.url === "/__mcp") {
142
+ let body = "";
143
+ req.on("data", (chunk) => {
144
+ body += chunk.toString();
145
+ });
146
+ req.on("end", async () => {
147
+ try {
148
+ const msg = JSON.parse(body);
149
+ const result = await callServerTool(msg.params.name, msg.params.arguments || {});
150
+ res.writeHead(200, { "Content-Type": "application/json" });
151
+ res.end(JSON.stringify(result));
152
+ } catch (err) {
153
+ res.writeHead(200, { "Content-Type": "application/json" });
154
+ res.end(
155
+ JSON.stringify({
156
+ jsonrpc: "2.0",
157
+ id: JSON.parse(body).id,
158
+ error: { code: -32e3, message: err.message }
159
+ })
160
+ );
161
+ }
162
+ });
163
+ return;
164
+ }
26
165
  next();
27
166
  });
28
167
  },
29
- transformIndexHtml(html) {
30
- if (!injectBridge) return html;
31
- const bridgeScript = `
32
- <script type="module">
33
- import { createSynapse } from "@nimblebrain/synapse";
34
- window.__synapse = createSynapse({
35
- name: ${JSON.stringify(appName)},
36
- version: "0.0.0-dev",
37
- });
38
- </script>`;
39
- return html.replace("</head>", `${bridgeScript}
40
- </head>`);
168
+ buildEnd() {
169
+ if (serverProcess) {
170
+ serverProcess.kill("SIGTERM");
171
+ serverProcess = null;
172
+ }
41
173
  }
42
174
  };
43
175
  }
176
+ function previewHostHtml(appName) {
177
+ return `<!DOCTYPE html>
178
+ <html>
179
+ <head>
180
+ <meta charset="utf-8" />
181
+ <title>${appName} \u2014 Synapse Preview</title>
182
+ <style>
183
+ * { margin: 0; padding: 0; box-sizing: border-box; }
184
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #0f172a; color: #e2e8f0; }
185
+ header { padding: 10px 16px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; align-items: center; gap: 10px; font-size: 13px; }
186
+ header .dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }
187
+ header .name { font-weight: 600; }
188
+ header .spacer { flex: 1; }
189
+ header button { background: #334155; border: none; color: #e2e8f0; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; }
190
+ header .url { color: #64748b; font-size: 11px; font-family: monospace; }
191
+ iframe { width: 100%; height: calc(100vh - 41px); border: none; }
192
+ </style>
193
+ </head>
194
+ <body>
195
+ <header>
196
+ <span class="dot"></span>
197
+ <span class="name">${appName}</span>
198
+ <span class="spacer"></span>
199
+ <button id="toggle">Toggle Theme</button>
200
+ <span class="url">Synapse Preview</span>
201
+ </header>
202
+ <iframe id="app" src="/"></iframe>
203
+
204
+ <script>
205
+ var iframe = document.getElementById("app");
206
+ var dark = true;
207
+
208
+ function getTokens(d) {
209
+ return d ? {
210
+ "--nb-background":"#0f172a","--nb-foreground":"#e2e8f0",
211
+ "--nb-card":"#1e293b","--nb-card-foreground":"#e2e8f0",
212
+ "--nb-primary":"#6366f1","--nb-primary-foreground":"#fff",
213
+ "--nb-muted-foreground":"#94a3b8","--nb-border":"#334155",
214
+ "--nb-ring":"#6366f1","--nb-destructive":"#ef4444",
215
+ "--nb-radius":"0.5rem","--nb-font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
216
+ } : {
217
+ "--nb-background":"#ffffff","--nb-foreground":"#0f172a",
218
+ "--nb-card":"#f8fafc","--nb-card-foreground":"#0f172a",
219
+ "--nb-primary":"#6366f1","--nb-primary-foreground":"#fff",
220
+ "--nb-muted-foreground":"#64748b","--nb-border":"#e2e8f0",
221
+ "--nb-ring":"#6366f1","--nb-destructive":"#ef4444",
222
+ "--nb-radius":"0.5rem","--nb-font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
223
+ };
224
+ }
225
+
226
+ function post(msg) { iframe.contentWindow.postMessage(msg, "*"); }
227
+
228
+ window.addEventListener("message", async function(e) {
229
+ if (e.source !== iframe.contentWindow) return;
230
+ var msg = e.data;
231
+ if (!msg || typeof msg !== "object") return;
232
+
233
+ // ext-apps handshake
234
+ if (msg.method === "ui/initialize" && msg.id) {
235
+ post({ jsonrpc:"2.0", id:msg.id, result: {
236
+ protocolVersion:"2026-01-26",
237
+ serverInfo:{name:"nimblebrain",version:"preview"},
238
+ capabilities:{openLinks:{},serverTools:{}},
239
+ hostContext:{theme:dark?"dark":"light",primaryColor:"#6366f1",tokens:getTokens(dark)}
240
+ }});
241
+ return;
242
+ }
243
+ if (msg.method === "ui/notifications/initialized") return;
244
+
245
+ // Tool calls \u2014 proxy via Vite middleware
246
+ if (msg.method === "tools/call" && msg.id) {
247
+ try {
248
+ var r = await fetch("/__mcp", {
249
+ method:"POST", headers:{"Content-Type":"application/json"},
250
+ body: JSON.stringify({jsonrpc:"2.0",id:msg.id,method:"tools/call",params:{name:msg.params.name,arguments:msg.params.arguments||{}}})
251
+ });
252
+ post(await r.json());
253
+ } catch(err) {
254
+ post({jsonrpc:"2.0",id:msg.id,error:{code:-32000,message:err.message}});
255
+ }
256
+ return;
257
+ }
258
+
259
+ // Log other messages
260
+ if (msg.method === "ui/chat") console.log("[chat]", msg.params?.message);
261
+ else if (msg.method === "ui/action") console.log("[action]", msg.params?.action, msg.params);
262
+ else if (msg.method === "ui/stateChanged") { console.log("[state]", msg.params?.state); post({jsonrpc:"2.0",method:"ui/stateAcknowledged",params:{truncated:false}}); }
263
+ else if (msg.method === "ui/keydown") { /* ignore */ }
264
+ else if (msg.method) console.log("[bridge]", msg.method, msg);
265
+ });
266
+
267
+ document.getElementById("toggle").onclick = function() {
268
+ dark = !dark;
269
+ document.body.style.background = dark ? "#0f172a" : "#f1f5f9";
270
+ post({jsonrpc:"2.0",method:"ui/themeChanged",params:{mode:dark?"dark":"light",tokens:getTokens(dark)}});
271
+ };
272
+ </script>
273
+ </body>
274
+ </html>`;
275
+ }
44
276
 
45
277
  export { synapseVite };
46
278
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/vite/plugin.ts"],"names":[],"mappings":";AAmBO,SAAS,YAAY,OAAA,EAA2C;AACrE,EAAA,MAAM,EAAE,OAAA,EAAS,WAAA,GAAc,uBAAA,EAAyB,YAAA,GAAe,MAAK,GAAI,OAAA;AAEhF,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,SAAA;AAAA,IAEN,MAAA,GAAS;AACP,MAAA,OAAO;AAAA,QACL,MAAA,EAAQ;AAAA,UACN,sCAAA,EAAwC,IAAA,CAAK,SAAA,CAAU,WAAW,CAAA;AAAA,UAClE,kCAAA,EAAoC,IAAA,CAAK,SAAA,CAAU,OAAO;AAAA,SAC5D;AAAA,QACA,MAAA,EAAQ;AAAA,UACN,GAAA,EAAK;AAAA;AAAA,YAEH,QAAA,EAAU,IAAA;AAAA,YACV,IAAA,EAAM;AAAA;AACR;AACF,OACF;AAAA,IACF,CAAA;AAAA,IAEA,gBAAgB,MAAA,EAAuB;AAErC,MAAA,MAAA,CAAO,WAAA,CAAY,GAAA,CAAI,CAAC,IAAA,EAAM,KAAK,IAAA,KAAS;AAC1C,QAAA,GAAA,CAAI,SAAA,CAAU,+BAA+B,GAAG,CAAA;AAChD,QAAA,GAAA,CAAI,SAAA,CAAU,gCAAgC,GAAG,CAAA;AACjD,QAAA,GAAA,CAAI,SAAA,CAAU,gCAAgC,GAAG,CAAA;AACjD,QAAA,IAAA,EAAK;AAAA,MACP,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IAEA,mBAAmB,IAAA,EAAc;AAC/B,MAAA,IAAI,CAAC,cAAc,OAAO,IAAA;AAE1B,MAAA,MAAM,YAAA,GAAe;AAAA;AAAA;AAAA;AAAA,UAAA,EAIf,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA;AAAA;AAAA,SAAA,CAAA;AAM7B,MAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,SAAA,EAAW,CAAA,EAAG,YAAY;AAAA,OAAA,CAAW,CAAA;AAAA,IAC3D;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import type { Plugin, ViteDevServer } from \"vite\";\n\nexport interface SynapseVitePluginOptions {\n /** App name (must match manifest) */\n appName: string;\n /** Platform API URL (default: http://localhost:4321) */\n platformUrl?: string;\n /** Auto-inject bridge runtime into HTML entry (default: true) */\n injectBridge?: boolean;\n}\n\n/**\n * Vite plugin for Synapse app development.\n *\n * - Configures CORS for cross-origin iframe communication\n * - Injects ext-apps bridge runtime if `injectBridge` is true\n * - Sets up HMR WebSocket to work inside iframe sandbox\n * - Exposes platform URL as `import.meta.env.SYNAPSE_PLATFORM_URL`\n */\nexport function synapseVite(options: SynapseVitePluginOptions): Plugin {\n const { appName, platformUrl = \"http://localhost:4321\", injectBridge = true } = options;\n\n return {\n name: \"synapse\",\n\n config() {\n return {\n define: {\n \"import.meta.env.SYNAPSE_PLATFORM_URL\": JSON.stringify(platformUrl),\n \"import.meta.env.SYNAPSE_APP_NAME\": JSON.stringify(appName),\n },\n server: {\n hmr: {\n // Required for HMR inside sandboxed iframe\n protocol: \"ws\",\n host: \"localhost\",\n },\n },\n };\n },\n\n configureServer(server: ViteDevServer) {\n // Add CORS headers for cross-origin iframe communication\n server.middlewares.use((_req, res, next) => {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"*\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n next();\n });\n },\n\n transformIndexHtml(html: string) {\n if (!injectBridge) return html;\n\n const bridgeScript = `\n<script type=\"module\">\n import { createSynapse } from \"@nimblebrain/synapse\";\n window.__synapse = createSynapse({\n name: ${JSON.stringify(appName)},\n version: \"0.0.0-dev\",\n });\n</script>`;\n\n // Inject before closing </head> tag\n return html.replace(\"</head>\", `${bridgeScript}\\n</head>`);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/vite/plugin.ts"],"names":["resolve"],"mappings":"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","file":"index.js","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport type { Plugin, ViteDevServer } from \"vite\";\n\nexport interface SynapseVitePluginOptions {\n /** App name. If omitted, reads from ../manifest.json */\n appName?: string;\n /** Path to manifest.json. Default: ../manifest.json (relative to ui/) */\n manifest?: string;\n /**\n * Shell command to start the MCP server. If omitted, derived from manifest.\n * The server runs in stdio mode — stdin/stdout JSON-RPC.\n */\n serverCmd?: string;\n /** Set to false to disable the preview host page at /__preview */\n preview?: boolean;\n}\n\ninterface Manifest {\n name: string;\n version?: string;\n server?: {\n type?: string;\n entry_point?: string;\n mcp_config?: {\n command?: string;\n args?: string[];\n };\n };\n}\n\n/**\n * Synapse Vite plugin — full local dev experience for MCP apps.\n *\n * What it does:\n * - Reads ../manifest.json to get app name and server config\n * - Spawns the MCP server as a child process (stdio mode)\n * - Serves a preview host page at /__preview that iframes your app\n * - Proxies tool calls from the iframe through POST /__mcp to the server\n * - Handles the ext-apps handshake so Synapse hooks work\n * - HMR works inside the iframe — edit .tsx, see changes instantly\n *\n * Usage in vite.config.ts:\n * import { synapseVite } from \"@nimblebrain/synapse/vite\";\n * export default { plugins: [react(), viteSingleFile(), synapseVite()] };\n *\n * Then: cd ui && npm run dev && open http://localhost:5173/__preview\n */\nexport function synapseVite(options: SynapseVitePluginOptions = {}): Plugin {\n const enablePreview = options.preview !== false;\n let manifest: Manifest | null = null;\n let appName = options.appName ?? \"app\";\n let serverProcess: ChildProcess | null = null;\n const pendingRequests = new Map<\n string,\n { resolve: (v: unknown) => void; reject: (e: Error) => void }\n >();\n let serverBuffer = \"\";\n\n function loadManifest(root: string): Manifest | null {\n const manifestPath = options.manifest\n ? resolve(options.manifest)\n : resolve(root, \"..\", \"manifest.json\");\n\n if (!existsSync(manifestPath)) return null;\n try {\n return JSON.parse(readFileSync(manifestPath, \"utf-8\"));\n } catch {\n return null;\n }\n }\n\n function deriveServerCmd(m: Manifest, root: string): string | null {\n if (options.serverCmd) return options.serverCmd;\n const cfg = m.server?.mcp_config;\n if (!cfg?.command) return null;\n\n const serverDir = resolve(root, \"..\");\n let cmd = cfg.command;\n const args = cfg.args ?? [];\n\n // Python projects: use `uv run` if pyproject.toml exists\n if (cmd === \"python\" && existsSync(join(serverDir, \"pyproject.toml\"))) {\n cmd = \"uv run python\";\n }\n\n return `cd ${JSON.stringify(serverDir)} && ${cmd} ${args.join(\" \")}`;\n }\n\n function startServer(cmd: string): void {\n serverProcess = spawn(cmd, {\n shell: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n serverProcess.stderr?.on(\"data\", (d: Buffer) => {\n process.stderr.write(` [mcp] ${d}`);\n });\n\n serverProcess.stdout?.on(\"data\", (d: Buffer) => {\n serverBuffer += d.toString();\n // Parse line-delimited JSON-RPC responses\n const lines = serverBuffer.split(\"\\n\");\n serverBuffer = lines.pop() ?? \"\"; // keep incomplete line\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const msg = JSON.parse(line);\n if (msg.id && pendingRequests.has(msg.id)) {\n const p = pendingRequests.get(msg.id);\n pendingRequests.delete(msg.id);\n p?.resolve(msg);\n }\n } catch {\n // Not JSON — log it\n process.stderr.write(` [mcp] ${line}\\n`);\n }\n }\n });\n\n serverProcess.on(\"exit\", (code) => {\n if (code !== null && code !== 0) {\n console.error(` [mcp] Server exited with code ${code}`);\n }\n serverProcess = null;\n });\n\n // Send initialize\n sendToServer({\n jsonrpc: \"2.0\",\n id: \"init-1\",\n method: \"initialize\",\n params: {\n protocolVersion: \"2024-11-05\",\n capabilities: {},\n clientInfo: { name: \"synapse-preview\", version: \"0.1.0\" },\n },\n });\n }\n\n function sendToServer(msg: Record<string, unknown>): void {\n if (!serverProcess?.stdin?.writable) return;\n serverProcess.stdin.write(`${JSON.stringify(msg)}\\n`);\n }\n\n function callServerTool(name: string, args: Record<string, unknown>): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const id = `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n pendingRequests.set(id, { resolve, reject });\n sendToServer({\n jsonrpc: \"2.0\",\n id,\n method: \"tools/call\",\n params: { name, arguments: args },\n });\n setTimeout(() => {\n if (pendingRequests.has(id)) {\n pendingRequests.delete(id);\n reject(new Error(\"Tool call timed out (10s)\"));\n }\n }, 10000);\n });\n }\n\n return {\n name: \"synapse\",\n\n config() {\n return {\n define: {\n \"import.meta.env.SYNAPSE_APP_NAME\": JSON.stringify(appName),\n },\n server: {\n hmr: {\n protocol: \"ws\",\n host: \"localhost\",\n },\n },\n };\n },\n\n configResolved(config) {\n manifest = loadManifest(config.root);\n if (manifest?.name) {\n appName = options.appName ?? manifest.name;\n }\n },\n\n configureServer(server: ViteDevServer) {\n // Start MCP server\n if (enablePreview && manifest) {\n const cmd = deriveServerCmd(manifest, server.config.root);\n if (cmd) {\n console.log(`\\n [synapse] Starting MCP server: ${cmd}\\n`);\n startServer(cmd);\n }\n }\n\n server.middlewares.use((req, res, next) => {\n // CORS for iframe communication\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"*\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n // /__preview — bridge host page\n if (req.url === \"/__preview\" || req.url === \"/__preview/\") {\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(previewHostHtml(appName));\n return;\n }\n\n // POST /__mcp — tool call proxy\n if (req.method === \"POST\" && req.url === \"/__mcp\") {\n let body = \"\";\n req.on(\"data\", (chunk: Buffer) => {\n body += chunk.toString();\n });\n req.on(\"end\", async () => {\n try {\n const msg = JSON.parse(body);\n const result = await callServerTool(msg.params.name, msg.params.arguments || {});\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(result));\n } catch (err) {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n id: JSON.parse(body).id,\n error: { code: -32000, message: (err as Error).message },\n }),\n );\n }\n });\n return;\n }\n\n next();\n });\n },\n\n buildEnd() {\n // Kill server on build end (for production builds)\n if (serverProcess) {\n serverProcess.kill(\"SIGTERM\");\n serverProcess = null;\n }\n },\n };\n}\n\nfunction previewHostHtml(appName: string): string {\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\" />\n <title>${appName} — Synapse Preview</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; background: #0f172a; color: #e2e8f0; }\n header { padding: 10px 16px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; align-items: center; gap: 10px; font-size: 13px; }\n header .dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }\n header .name { font-weight: 600; }\n header .spacer { flex: 1; }\n header button { background: #334155; border: none; color: #e2e8f0; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; }\n header .url { color: #64748b; font-size: 11px; font-family: monospace; }\n iframe { width: 100%; height: calc(100vh - 41px); border: none; }\n </style>\n</head>\n<body>\n <header>\n <span class=\"dot\"></span>\n <span class=\"name\">${appName}</span>\n <span class=\"spacer\"></span>\n <button id=\"toggle\">Toggle Theme</button>\n <span class=\"url\">Synapse Preview</span>\n </header>\n <iframe id=\"app\" src=\"/\"></iframe>\n\n <script>\n var iframe = document.getElementById(\"app\");\n var dark = true;\n\n function getTokens(d) {\n return d ? {\n \"--nb-background\":\"#0f172a\",\"--nb-foreground\":\"#e2e8f0\",\n \"--nb-card\":\"#1e293b\",\"--nb-card-foreground\":\"#e2e8f0\",\n \"--nb-primary\":\"#6366f1\",\"--nb-primary-foreground\":\"#fff\",\n \"--nb-muted-foreground\":\"#94a3b8\",\"--nb-border\":\"#334155\",\n \"--nb-ring\":\"#6366f1\",\"--nb-destructive\":\"#ef4444\",\n \"--nb-radius\":\"0.5rem\",\"--nb-font-sans\":\"-apple-system,BlinkMacSystemFont,sans-serif\"\n } : {\n \"--nb-background\":\"#ffffff\",\"--nb-foreground\":\"#0f172a\",\n \"--nb-card\":\"#f8fafc\",\"--nb-card-foreground\":\"#0f172a\",\n \"--nb-primary\":\"#6366f1\",\"--nb-primary-foreground\":\"#fff\",\n \"--nb-muted-foreground\":\"#64748b\",\"--nb-border\":\"#e2e8f0\",\n \"--nb-ring\":\"#6366f1\",\"--nb-destructive\":\"#ef4444\",\n \"--nb-radius\":\"0.5rem\",\"--nb-font-sans\":\"-apple-system,BlinkMacSystemFont,sans-serif\"\n };\n }\n\n function post(msg) { iframe.contentWindow.postMessage(msg, \"*\"); }\n\n window.addEventListener(\"message\", async function(e) {\n if (e.source !== iframe.contentWindow) return;\n var msg = e.data;\n if (!msg || typeof msg !== \"object\") return;\n\n // ext-apps handshake\n if (msg.method === \"ui/initialize\" && msg.id) {\n post({ jsonrpc:\"2.0\", id:msg.id, result: {\n protocolVersion:\"2026-01-26\",\n serverInfo:{name:\"nimblebrain\",version:\"preview\"},\n capabilities:{openLinks:{},serverTools:{}},\n hostContext:{theme:dark?\"dark\":\"light\",primaryColor:\"#6366f1\",tokens:getTokens(dark)}\n }});\n return;\n }\n if (msg.method === \"ui/notifications/initialized\") return;\n\n // Tool calls — proxy via Vite middleware\n if (msg.method === \"tools/call\" && msg.id) {\n try {\n var r = await fetch(\"/__mcp\", {\n method:\"POST\", headers:{\"Content-Type\":\"application/json\"},\n body: JSON.stringify({jsonrpc:\"2.0\",id:msg.id,method:\"tools/call\",params:{name:msg.params.name,arguments:msg.params.arguments||{}}})\n });\n post(await r.json());\n } catch(err) {\n post({jsonrpc:\"2.0\",id:msg.id,error:{code:-32000,message:err.message}});\n }\n return;\n }\n\n // Log other messages\n if (msg.method === \"ui/chat\") console.log(\"[chat]\", msg.params?.message);\n else if (msg.method === \"ui/action\") console.log(\"[action]\", msg.params?.action, msg.params);\n else if (msg.method === \"ui/stateChanged\") { console.log(\"[state]\", msg.params?.state); post({jsonrpc:\"2.0\",method:\"ui/stateAcknowledged\",params:{truncated:false}}); }\n else if (msg.method === \"ui/keydown\") { /* ignore */ }\n else if (msg.method) console.log(\"[bridge]\", msg.method, msg);\n });\n\n document.getElementById(\"toggle\").onclick = function() {\n dark = !dark;\n document.body.style.background = dark ? \"#0f172a\" : \"#f1f5f9\";\n post({jsonrpc:\"2.0\",method:\"ui/themeChanged\",params:{mode:dark?\"dark\":\"light\",tokens:getTokens(dark)}});\n };\n </script>\n</body>\n</html>`;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimblebrain/synapse",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Agent-aware app SDK for the NimbleBrain platform",
5
5
  "type": "module",
6
6
  "exports": {