@nimblebrain/synapse 0.18.0 → 0.20.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.
Files changed (69) hide show
  1. package/README.md +32 -37
  2. package/dist/check-JEM5PN64.cjs +435 -0
  3. package/dist/check-JEM5PN64.cjs.map +1 -0
  4. package/dist/check-YDZUQT6A.js +429 -0
  5. package/dist/check-YDZUQT6A.js.map +1 -0
  6. package/dist/{chunk-VDQZQDU3.cjs → chunk-BNF352SA.cjs} +56 -145
  7. package/dist/chunk-BNF352SA.cjs.map +1 -0
  8. package/dist/{chunk-HKRTDGXN.js → chunk-DJS2FKBN.js} +21 -89
  9. package/dist/chunk-DJS2FKBN.js.map +1 -0
  10. package/dist/chunk-H3INUKA3.js +32 -0
  11. package/dist/chunk-H3INUKA3.js.map +1 -0
  12. package/dist/{chunk-FB7GPBEM.cjs → chunk-HQLSVUOR.cjs} +193 -314
  13. package/dist/chunk-HQLSVUOR.cjs.map +1 -0
  14. package/dist/chunk-ISV6HFQE.cjs +40 -0
  15. package/dist/chunk-ISV6HFQE.cjs.map +1 -0
  16. package/dist/{chunk-3YSXPBEQ.js → chunk-TR2YNWZ5.js} +190 -313
  17. package/dist/chunk-TR2YNWZ5.js.map +1 -0
  18. package/dist/{chunk-ZLASWV4N.js → chunk-UKP4W6DI.js} +56 -145
  19. package/dist/chunk-UKP4W6DI.js.map +1 -0
  20. package/dist/{chunk-LPWEQCZV.cjs → chunk-YSL7KJNU.cjs} +21 -92
  21. package/dist/chunk-YSL7KJNU.cjs.map +1 -0
  22. package/dist/codegen/cli.cjs +38 -1
  23. package/dist/codegen/cli.cjs.map +1 -1
  24. package/dist/codegen/cli.js +38 -1
  25. package/dist/codegen/cli.js.map +1 -1
  26. package/dist/codegen/index.d.cts +1 -1
  27. package/dist/codegen/index.d.ts +1 -1
  28. package/dist/connect.iife.global.js +66 -43
  29. package/dist/{detect-BHYg26_d.d.cts → detect-DVGaL2bH.d.cts} +39 -33
  30. package/dist/{detect-Bf8Q_0dK.d.ts → detect-DVGaL2bH.d.ts} +39 -33
  31. package/dist/host/index.cjs +17 -17
  32. package/dist/host/index.d.cts +11 -27
  33. package/dist/host/index.d.ts +11 -27
  34. package/dist/host/index.js +2 -2
  35. package/dist/index.cjs +52 -17
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +104 -31
  38. package/dist/index.d.ts +104 -31
  39. package/dist/index.js +34 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/react/index.cjs +11 -10
  42. package/dist/react/index.cjs.map +1 -1
  43. package/dist/react/index.d.cts +49 -12
  44. package/dist/react/index.d.ts +49 -12
  45. package/dist/react/index.js +6 -5
  46. package/dist/react/index.js.map +1 -1
  47. package/dist/{server-QIYIJHD5.cjs → server-BR5MNFOB.cjs} +40 -29
  48. package/dist/server-BR5MNFOB.cjs.map +1 -0
  49. package/dist/{server-5N76YCWC.js → server-OOEEWAEG.js} +40 -30
  50. package/dist/server-OOEEWAEG.js.map +1 -0
  51. package/dist/synapse-runtime.iife.global.js +66 -43
  52. package/dist/synapse-ui.iife.global.js +4 -4
  53. package/dist/{types-uEO4VFJ2.d.cts → types-BxPfGHKO.d.cts} +48 -125
  54. package/dist/{types-uEO4VFJ2.d.ts → types-BxPfGHKO.d.ts} +48 -125
  55. package/dist/vite/index.cjs +77 -33
  56. package/dist/vite/index.cjs.map +1 -1
  57. package/dist/vite/index.d.cts +5 -1
  58. package/dist/vite/index.d.ts +5 -1
  59. package/dist/vite/index.js +77 -33
  60. package/dist/vite/index.js.map +1 -1
  61. package/package.json +12 -6
  62. package/dist/chunk-3YSXPBEQ.js.map +0 -1
  63. package/dist/chunk-FB7GPBEM.cjs.map +0 -1
  64. package/dist/chunk-HKRTDGXN.js.map +0 -1
  65. package/dist/chunk-LPWEQCZV.cjs.map +0 -1
  66. package/dist/chunk-VDQZQDU3.cjs.map +0 -1
  67. package/dist/chunk-ZLASWV4N.js.map +0 -1
  68. package/dist/server-5N76YCWC.js.map +0 -1
  69. package/dist/server-QIYIJHD5.cjs.map +0 -1
@@ -1,5 +1,17 @@
1
- import { detectHost, foldFontFaces, applyTheme, fontFacesKey, extractTheme } from './chunk-HKRTDGXN.js';
2
- import { INITIALIZE_METHOD, LATEST_PROTOCOL_VERSION, HOST_CONTEXT_CHANGED_METHOD, INITIALIZED_METHOD, MESSAGE_METHOD, OPEN_LINK_METHOD, RESOURCE_TEARDOWN_METHOD, TOOL_CANCELLED_METHOD, TOOL_INPUT_PARTIAL_METHOD, TOOL_INPUT_METHOD, TOOL_RESULT_METHOD } from '@modelcontextprotocol/ext-apps';
1
+ import { extractHostFontCss, applyTheme, extractTheme } from './chunk-DJS2FKBN.js';
2
+ import { TOOLS_CALL_METHOD, NIMBLEBRAIN_EXTENSIONS, resolveEventMethod, KEYDOWN_METHOD } from './chunk-H3INUKA3.js';
3
+ import { App, TOOL_INPUT_METHOD, TOOL_INPUT_PARTIAL_METHOD, TOOL_RESULT_METHOD, TOOL_CANCELLED_METHOD, applyHostFonts, HOST_CONTEXT_CHANGED_METHOD, RESOURCE_TEARDOWN_METHOD, SIZE_CHANGED_METHOD } from '@modelcontextprotocol/ext-apps';
4
+ import { CallToolResultSchema, ResultSchema } from '@modelcontextprotocol/sdk/types.js';
5
+
6
+ // src/errors.ts
7
+ var HostCapabilityError = class extends Error {
8
+ capability;
9
+ constructor(feature, capability) {
10
+ super(`${feature} is not supported in this host: it did not declare ${capability}`);
11
+ this.name = "HostCapabilityError";
12
+ this.capability = capability;
13
+ }
14
+ };
3
15
 
4
16
  // src/internals.ts
5
17
  var INTERNALS = /* @__PURE__ */ new WeakMap();
@@ -63,14 +75,18 @@ function parseCallToolResult(result) {
63
75
  }
64
76
 
65
77
  // src/task-handle.ts
66
- var TOOLS_CALL_METHOD = "tools/call";
78
+ var TASKS_EXTENSION_ID = "io.modelcontextprotocol/tasks";
79
+ function readHostTasksCapability(capabilities) {
80
+ const entry = capabilities?.experimental?.[TASKS_EXTENSION_ID];
81
+ return entry && typeof entry === "object" && !Array.isArray(entry) ? entry : void 0;
82
+ }
67
83
  var TASKS_GET_METHOD = "tasks/get";
68
84
  var TASKS_RESULT_METHOD = "tasks/result";
69
85
  var TASKS_CANCEL_METHOD = "tasks/cancel";
70
86
  var TASKS_STATUS_NOTIFICATION_METHOD = "notifications/tasks/status";
71
- function createTaskStatusRouter(transport) {
87
+ function createTaskStatusRouter(subscribe) {
72
88
  const listeners = /* @__PURE__ */ new Map();
73
- const unsub = transport.onMessage(TASKS_STATUS_NOTIFICATION_METHOD, (rawParams) => {
89
+ const unsub = subscribe(TASKS_STATUS_NOTIFICATION_METHOD, (rawParams) => {
74
90
  if (!rawParams) return;
75
91
  const params = rawParams;
76
92
  const taskId = params.taskId;
@@ -109,18 +125,17 @@ async function callToolAsTask(app, toolName, args, options) {
109
125
  const deps = internalsFor(app);
110
126
  const hostTasks = deps.hostTasksCapability;
111
127
  if (!hostTasks?.requests?.tools?.call) {
112
- throw new Error(
113
- "callToolAsTask: host did not advertise tasks.requests.tools.call in its capabilities. Per MCP 2025-11-25 \xA7, requestors MUST NOT task-augment a tools/call without matching receiver capability. Fall back to `app.callTool`."
128
+ throw new HostCapabilityError(
129
+ "callToolAsTask",
130
+ `${TASKS_EXTENSION_ID} with requests.tools.call`
114
131
  );
115
132
  }
116
133
  const taskParam = {};
117
134
  if (options?.ttl !== void 0) taskParam.ttl = options.ttl;
118
- const crossServer = options?.internal ?? deps.internalApp;
119
135
  const callParams = {
120
136
  name: toolName,
121
137
  arguments: args ?? {},
122
- task: taskParam,
123
- ...crossServer ? { server: deps.appName } : {}
138
+ task: taskParam
124
139
  };
125
140
  const raw = await deps.request(
126
141
  TOOLS_CALL_METHOD,
@@ -232,32 +247,18 @@ function parseToolResultParams(params) {
232
247
  }
233
248
  return { content: rawContent ?? null, structuredContent: null, raw };
234
249
  }
235
- var DATA_CHANGED_METHOD = "synapse/data-changed";
236
- var ACTION_METHOD = "synapse/action";
237
- var REQUEST_FILE_METHOD = "synapse/request-file";
238
- var DOWNLOAD_FILE_METHOD = "synapse/download-file";
239
- var EVENT_MAP = {
240
- "tool-result": TOOL_RESULT_METHOD,
241
- "tool-input": TOOL_INPUT_METHOD,
242
- "tool-input-partial": TOOL_INPUT_PARTIAL_METHOD,
243
- "tool-cancelled": TOOL_CANCELLED_METHOD,
244
- teardown: RESOURCE_TEARDOWN_METHOD
245
- };
246
- function resolveEventMethod(name) {
247
- return EVENT_MAP[name] ?? name;
248
- }
249
250
 
250
251
  // src/keyboard.ts
251
252
  var KeyboardForwarder = class {
252
253
  listener;
253
254
  destroyed = false;
254
- constructor(transport, customKeys) {
255
+ constructor(send, customKeys) {
255
256
  const config = customKeys ?? null;
256
257
  this.listener = (event) => {
257
258
  if (this.destroyed) return;
258
259
  if (this.shouldForward(event, config)) {
259
260
  event.preventDefault();
260
- transport.send("synapse/keydown", {
261
+ send(KEYDOWN_METHOD, {
261
262
  key: event.key,
262
263
  ctrlKey: event.ctrlKey,
263
264
  metaKey: event.metaKey,
@@ -289,271 +290,125 @@ var KeyboardForwarder = class {
289
290
  return false;
290
291
  }
291
292
  };
292
-
293
- // src/resize.ts
294
293
  function createResizer(send, autoResize) {
295
294
  let destroyed = false;
296
295
  let observer = null;
297
- let rafId = null;
298
296
  function measureAndSend() {
299
297
  if (destroyed) return;
300
298
  const width = document.body.scrollWidth;
301
299
  const height = document.body.scrollHeight;
302
- send("ui/notifications/size-changed", { width, height });
300
+ send(SIZE_CHANGED_METHOD, { width, height });
303
301
  }
304
302
  function resize(width, height) {
305
303
  if (destroyed) return;
306
304
  if (width !== void 0 && height !== void 0) {
307
- send("ui/notifications/size-changed", { width, height });
305
+ send(SIZE_CHANGED_METHOD, { width, height });
308
306
  } else {
309
307
  measureAndSend();
310
308
  }
311
309
  }
312
- if (autoResize && typeof ResizeObserver !== "undefined") {
313
- observer = new ResizeObserver(() => {
314
- if (destroyed) return;
315
- if (rafId !== null) cancelAnimationFrame(rafId);
316
- rafId = requestAnimationFrame(() => {
317
- rafId = null;
318
- measureAndSend();
319
- });
320
- });
321
- observer.observe(document.body);
322
- }
323
310
  function destroy() {
324
311
  if (destroyed) return;
325
312
  destroyed = true;
326
- if (rafId !== null) cancelAnimationFrame(rafId);
327
313
  observer?.disconnect();
328
314
  observer = null;
329
315
  }
330
316
  return { resize, measureAndSend, destroy };
331
317
  }
332
318
 
333
- // src/transport.ts
334
- var SynapseTransport = class {
335
- counter = 0;
336
- destroyed = false;
337
- pending = /* @__PURE__ */ new Map();
338
- handlers = /* @__PURE__ */ new Map();
339
- listener;
340
- constructor() {
341
- this.listener = (event) => this.handleMessage(event);
342
- window.addEventListener("message", this.listener);
343
- }
344
- send(method, params) {
345
- if (this.destroyed) return;
346
- const msg = {
347
- jsonrpc: "2.0",
348
- method,
349
- ...params !== void 0 && { params }
350
- };
351
- window.parent.postMessage(msg, "*");
352
- }
353
- request(method, params) {
354
- if (this.destroyed) {
355
- return Promise.reject(new Error("Transport destroyed"));
356
- }
357
- const id = `syn-${++this.counter}`;
358
- const msg = {
359
- jsonrpc: "2.0",
360
- method,
361
- id,
362
- ...params !== void 0 && { params }
363
- };
364
- return new Promise((resolve, reject) => {
365
- this.pending.set(id, { resolve, reject });
366
- window.parent.postMessage(msg, "*");
367
- });
368
- }
369
- onMessage(method, callback) {
370
- if (!this.handlers.has(method)) {
371
- this.handlers.set(method, /* @__PURE__ */ new Set());
372
- }
373
- this.handlers.get(method)?.add(callback);
374
- return () => {
375
- const set = this.handlers.get(method);
376
- if (set) {
377
- set.delete(callback);
378
- if (set.size === 0) {
379
- this.handlers.delete(method);
380
- }
381
- }
382
- };
383
- }
384
- destroy() {
385
- if (this.destroyed) return;
386
- this.destroyed = true;
387
- window.removeEventListener("message", this.listener);
388
- const error = new Error("Transport destroyed");
389
- for (const entry of this.pending.values()) {
390
- entry.reject(error);
391
- }
392
- this.pending.clear();
393
- this.handlers.clear();
394
- }
395
- handleMessage(event) {
396
- if (this.destroyed) return;
397
- const data = event.data;
398
- if (!data || data.jsonrpc !== "2.0") return;
399
- if ("id" in data && data.id && !("method" in data)) {
400
- const response = data;
401
- const entry = this.pending.get(response.id);
402
- if (!entry) return;
403
- this.pending.delete(response.id);
404
- if (response.error) {
405
- const err = new Error(response.error.message);
406
- err.code = response.error.code;
407
- err.data = response.error.data;
408
- entry.reject(err);
409
- } else {
410
- entry.resolve(response.result);
411
- }
412
- return;
413
- }
414
- if ("method" in data && !("id" in data && data.id)) {
415
- const notification = data;
416
- const set = this.handlers.get(notification.method);
417
- if (set) {
418
- for (const handler of set) {
419
- handler(notification.params);
420
- }
421
- }
422
- }
423
- }
424
- };
425
-
426
319
  // src/connect.ts
427
- var READ_RESOURCE_METHOD = "resources/read";
428
- var UPDATE_MODEL_CONTEXT_METHOD = "ui/update-model-context";
320
+ var NIMBLEBRAIN_HOST = "nimblebrain";
321
+ var CHAT_CONTEXT_META_KEY = "ai.nimblebrain/context";
322
+ var NO_DEADLINE = { timeout: 2147483647 };
323
+ var MAPPED_EVENTS = [
324
+ ["toolinput", TOOL_INPUT_METHOD],
325
+ ["toolinputpartial", TOOL_INPUT_PARTIAL_METHOD],
326
+ ["toolresult", TOOL_RESULT_METHOD],
327
+ ["toolcancelled", TOOL_CANCELLED_METHOD]
328
+ ];
429
329
  async function connect(options) {
430
- const { name, version, autoResize = false, internal = false, forwardKeys } = options;
431
- const transport = new SynapseTransport();
330
+ const { name, version, autoResize = false, forwardKeys } = options;
432
331
  let destroyed = false;
433
- let hostContext = {};
434
332
  let hostInfo = { name: "unknown", version: "unknown" };
435
333
  let isNimbleBrainHost = false;
436
334
  let toolInfo = null;
437
335
  let containerDimensions = null;
336
+ let hostCapabilities = {};
438
337
  let hostTasksCapability;
439
338
  let keyboard = null;
440
- let fontFaces;
441
- function resolveTheme() {
442
- const theme = extractTheme(hostContext);
443
- return fontFaces ? { ...theme, fontFaces } : theme;
444
- }
445
- const handlers = /* @__PURE__ */ new Map();
446
- const themeCallbacks = /* @__PURE__ */ new Set();
447
- const hostContextCallbacks = /* @__PURE__ */ new Set();
448
- const dataCallbacks = /* @__PURE__ */ new Set();
449
- const actionCallbacks = /* @__PURE__ */ new Set();
450
- const resizer = createResizer((method, params) => transport.send(method, params), autoResize);
451
- resizer.measureAndSend();
339
+ let appliedTheme = { mode: "light", tokens: {} };
452
340
  const appCapabilities = {
453
341
  tasks: {
454
342
  cancel: {},
455
343
  requests: { tools: { call: {} } }
456
344
  }
457
345
  };
458
- const initParams = {
459
- protocolVersion: LATEST_PROTOCOL_VERSION,
460
- appInfo: { name, version },
461
- appCapabilities
462
- };
463
- const result = await transport.request(
464
- INITIALIZE_METHOD,
465
- initParams
346
+ const client = new App(
347
+ { name, version },
348
+ appCapabilities,
349
+ { autoResize }
466
350
  );
467
- if (result) {
468
- const detected = detectHost(result);
469
- isNimbleBrainHost = detected.isNimbleBrain;
470
- hostInfo = {
471
- name: result.hostInfo?.name ?? "unknown",
472
- version: result.hostInfo?.version ?? "unknown"
473
- };
474
- const rawTasks = result.hostCapabilities?.tasks;
475
- hostTasksCapability = rawTasks && typeof rawTasks === "object" && !Array.isArray(rawTasks) ? rawTasks : void 0;
476
- const ctx = result.hostContext;
477
- if (ctx) {
478
- hostContext = ctx;
479
- fontFaces = foldFontFaces(fontFaces, ctx);
480
- if (ctx.toolInfo && typeof ctx.toolInfo === "object") {
481
- toolInfo = { tool: ctx.toolInfo.tool ?? {} };
482
- }
483
- if (ctx.containerDimensions && typeof ctx.containerDimensions === "object") {
484
- containerDimensions = ctx.containerDimensions;
485
- }
486
- const theme = resolveTheme();
487
- applyTheme(theme.mode, theme.tokens, theme.fontFaces);
488
- }
351
+ function currentContext() {
352
+ return client.getHostContext() ?? {};
353
+ }
354
+ function resolveTheme() {
355
+ return extractTheme(currentContext());
489
356
  }
490
- if (forwardKeys && isNimbleBrainHost) {
491
- keyboard = new KeyboardForwarder(transport, forwardKeys === true ? void 0 : forwardKeys);
357
+ function applyFontsFrom(ctx) {
358
+ const css = extractHostFontCss(ctx);
359
+ if (css !== void 0 && typeof document !== "undefined") applyHostFonts(css);
492
360
  }
493
- function fanOutRaw(method, params) {
361
+ function applyResolvedTheme() {
362
+ const theme = resolveTheme();
363
+ applyTheme(theme.mode, theme.tokens);
364
+ appliedTheme = theme;
365
+ return theme;
366
+ }
367
+ const handlers = /* @__PURE__ */ new Map();
368
+ const themeCallbacks = /* @__PURE__ */ new Set();
369
+ const hostContextCallbacks = /* @__PURE__ */ new Set();
370
+ function fanOut(method, payload) {
494
371
  const set = handlers.get(method);
495
372
  if (!set) return;
496
- for (const handler of set) handler(params);
373
+ for (const handler of set) handler(payload);
374
+ }
375
+ function sendRaw(method, params) {
376
+ if (destroyed) return;
377
+ void client.notification({ method, params }).catch(() => {
378
+ });
379
+ }
380
+ function requestRaw(method, params) {
381
+ return client.request({ method, params }, ResultSchema, NO_DEADLINE);
382
+ }
383
+ for (const [event, method] of MAPPED_EVENTS) {
384
+ client.addEventListener(event, (params) => {
385
+ if (destroyed) return;
386
+ fanOut(
387
+ method,
388
+ method === TOOL_RESULT_METHOD ? parseToolResultParams(params) : params
389
+ );
390
+ });
497
391
  }
498
- transport.onMessage(HOST_CONTEXT_CHANGED_METHOD, (params) => {
392
+ client.addEventListener("hostcontextchanged", (params) => {
499
393
  if (destroyed) return;
500
- const ctx = params ?? {};
501
- const prevTheme = resolveTheme();
502
- hostContext = { ...hostContext, ...ctx };
503
- fontFaces = foldFontFaces(fontFaces, ctx);
504
- const nextTheme = resolveTheme();
505
- applyTheme(nextTheme.mode, nextTheme.tokens, nextTheme.fontFaces);
506
- for (const cb of hostContextCallbacks) cb(hostContext);
507
- fanOutRaw(HOST_CONTEXT_CHANGED_METHOD, params);
394
+ const prevTheme = appliedTheme;
395
+ applyFontsFrom(params);
396
+ const nextTheme = applyResolvedTheme();
397
+ const merged = currentContext();
398
+ for (const cb of hostContextCallbacks) cb(merged);
399
+ fanOut(HOST_CONTEXT_CHANGED_METHOD, params);
508
400
  if (!themesEqual(prevTheme, nextTheme)) {
509
401
  for (const cb of themeCallbacks) cb(nextTheme);
510
402
  }
511
403
  });
512
- transport.onMessage(DATA_CHANGED_METHOD, (params) => {
513
- if (destroyed) return;
514
- fanOutRaw(DATA_CHANGED_METHOD, params);
515
- if (!params) return;
516
- const event = {
517
- source: "agent",
518
- server: params.server ?? "",
519
- tool: params.tool ?? ""
520
- };
521
- for (const cb of dataCallbacks) cb(event);
522
- });
523
- transport.onMessage(ACTION_METHOD, (params) => {
404
+ client.onteardown = (params) => {
405
+ if (!destroyed) fanOut(RESOURCE_TEARDOWN_METHOD, params);
406
+ return {};
407
+ };
408
+ client.fallbackNotificationHandler = async (notification) => {
524
409
  if (destroyed) return;
525
- fanOutRaw(ACTION_METHOD, params);
526
- if (!params || typeof params.type !== "string") return;
527
- const action2 = {
528
- type: params.type,
529
- payload: params.payload ?? {},
530
- requiresConfirmation: params.requiresConfirmation === true,
531
- label: typeof params.label === "string" ? params.label : void 0
532
- };
533
- for (const cb of actionCallbacks) cb(action2);
534
- });
535
- const subscribedMethods = /* @__PURE__ */ new Set([
536
- HOST_CONTEXT_CHANGED_METHOD,
537
- DATA_CHANGED_METHOD,
538
- ACTION_METHOD
539
- ]);
540
- function ensureTransportSub(method) {
541
- if (subscribedMethods.has(method)) return;
542
- subscribedMethods.add(method);
543
- const isToolResult = method === TOOL_RESULT_METHOD;
544
- transport.onMessage(method, (params) => {
545
- if (destroyed) return;
546
- const set = handlers.get(method);
547
- if (!set) return;
548
- for (const handler of set) {
549
- if (isToolResult) {
550
- handler(parseToolResultParams(params));
551
- } else {
552
- handler(params);
553
- }
554
- }
555
- });
556
- }
410
+ fanOut(notification.method, notification.params);
411
+ };
557
412
  function subscribe(event, handler) {
558
413
  switch (event) {
559
414
  case "theme-changed":
@@ -566,21 +421,10 @@ async function connect(options) {
566
421
  return () => {
567
422
  hostContextCallbacks.delete(handler);
568
423
  };
569
- case "data-changed":
570
- dataCallbacks.add(handler);
571
- return () => {
572
- dataCallbacks.delete(handler);
573
- };
574
- case "action":
575
- actionCallbacks.add(handler);
576
- return () => {
577
- actionCallbacks.delete(handler);
578
- };
579
424
  default: {
580
425
  const method = resolveEventMethod(event);
581
426
  if (!handlers.has(method)) handlers.set(method, /* @__PURE__ */ new Set());
582
427
  handlers.get(method)?.add(handler);
583
- ensureTransportSub(method);
584
428
  return () => {
585
429
  const set = handlers.get(method);
586
430
  if (set) {
@@ -596,8 +440,36 @@ async function connect(options) {
596
440
  if (typeof handler === "function") subscribe(event, handler);
597
441
  }
598
442
  }
599
- transport.send(INITIALIZED_METHOD, {});
600
- const taskRouter = createTaskStatusRouter(transport);
443
+ await client.connect();
444
+ const hostVersion = client.getHostVersion();
445
+ hostInfo = {
446
+ name: hostVersion?.name ?? "unknown",
447
+ version: hostVersion?.version ?? "unknown"
448
+ };
449
+ isNimbleBrainHost = hostInfo.name === NIMBLEBRAIN_HOST;
450
+ hostCapabilities = client.getHostCapabilities() ?? {};
451
+ hostTasksCapability = readHostTasksCapability(hostCapabilities);
452
+ const initialContext = client.getHostContext();
453
+ if (initialContext) {
454
+ if (initialContext.toolInfo && typeof initialContext.toolInfo === "object") {
455
+ toolInfo = {
456
+ tool: initialContext.toolInfo.tool ?? {}
457
+ };
458
+ }
459
+ if (initialContext.containerDimensions && typeof initialContext.containerDimensions === "object") {
460
+ containerDimensions = initialContext.containerDimensions;
461
+ }
462
+ applyFontsFrom(initialContext);
463
+ applyResolvedTheme();
464
+ }
465
+ if (forwardKeys && hostCapabilities.experimental?.[NIMBLEBRAIN_EXTENSIONS.keydown.capability] !== void 0) {
466
+ keyboard = new KeyboardForwarder(sendRaw, forwardKeys === true ? void 0 : forwardKeys);
467
+ }
468
+ const resizer = createResizer(sendRaw);
469
+ if (!autoResize) resizer.measureAndSend();
470
+ const taskRouter = createTaskStatusRouter(
471
+ (method, handler) => subscribe(method, handler)
472
+ );
601
473
  const app = {
602
474
  get theme() {
603
475
  return resolveTheme();
@@ -606,7 +478,10 @@ async function connect(options) {
606
478
  return { ...hostInfo };
607
479
  },
608
480
  get hostContext() {
609
- return hostContext;
481
+ return currentContext();
482
+ },
483
+ get hostCapabilities() {
484
+ return hostCapabilities;
610
485
  },
611
486
  get isNimbleBrainHost() {
612
487
  return isNimbleBrainHost;
@@ -631,52 +506,62 @@ async function connect(options) {
631
506
  },
632
507
  openLink(url) {
633
508
  if (destroyed) return;
509
+ if (!hostCapabilities.openLinks) {
510
+ window.open(url, "_blank", "noopener");
511
+ return;
512
+ }
634
513
  const params = { url };
635
- transport.request(OPEN_LINK_METHOD, params).catch(() => {
514
+ client.openLink(params, NO_DEADLINE).catch(() => {
636
515
  window.open(url, "_blank", "noopener");
637
516
  });
638
517
  },
639
518
  updateModelContext(state, summary) {
640
- if (destroyed) return;
519
+ if (destroyed || !hostCapabilities.updateModelContext) return;
641
520
  const params = {
642
521
  structuredContent: state,
643
522
  ...summary !== void 0 && {
644
523
  content: [{ type: "text", text: summary }]
645
524
  }
646
525
  };
647
- transport.send(UPDATE_MODEL_CONTEXT_METHOD, params);
526
+ client.updateModelContext(params, NO_DEADLINE).catch(() => {
527
+ });
648
528
  },
649
- async callTool(toolName, args, callOptions) {
650
- const server = callOptions?.server ?? (internal ? name : void 0);
529
+ async callTool(toolName, args) {
530
+ if (!hostCapabilities.serverTools) {
531
+ throw new HostCapabilityError("callTool", "serverTools");
532
+ }
651
533
  const params = {
652
534
  name: toolName,
653
- arguments: args ?? {},
654
- ...server !== void 0 && { server }
535
+ arguments: args ?? {}
655
536
  };
656
- const raw = await transport.request(TOOLS_CALL_METHOD, params);
537
+ const raw = await client.request(
538
+ { method: TOOLS_CALL_METHOD, params },
539
+ CallToolResultSchema,
540
+ NO_DEADLINE
541
+ );
657
542
  return parseToolResult(raw);
658
543
  },
659
544
  async readServerResource(params) {
660
- const raw = await transport.request(
661
- READ_RESOURCE_METHOD,
662
- params
663
- );
664
- return raw;
545
+ if (!hostCapabilities.serverResources) {
546
+ throw new HostCapabilityError("readServerResource", "serverResources");
547
+ }
548
+ return await client.readServerResource(params, NO_DEADLINE);
665
549
  },
666
550
  sendMessage(text, context) {
667
- if (destroyed) return;
551
+ if (destroyed || !hostCapabilities.message) return;
668
552
  const textBlock = {
669
553
  type: "text",
670
554
  text,
671
- // `_meta.context` is a NimbleBrain convention; other hosts ignore it,
555
+ // The chat context is a NimbleBrain host field; other hosts ignore it,
672
556
  // but there is no reason to spend the bytes off one.
673
- ...isNimbleBrainHost && context && { _meta: { context } }
557
+ ...isNimbleBrainHost && context && { _meta: { [CHAT_CONTEXT_META_KEY]: context } }
674
558
  };
675
559
  const params = {
676
560
  role: "user",
677
561
  content: [textBlock]
678
562
  };
679
- transport.send(MESSAGE_METHOD, params);
563
+ client.sendMessage(params, NO_DEADLINE).catch(() => {
564
+ });
680
565
  },
681
566
  destroy() {
682
567
  if (destroyed) return;
@@ -687,34 +572,28 @@ async function connect(options) {
687
572
  handlers.clear();
688
573
  themeCallbacks.clear();
689
574
  hostContextCallbacks.clear();
690
- dataCallbacks.clear();
691
- actionCallbacks.clear();
692
- transport.destroy();
575
+ void client.close();
693
576
  }
694
577
  };
695
578
  registerInternals(app, {
696
579
  send(method, params) {
697
- if (destroyed) return;
698
- transport.send(method, params);
580
+ sendRaw(method, params);
699
581
  },
700
582
  request(method, params) {
701
- return transport.request(method, params);
583
+ return requestRaw(method, params);
702
584
  },
703
585
  onMessage(method, handler) {
704
- return transport.onMessage(method, handler);
586
+ return subscribe(method, handler);
705
587
  },
706
588
  taskRouter,
707
589
  get hostTasksCapability() {
708
590
  return hostTasksCapability;
709
- },
710
- appName: name,
711
- internalApp: internal
591
+ }
712
592
  });
713
593
  return app;
714
594
  }
715
595
  function themesEqual(a, b) {
716
596
  if (a.mode !== b.mode) return false;
717
- if (fontFacesKey(a.fontFaces) !== fontFacesKey(b.fontFaces)) return false;
718
597
  const aKeys = Object.keys(a.tokens);
719
598
  const bKeys = Object.keys(b.tokens);
720
599
  if (aKeys.length !== bKeys.length) return false;
@@ -725,57 +604,55 @@ function themesEqual(a, b) {
725
604
  }
726
605
 
727
606
  // src/extensions.ts
607
+ function hostSupports(app, extension) {
608
+ const { capability } = NIMBLEBRAIN_EXTENSIONS[extension];
609
+ return app.hostCapabilities.experimental?.[capability] !== void 0;
610
+ }
728
611
  var DEFAULT_MAX_FILE_SIZE = 26214400;
729
612
  function action(app, name, params) {
730
- if (!app.isNimbleBrainHost) return;
731
- internalsFor(app).send(ACTION_METHOD, { action: name, ...params });
732
- }
733
- function downloadFile(app, filename, content, mimeType) {
734
- const resolvedMime = mimeType || (content instanceof Blob ? content.type : "") || "application/octet-stream";
735
- const blob = content instanceof Blob ? content : new Blob([content], { type: resolvedMime });
736
- internalsFor(app).send(DOWNLOAD_FILE_METHOD, {
737
- data: blob,
738
- filename,
739
- mimeType: resolvedMime
740
- });
613
+ if (!hostSupports(app, "action")) return;
614
+ internalsFor(app).send(NIMBLEBRAIN_EXTENSIONS.action.method, { action: name, ...params });
741
615
  }
742
616
  async function pickFile(app, options) {
743
- if (!app.isNimbleBrainHost) {
744
- throw new Error("pickFile is not supported in this host");
745
- }
746
- const result = await requestFile(app, options, false);
747
- if (result === null) return null;
748
- return Array.isArray(result) ? result[0] ?? null : result;
617
+ requireRequestFile("pickFile", app);
618
+ const files = await requestFile(app, options, false);
619
+ return files[0] ?? null;
749
620
  }
750
621
  async function pickFiles(app, options) {
751
- if (!app.isNimbleBrainHost) {
752
- throw new Error("pickFiles is not supported in this host");
622
+ requireRequestFile("pickFiles", app);
623
+ return await requestFile(app, options, true);
624
+ }
625
+ function requireRequestFile(feature, app) {
626
+ if (!hostSupports(app, "requestFile")) {
627
+ throw new HostCapabilityError(feature, NIMBLEBRAIN_EXTENSIONS.requestFile.capability);
753
628
  }
754
- const result = await requestFile(app, options, true);
755
- if (result === null) return [];
756
- return Array.isArray(result) ? result : [result];
757
629
  }
758
630
  async function requestFile(app, options, multiple) {
759
- const result = await internalsFor(app).request(REQUEST_FILE_METHOD, {
631
+ const result = await internalsFor(app).request(NIMBLEBRAIN_EXTENSIONS.requestFile.method, {
760
632
  accept: options?.accept,
761
633
  maxSize: options?.maxSize ?? DEFAULT_MAX_FILE_SIZE,
762
634
  multiple
763
635
  });
764
- if (result == null) return null;
765
- if (Array.isArray(result)) {
766
- return result.map(validateFileResult);
636
+ const files = result?.files;
637
+ if (files === void 0) {
638
+ throw new Error(
639
+ "ai.nimblebrain/request-file returned no `files`. The host is older than this SDK targets: it answers the picker with a bare array or `null`, which a spec-compliant client cannot parse."
640
+ );
641
+ }
642
+ if (!Array.isArray(files)) {
643
+ throw new Error("ai.nimblebrain/request-file returned a `files` field that is not an array.");
767
644
  }
768
- return validateFileResult(result);
645
+ return files.map(validateFileResult);
769
646
  }
770
647
  function validateFileResult(value) {
771
648
  if (typeof value !== "object" || value === null || typeof value.id !== "string") {
772
649
  throw new Error(
773
- "synapse/request-file returned a result without a string `id`. The host appears to be on a version older than this SDK targets \u2014 @nimblebrain/synapse 0.8.0+ requires a host with POST /v1/resources (NimbleBrain \u2265 the version that ships PR #93)."
650
+ "ai.nimblebrain/request-file returned a result without a string `id`. The host appears to be on a version older than this SDK targets \u2014 @nimblebrain/synapse 0.8.0+ requires a host with POST /v1/resources (NimbleBrain \u2265 the version that ships PR #93)."
774
651
  );
775
652
  }
776
653
  return value;
777
654
  }
778
655
 
779
- export { action, callToolAsTask, connect, downloadFile, pickFile, pickFiles };
780
- //# sourceMappingURL=chunk-3YSXPBEQ.js.map
781
- //# sourceMappingURL=chunk-3YSXPBEQ.js.map
656
+ export { HostCapabilityError, action, callToolAsTask, connect, hostSupports, internalsFor, pickFile, pickFiles };
657
+ //# sourceMappingURL=chunk-TR2YNWZ5.js.map
658
+ //# sourceMappingURL=chunk-TR2YNWZ5.js.map