@get-bb/plugin-sdk 0.4.8 → 0.4.10

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.
@@ -251,6 +251,137 @@ function collectComposerCustomization(registration, seenIds, onRejected) {
251
251
  }
252
252
  }
253
253
 
254
+ // src/internal/file-navigation-validation.ts
255
+ var FILE_PATH_MAX_LENGTH = 32768;
256
+ var WINDOWS_DRIVE_ABSOLUTE_PATH = /^[A-Za-z]:[\\/]/u;
257
+ var WINDOWS_UNC_ABSOLUTE_PATH = /^\\\\/u;
258
+ function isJsonObject(value) {
259
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
260
+ return false;
261
+ }
262
+ const prototype = Object.getPrototypeOf(value);
263
+ return prototype === Object.prototype || prototype === null;
264
+ }
265
+ function hasExactKeys(value, keys) {
266
+ const actualKeys = Object.keys(value);
267
+ return actualKeys.length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
268
+ }
269
+ function isNonEmptyIdentity(value) {
270
+ return typeof value === "string" && value.length > 0 && value.length <= FILE_PATH_MAX_LENGTH && value.trim() === value;
271
+ }
272
+ function hasControlCharacter(value) {
273
+ for (const character of value) {
274
+ const codePoint = character.codePointAt(0);
275
+ if (codePoint !== void 0 && codePoint < 32) return true;
276
+ }
277
+ return false;
278
+ }
279
+ function hasUnpairedSurrogate(value) {
280
+ for (let index = 0; index < value.length; index += 1) {
281
+ const codeUnit = value.charCodeAt(index);
282
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
283
+ if (index + 1 >= value.length) return true;
284
+ const nextCodeUnit = value.charCodeAt(index + 1);
285
+ if (nextCodeUnit < 56320 || nextCodeUnit > 57343) return true;
286
+ index += 1;
287
+ } else if (codeUnit >= 56320 && codeUnit <= 57343) {
288
+ return true;
289
+ }
290
+ }
291
+ return false;
292
+ }
293
+ function isValidPathSegment(segment) {
294
+ return segment.length > 0 && segment !== "." && segment !== "..";
295
+ }
296
+ function isPositiveSafeInteger(value) {
297
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
298
+ }
299
+ function isValidRelativeFilePath(value) {
300
+ if (typeof value !== "string" || value.length === 0 || value.length > FILE_PATH_MAX_LENGTH || value.trim() !== value || value.includes("\\") || hasControlCharacter(value) || hasUnpairedSurrogate(value)) {
301
+ return false;
302
+ }
303
+ return value.split("/").every(isValidPathSegment);
304
+ }
305
+ function isValidAbsoluteHostFilePath(value) {
306
+ if (typeof value !== "string" || value.length === 0 || value.length > FILE_PATH_MAX_LENGTH || value.trim() !== value || hasControlCharacter(value) || hasUnpairedSurrogate(value)) {
307
+ return false;
308
+ }
309
+ if (value.startsWith("/") && !value.startsWith("//")) {
310
+ const segments = value.slice(1).split("/");
311
+ return segments.length > 0 && segments.every(isValidPathSegment);
312
+ }
313
+ if (WINDOWS_DRIVE_ABSOLUTE_PATH.test(value)) {
314
+ const segments = value.slice(3).split(/[\\/]/u);
315
+ return segments.length > 0 && segments.every(isValidPathSegment);
316
+ }
317
+ if (WINDOWS_UNC_ABSOLUTE_PATH.test(value)) {
318
+ const segments = value.slice(2).split(/[\\/]/u);
319
+ return segments.length >= 3 && segments.every(isValidPathSegment);
320
+ }
321
+ return false;
322
+ }
323
+ function normalizeExperimentalLiveFileTarget(value) {
324
+ if (!isJsonObject(value) || typeof value.kind !== "string") return null;
325
+ switch (value.kind) {
326
+ case "workspace":
327
+ if (!hasExactKeys(value, ["kind", "environmentId", "path"]) || !isNonEmptyIdentity(value.environmentId) || !isValidRelativeFilePath(value.path)) {
328
+ return null;
329
+ }
330
+ return {
331
+ kind: value.kind,
332
+ environmentId: value.environmentId,
333
+ path: value.path
334
+ };
335
+ case "host":
336
+ if (!hasExactKeys(value, ["kind", "hostId", "path"]) || !isNonEmptyIdentity(value.hostId) || !isValidAbsoluteHostFilePath(value.path)) {
337
+ return null;
338
+ }
339
+ return { kind: value.kind, hostId: value.hostId, path: value.path };
340
+ case "thread-storage":
341
+ if (!hasExactKeys(value, ["kind", "threadId", "path"]) || !isNonEmptyIdentity(value.threadId) || !isValidRelativeFilePath(value.path)) {
342
+ return null;
343
+ }
344
+ return { kind: value.kind, threadId: value.threadId, path: value.path };
345
+ default:
346
+ return null;
347
+ }
348
+ }
349
+ function normalizeExperimentalFileLocation(value) {
350
+ if (value === null) return null;
351
+ if (!isJsonObject(value) || typeof value.kind !== "string") return void 0;
352
+ switch (value.kind) {
353
+ case "line":
354
+ if (!hasExactKeys(value, ["kind", "line", "column"]) || !isPositiveSafeInteger(value.line) || value.column !== null && !isPositiveSafeInteger(value.column)) {
355
+ return void 0;
356
+ }
357
+ return {
358
+ kind: value.kind,
359
+ line: value.line,
360
+ column: value.column
361
+ };
362
+ case "range":
363
+ if (!hasExactKeys(value, ["kind", "startLine", "endLine"]) || !isPositiveSafeInteger(value.startLine) || !isPositiveSafeInteger(value.endLine) || value.endLine < value.startLine) {
364
+ return void 0;
365
+ }
366
+ return {
367
+ kind: value.kind,
368
+ startLine: value.startLine,
369
+ endLine: value.endLine
370
+ };
371
+ default:
372
+ return void 0;
373
+ }
374
+ }
375
+ function normalizeExperimentalFileOpenOptions(value) {
376
+ if (!isJsonObject(value) || !hasExactKeys(value, ["target", "location"])) {
377
+ return null;
378
+ }
379
+ const target = normalizeExperimentalLiveFileTarget(value.target);
380
+ const location = normalizeExperimentalFileLocation(value.location);
381
+ if (target === null || location === void 0) return null;
382
+ return { target, location };
383
+ }
384
+
254
385
  // src/internal/plugin-app-collector.ts
255
386
  function collectPluginAppRegistrations(definition, onComposerCustomizationRejected = (reason) => console.warn(reason)) {
256
387
  const collected = {
@@ -265,6 +396,8 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
265
396
  threadLists: [],
266
397
  threadHeaderActions: [],
267
398
  fileOpeners: [],
399
+ sourceCodeRenderers: [],
400
+ diffRenderers: [],
268
401
  messageDirectives: [],
269
402
  messageActions: [],
270
403
  providerIcons: [],
@@ -282,6 +415,8 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
282
415
  threadList: /* @__PURE__ */ new Set(),
283
416
  threadHeaderAction: /* @__PURE__ */ new Set(),
284
417
  fileOpener: /* @__PURE__ */ new Set(),
418
+ sourceCodeRenderer: /* @__PURE__ */ new Set(),
419
+ diffRenderer: /* @__PURE__ */ new Set(),
285
420
  messageDirective: /* @__PURE__ */ new Set(),
286
421
  messageAction: /* @__PURE__ */ new Set(),
287
422
  providerIcon: /* @__PURE__ */ new Set(),
@@ -320,6 +455,7 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
320
455
  const kind = "slots.navPanel";
321
456
  const id = requireSlotId(kind, registration?.id);
322
457
  requireUniqueId(kind, seenIds.navPanel, id);
458
+ const panelId = id;
323
459
  const path = requireNonEmptyString(kind, "path", registration.path);
324
460
  if (!PLUGIN_SLOT_ID_PATTERN.test(path)) {
325
461
  throw new Error(
@@ -355,8 +491,25 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
355
491
  `${fixedTabKind}: "layout" must be "padded" or "flush" when set`
356
492
  );
357
493
  }
494
+ const fixedTabPanelId = requireNonEmptyString(
495
+ fixedTabKind,
496
+ "panelId",
497
+ fixedTab?.panelId
498
+ );
499
+ if (fixedTabPanelId !== panelId) {
500
+ throw new Error(
501
+ `${fixedTabKind}: "panelId" must match its containing navPanel id ${JSON.stringify(panelId)}`
502
+ );
503
+ }
504
+ const experimentalTarget = fixedTab?.experimental_target;
505
+ if (experimentalTarget !== void 0 && (typeof experimentalTarget !== "object" || experimentalTarget === null || typeof Reflect.get(experimentalTarget, "validate") !== "function")) {
506
+ throw new Error(
507
+ `${fixedTabKind}: "experimental_target.validate" must be a function when set`
508
+ );
509
+ }
358
510
  return {
359
511
  id: id2,
512
+ panelId: fixedTabPanelId,
360
513
  title: requireNonEmptyString(
361
514
  fixedTabKind,
362
515
  "title",
@@ -368,7 +521,10 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
368
521
  fixedTab?.icon
369
522
  ),
370
523
  component: requireComponent(fixedTabKind, fixedTab?.component),
371
- ...layout === void 0 ? {} : { layout }
524
+ ...layout === void 0 ? {} : { layout },
525
+ ...experimentalTarget === void 0 ? {} : {
526
+ experimental_target: experimentalTarget
527
+ }
372
528
  };
373
529
  });
374
530
  })();
@@ -501,6 +657,38 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
501
657
  component: requireComponent(kind, registration.component)
502
658
  });
503
659
  },
660
+ experimental_sourceCodeRenderer(registration) {
661
+ const kind = "slots.experimental_sourceCodeRenderer";
662
+ const id = requireSlotId(kind, registration?.id);
663
+ requireUniqueId(kind, seenIds.sourceCodeRenderer, id);
664
+ const description = requireOptionalString(
665
+ kind,
666
+ "description",
667
+ registration.description
668
+ );
669
+ collected.sourceCodeRenderers.push({
670
+ id,
671
+ title: requireNonEmptyString(kind, "title", registration.title),
672
+ ...description !== void 0 ? { description } : {},
673
+ component: requireComponent(kind, registration.component)
674
+ });
675
+ },
676
+ experimental_diffRenderer(registration) {
677
+ const kind = "slots.experimental_diffRenderer";
678
+ const id = requireSlotId(kind, registration?.id);
679
+ requireUniqueId(kind, seenIds.diffRenderer, id);
680
+ const description = requireOptionalString(
681
+ kind,
682
+ "description",
683
+ registration.description
684
+ );
685
+ collected.diffRenderers.push({
686
+ id,
687
+ title: requireNonEmptyString(kind, "title", registration.title),
688
+ ...description !== void 0 ? { description } : {},
689
+ component: requireComponent(kind, registration.component)
690
+ });
691
+ },
504
692
  messageDirective(registration) {
505
693
  const kind = "slots.messageDirective";
506
694
  const id = requireMessageDirectiveId(kind, registration?.id);
@@ -643,6 +831,65 @@ function TestThreadChat({
643
831
  function TestMarkdown({ content, className }) {
644
832
  return /* @__PURE__ */ jsx("div", { "data-testid": "bb-markdown", className, children: content });
645
833
  }
834
+ function TestUrlLink({
835
+ href,
836
+ onClick,
837
+ rel,
838
+ target,
839
+ ...anchorProps
840
+ }) {
841
+ const navigate = useSlotEnv("experimental_UrlLink").navigate;
842
+ const normalizedTarget = target?.toLowerCase();
843
+ const opensNewBrowsingContext = normalizedTarget !== void 0 && normalizedTarget !== "" && normalizedTarget !== "_self" && normalizedTarget !== "_parent" && normalizedTarget !== "_top" && normalizedTarget !== "_unfencedtop";
844
+ const relTokens = rel?.split(/\s+/u).filter(Boolean) ?? [];
845
+ const normalizedRelTokens = relTokens.map((token) => token.toLowerCase());
846
+ const resolvedRel = opensNewBrowsingContext && !normalizedRelTokens.includes("opener") ? [
847
+ ...relTokens,
848
+ ...normalizedRelTokens.includes("noopener") ? [] : ["noopener"],
849
+ ...normalizedRelTokens.includes("noreferrer") ? [] : ["noreferrer"]
850
+ ].join(" ") : rel;
851
+ return /* @__PURE__ */ jsx(
852
+ "a",
853
+ {
854
+ ...anchorProps,
855
+ href,
856
+ target,
857
+ rel: resolvedRel,
858
+ onClick: (event) => {
859
+ onClick?.(event);
860
+ if (event.defaultPrevented || event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.currentTarget.hasAttribute("download") || event.currentTarget.hasAttribute("target")) {
861
+ return;
862
+ }
863
+ if (navigate.experimental_openUrl(href)) event.preventDefault();
864
+ }
865
+ }
866
+ );
867
+ }
868
+ function TestFileLink({
869
+ target,
870
+ location = null,
871
+ onClick,
872
+ ...anchorProps
873
+ }) {
874
+ const navigate = useSlotEnv("experimental_FileLink").navigate;
875
+ const options = normalizeExperimentalFileOpenOptions({ target, location });
876
+ const href = options === null ? void 0 : `./${encodeURIComponent(options.target.path)}`;
877
+ return /* @__PURE__ */ jsx(
878
+ "a",
879
+ {
880
+ ...anchorProps,
881
+ href,
882
+ onClick: (event) => {
883
+ onClick?.(event);
884
+ if (options === null || event.defaultPrevented || event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.currentTarget.hasAttribute("download")) {
885
+ return;
886
+ }
887
+ event.preventDefault();
888
+ navigate.experimental_openFilePreview(options);
889
+ }
890
+ }
891
+ );
892
+ }
646
893
  function TestNewThreadComposer({
647
894
  defaultProjectId,
648
895
  defaultProviderId,
@@ -710,6 +957,46 @@ function TestNewThreadComposer({
710
957
  }
711
958
  );
712
959
  }
960
+ function TestSourceCode({
961
+ content,
962
+ path,
963
+ overflow = "scroll",
964
+ highlightedLines = null,
965
+ className
966
+ }) {
967
+ return /* @__PURE__ */ jsx(
968
+ "pre",
969
+ {
970
+ "data-testid": "bb-source-code",
971
+ "data-path": path,
972
+ "data-overflow": overflow,
973
+ "data-highlighted-lines": highlightedLines === null ? "" : `${highlightedLines.start}-${highlightedLines.end}`,
974
+ className,
975
+ children: content
976
+ }
977
+ );
978
+ }
979
+ function TestDiff({
980
+ patch,
981
+ path,
982
+ view = "unified",
983
+ overflow = "scroll",
984
+ showLineNumbers = true,
985
+ className
986
+ }) {
987
+ return /* @__PURE__ */ jsx(
988
+ "pre",
989
+ {
990
+ "data-testid": "bb-diff",
991
+ "data-path": path,
992
+ "data-view": view,
993
+ "data-overflow": overflow,
994
+ "data-show-line-numbers": showLineNumbers ? "true" : "false",
995
+ className,
996
+ children: patch
997
+ }
998
+ );
999
+ }
713
1000
  var testPluginSdkApp = {
714
1001
  definePluginApp,
715
1002
  useRpc() {
@@ -753,6 +1040,30 @@ var testPluginSdkApp = {
753
1040
  useBbNavigate() {
754
1041
  return useSlotEnv("useBbNavigate").navigate;
755
1042
  },
1043
+ experimental_useAppPanel() {
1044
+ return useSlotEnv("experimental_useAppPanel").appPanel;
1045
+ },
1046
+ experimental_useFixedTabTarget(tab) {
1047
+ const store = useSlotEnv("experimental_useFixedTabTarget").fixedTabTarget;
1048
+ const state = useSyncExternalStore(
1049
+ store.subscribe,
1050
+ store.getSnapshot,
1051
+ store.getSnapshot
1052
+ );
1053
+ if (state === null || state.panelId !== tab.panelId || state.tabId !== tab.id || tab.experimental_target === void 0) {
1054
+ return null;
1055
+ }
1056
+ try {
1057
+ if (!tab.experimental_target.validate(state.target)) return null;
1058
+ } catch {
1059
+ return null;
1060
+ }
1061
+ return {
1062
+ clear: () => store.clear(state.sequence),
1063
+ sequence: state.sequence,
1064
+ target: state.target
1065
+ };
1066
+ },
756
1067
  useComposer() {
757
1068
  const composer = useSlotEnv("useComposer").composer;
758
1069
  const version = useSyncExternalStore(
@@ -771,7 +1082,11 @@ var testPluginSdkApp = {
771
1082
  },
772
1083
  ThreadChat: TestThreadChat,
773
1084
  Markdown: TestMarkdown,
1085
+ experimental_FileLink: TestFileLink,
1086
+ experimental_UrlLink: TestUrlLink,
774
1087
  experimental_NewThreadComposer: TestNewThreadComposer,
1088
+ experimental_SourceCode: TestSourceCode,
1089
+ experimental_Diff: TestDiff,
775
1090
  experimental_useSidebarThreads() {
776
1091
  return useSlotEnv("experimental_useSidebarThreads").sidebarThreads;
777
1092
  },
@@ -1008,6 +1323,70 @@ function renderSlot(registration, props, options = {}) {
1008
1323
  }
1009
1324
  };
1010
1325
  const navigateCalls = [];
1326
+ const experimental_fixedTabOpenCalls = [];
1327
+ let fixedTabTargetSnapshot = options.experimental_fixedTabTarget === void 0 ? null : {
1328
+ panelId: options.experimental_fixedTabTarget.panelId,
1329
+ sequence: 1,
1330
+ tabId: options.experimental_fixedTabTarget.tabId,
1331
+ target: strictJsonRoundTrip(
1332
+ options.experimental_fixedTabTarget.target,
1333
+ "fixed tab target"
1334
+ )
1335
+ };
1336
+ const fixedTabTargetListeners = /* @__PURE__ */ new Set();
1337
+ const fixedTabTarget = {
1338
+ getSnapshot: () => fixedTabTargetSnapshot,
1339
+ subscribe(listener) {
1340
+ fixedTabTargetListeners.add(listener);
1341
+ return () => fixedTabTargetListeners.delete(listener);
1342
+ },
1343
+ clear(sequence) {
1344
+ if (fixedTabTargetSnapshot?.sequence !== sequence) return;
1345
+ fixedTabTargetSnapshot = null;
1346
+ for (const listener of fixedTabTargetListeners) listener();
1347
+ }
1348
+ };
1349
+ const appPanel = {
1350
+ openFixedTab(panelOptions) {
1351
+ let target;
1352
+ if (panelOptions.target !== void 0) {
1353
+ try {
1354
+ target = strictJsonRoundTrip(
1355
+ panelOptions.target,
1356
+ "fixed tab open target"
1357
+ );
1358
+ } catch {
1359
+ return false;
1360
+ }
1361
+ if (panelOptions.tab.experimental_target === void 0) return false;
1362
+ try {
1363
+ if (!panelOptions.tab.experimental_target.validate(target)) {
1364
+ return false;
1365
+ }
1366
+ } catch {
1367
+ return false;
1368
+ }
1369
+ }
1370
+ const call = {
1371
+ surface: panelOptions.surface,
1372
+ panelId: panelOptions.tab.panelId,
1373
+ tabId: panelOptions.tab.id,
1374
+ ...target === void 0 ? {} : { target }
1375
+ };
1376
+ experimental_fixedTabOpenCalls.push(call);
1377
+ const accepted = options.experimental_openFixedTab?.(call) ?? false;
1378
+ if (accepted && target !== void 0) {
1379
+ fixedTabTargetSnapshot = {
1380
+ panelId: panelOptions.tab.panelId,
1381
+ sequence: (fixedTabTargetSnapshot?.sequence ?? 0) + 1,
1382
+ tabId: panelOptions.tab.id,
1383
+ target
1384
+ };
1385
+ for (const listener of fixedTabTargetListeners) listener();
1386
+ }
1387
+ return accepted;
1388
+ }
1389
+ };
1011
1390
  const sidebarActionCalls = [];
1012
1391
  const sidebarPullRequests = new Map(
1013
1392
  Object.entries(options.sidebarPullRequests ?? {})
@@ -1073,6 +1452,24 @@ function renderSlot(registration, props, options = {}) {
1073
1452
  options: panelOptions
1074
1453
  });
1075
1454
  return options.openThreadPanel?.(panelOptions) ?? false;
1455
+ },
1456
+ experimental_openUrl(url) {
1457
+ navigateCalls.push({ method: "experimental_openUrl", url });
1458
+ return options.openUrl?.(url) ?? false;
1459
+ },
1460
+ experimental_openFilePreview(fileOptions) {
1461
+ navigateCalls.push({
1462
+ method: "experimental_openFilePreview",
1463
+ options: fileOptions
1464
+ });
1465
+ return options.openFilePreview?.(fileOptions) ?? false;
1466
+ },
1467
+ experimental_openFileExternally(fileOptions) {
1468
+ navigateCalls.push({
1469
+ method: "experimental_openFileExternally",
1470
+ options: fileOptions
1471
+ });
1472
+ return options.openFileExternally?.(fileOptions) ?? false;
1076
1473
  }
1077
1474
  };
1078
1475
  const projectId = options.context?.projectId ?? null;
@@ -1174,6 +1571,9 @@ ${block}
1174
1571
  bbContext: { projectId, threadId },
1175
1572
  navigate,
1176
1573
  navigateCalls,
1574
+ appPanel,
1575
+ experimental_fixedTabOpenCalls,
1576
+ fixedTabTarget,
1177
1577
  composer,
1178
1578
  composerLog,
1179
1579
  sidebarThreads,
@@ -1229,6 +1629,7 @@ ${block}
1229
1629
  setComposerText,
1230
1630
  setComposerScope,
1231
1631
  navigateCalls,
1632
+ experimental_fixedTabOpenCalls,
1232
1633
  sidebarActionCalls,
1233
1634
  composer: composerLog,
1234
1635
  behavior: {
@@ -1240,6 +1641,7 @@ ${block}
1240
1641
  inspection: {
1241
1642
  rpcCalls,
1242
1643
  navigateCalls,
1644
+ experimental_fixedTabOpenCalls,
1243
1645
  sidebarActionCalls,
1244
1646
  composer: composerLog
1245
1647
  },
@@ -66,6 +66,7 @@ var PLUGIN_AGENT_DYNAMIC_INSTRUCTIONS_MAX_CHARS = 4096;
66
66
  var PLUGIN_AGENT_TOOL_PARAMETERS_MAX_BYTES = 128 * 1024;
67
67
  var MENTION_PROVIDER_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
68
68
  var PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,63}$/;
69
+ var PLUGIN_PROVIDER_BRIDGE_OPTIONS_MAX_BYTES = 64 * 1024;
69
70
  var SETTING_KEY_PATTERN = /^[a-zA-Z0-9_-]+$/;
70
71
  var settingsBaseFields = {
71
72
  label: z2.string().min(1),
@@ -267,6 +268,70 @@ function validateProviderLiteralArray(args) {
267
268
  }
268
269
  return Object.freeze(normalized);
269
270
  }
271
+ function normalizeProviderBridgeOptions(providerId, value) {
272
+ const active = /* @__PURE__ */ new Set();
273
+ function visit(current, path) {
274
+ if (current === null || typeof current === "string" || typeof current === "boolean") {
275
+ return current;
276
+ }
277
+ if (typeof current === "number") {
278
+ if (!Number.isFinite(current)) {
279
+ throw new Error(
280
+ `provider "${providerId}" experimental_bridgeOptions${path} must be finite JSON`
281
+ );
282
+ }
283
+ return current;
284
+ }
285
+ if (typeof current !== "object") {
286
+ throw new Error(
287
+ `provider "${providerId}" experimental_bridgeOptions${path} must be JSON`
288
+ );
289
+ }
290
+ if (active.has(current)) {
291
+ throw new Error(
292
+ `provider "${providerId}" experimental_bridgeOptions must not contain cycles`
293
+ );
294
+ }
295
+ active.add(current);
296
+ try {
297
+ if (Array.isArray(current)) {
298
+ const normalized3 = current.map(
299
+ (entry, index) => visit(entry, `${path}[${index}]`)
300
+ );
301
+ Object.freeze(normalized3);
302
+ return normalized3;
303
+ }
304
+ const prototype = Object.getPrototypeOf(current);
305
+ if (prototype !== Object.prototype && prototype !== null) {
306
+ throw new Error(
307
+ `provider "${providerId}" experimental_bridgeOptions${path} must contain only plain JSON objects`
308
+ );
309
+ }
310
+ const normalized2 = Object.fromEntries(
311
+ Object.entries(current).map(([key, entry]) => [
312
+ key,
313
+ visit(entry, `${path}.${key}`)
314
+ ])
315
+ );
316
+ Object.freeze(normalized2);
317
+ return normalized2;
318
+ } finally {
319
+ active.delete(current);
320
+ }
321
+ }
322
+ const normalized = visit(value, "");
323
+ if (normalized === null || Array.isArray(normalized) || typeof normalized !== "object") {
324
+ throw new Error(
325
+ `provider "${providerId}" experimental_bridgeOptions must be an object`
326
+ );
327
+ }
328
+ if (Buffer.byteLength(JSON.stringify(normalized), "utf8") > PLUGIN_PROVIDER_BRIDGE_OPTIONS_MAX_BYTES) {
329
+ throw new Error(
330
+ `provider "${providerId}" experimental_bridgeOptions exceeds ${PLUGIN_PROVIDER_BRIDGE_OPTIONS_MAX_BYTES} bytes`
331
+ );
332
+ }
333
+ return normalized;
334
+ }
270
335
  function validatePluginProviderDeclaration(declaration) {
271
336
  if (typeof declaration !== "object" || declaration === null) {
272
337
  throw new Error("provider declaration must be an object");
@@ -304,6 +369,24 @@ function validatePluginProviderDeclaration(declaration) {
304
369
  if (typeof capabilities !== "object" || capabilities === null) {
305
370
  throw new Error(`provider "${id}" capabilities must be an object`);
306
371
  }
372
+ const experimentalProviderHealth = capabilities.experimental_providerHealth ?? false;
373
+ const experimentalProviderUsage = capabilities.experimental_providerUsage ?? false;
374
+ const experimentalProviderInstallation = capabilities.experimental_providerInstallation ?? false;
375
+ if (typeof experimentalProviderHealth !== "boolean") {
376
+ throw new Error(
377
+ `provider "${id}" capabilities.experimental_providerHealth must be a boolean`
378
+ );
379
+ }
380
+ if (typeof experimentalProviderUsage !== "boolean") {
381
+ throw new Error(
382
+ `provider "${id}" capabilities.experimental_providerUsage must be a boolean`
383
+ );
384
+ }
385
+ if (typeof experimentalProviderInstallation !== "boolean") {
386
+ throw new Error(
387
+ `provider "${id}" capabilities.experimental_providerInstallation must be a boolean`
388
+ );
389
+ }
307
390
  const booleanCapabilityFields = [
308
391
  "supportsServiceTier",
309
392
  "supportsNativeUserQuestion",
@@ -325,6 +408,9 @@ function validatePluginProviderDeclaration(declaration) {
325
408
  );
326
409
  }
327
410
  const normalizedCapabilities = Object.freeze({
411
+ experimental_providerHealth: experimentalProviderHealth,
412
+ experimental_providerUsage: experimentalProviderUsage,
413
+ experimental_providerInstallation: experimentalProviderInstallation,
328
414
  supportsServiceTier: capabilities.supportsServiceTier,
329
415
  supportsNativeUserQuestion: capabilities.supportsNativeUserQuestion,
330
416
  fork: capabilities.fork,
@@ -354,10 +440,27 @@ function validatePluginProviderDeclaration(declaration) {
354
440
  allowed: PLUGIN_PROVIDER_COMPOSER_ACTION_VALUES,
355
441
  requireNonEmpty: false
356
442
  });
443
+ const bridgeOptions = declaration.experimental_bridgeOptions === void 0 ? void 0 : normalizeProviderBridgeOptions(
444
+ id,
445
+ declaration.experimental_bridgeOptions
446
+ );
447
+ const visibility = declaration.experimental_visibility ?? "always";
448
+ if (visibility !== "always" && visibility !== "installed") {
449
+ throw new Error(
450
+ `provider "${id}" experimental_visibility must be "always" or "installed"`
451
+ );
452
+ }
453
+ if (visibility === "installed" && !normalizedCapabilities.experimental_providerHealth) {
454
+ throw new Error(
455
+ `provider "${id}" experimental_visibility "installed" requires experimental_providerHealth`
456
+ );
457
+ }
357
458
  return Object.freeze({
358
459
  id,
359
460
  displayName,
360
461
  ...icon === void 0 ? {} : { icon },
462
+ ...bridgeOptions === void 0 ? {} : { experimental_bridgeOptions: bridgeOptions },
463
+ experimental_visibility: visibility,
361
464
  capabilities: normalizedCapabilities,
362
465
  composerActions
363
466
  });
@@ -1047,7 +1150,7 @@ function createFakePluginHostInternal(options, sharedState) {
1047
1150
  kv,
1048
1151
  database() {
1049
1152
  assertLive();
1050
- if (!databaseHandle) {
1153
+ if (!databaseHandle?.open) {
1051
1154
  databaseHandle = new Database(join(storageRoot, "data.db"));
1052
1155
  databaseHandle.pragma("busy_timeout = 5000");
1053
1156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@get-bb/plugin-sdk",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "homepage": "https://github.com/get-bb/bb#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/get-bb/bb/issues"
@@ -56,6 +56,12 @@
56
56
  "import": "./dist/internal/composer-view.js",
57
57
  "default": "./dist/internal/composer-view.js"
58
58
  },
59
+ "./internal/file-navigation-validation": {
60
+ "source": "./src/internal/file-navigation-validation.ts",
61
+ "types": "./bundled-types/bb-plugin-sdk-internal-file-navigation-validation.d.ts",
62
+ "import": "./dist/internal/file-navigation-validation.js",
63
+ "default": "./dist/internal/file-navigation-validation.js"
64
+ },
59
65
  "./internal/host-policy": {
60
66
  "source": "./src/internal/host-policy.ts",
61
67
  "types": "./bundled-types/bb-plugin-sdk-internal-host-policy.d.ts",
@@ -89,12 +95,13 @@
89
95
  },
90
96
  "types": "./bundled-types/bb-plugin-sdk.d.ts",
91
97
  "scripts": {
92
- "build": "node scripts/build-bundled-dts.mjs && node scripts/build-runtime.mjs",
98
+ "build": "node scripts/build-runtime.mjs",
93
99
  "build:runtime": "node scripts/build-runtime.mjs",
94
- "clean": "rimraf dist tsconfig.tsbuildinfo",
100
+ "build:types": "node scripts/build-bundled-dts.mjs",
101
+ "clean": "rimraf bundled-types dist tsconfig.tsbuildinfo",
95
102
  "prepack": "node scripts/build-bundled-dts.mjs && node scripts/build-runtime.mjs",
96
103
  "test": "vitest run --config vitest.config.ts",
97
- "typecheck": "node scripts/build-bundled-dts.mjs --check && tsc --noEmit"
104
+ "typecheck": "tsc --noEmit"
98
105
  },
99
106
  "devDependencies": {
100
107
  "@bb/domain": "workspace:*",