@get-bb/plugin-sdk 0.4.30 → 0.4.35

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.
@@ -431,6 +431,7 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
431
431
  composerCustomizations: [],
432
432
  pendingInteractions: [],
433
433
  sidebarFooterActions: [],
434
+ experimentalSidebarNavigations: [],
434
435
  threadLists: [],
435
436
  threadHeaderActions: [],
436
437
  fileOpeners: [],
@@ -452,6 +453,7 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
452
453
  composerCustomization: /* @__PURE__ */ new Set(),
453
454
  pendingInteraction: /* @__PURE__ */ new Set(),
454
455
  sidebarFooterAction: /* @__PURE__ */ new Set(),
456
+ sidebarNavigation: /* @__PURE__ */ new Set(),
455
457
  threadList: /* @__PURE__ */ new Set(),
456
458
  threadHeaderAction: /* @__PURE__ */ new Set(),
457
459
  fileOpener: /* @__PURE__ */ new Set(),
@@ -643,6 +645,22 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
643
645
  run: registration.run
644
646
  });
645
647
  },
648
+ experimental_sidebarNavigation(registration) {
649
+ const kind = "slots.experimental_sidebarNavigation";
650
+ const id = requireSlotId(kind, registration?.id);
651
+ requireUniqueId(kind, seenIds.sidebarNavigation, id);
652
+ const description = requireOptionalString(
653
+ kind,
654
+ "description",
655
+ registration.description
656
+ );
657
+ collected.experimentalSidebarNavigations.push({
658
+ id,
659
+ title: requireNonEmptyString(kind, "title", registration.title),
660
+ ...description !== void 0 ? { description } : {},
661
+ component: requireComponent(kind, registration.component)
662
+ });
663
+ },
646
664
  experimental_threadList(registration) {
647
665
  const kind = "slots.experimental_threadList";
648
666
  const id = requireSlotId(kind, registration?.id);
@@ -1713,7 +1731,8 @@ function renderSlot(registration, props, options = {}) {
1713
1731
  inputLockCalls: [],
1714
1732
  quotes: [],
1715
1733
  mentions: [],
1716
- focusCount: 0
1734
+ focusCount: 0,
1735
+ submits: []
1717
1736
  };
1718
1737
  const composerOwnership = { active: true };
1719
1738
  const composer = {
@@ -1768,6 +1787,19 @@ ${block}
1768
1787
  },
1769
1788
  focus() {
1770
1789
  composerLog.focusCount += 1;
1790
+ },
1791
+ async experimental_submit({ sendAt }) {
1792
+ if (!composerOwnership.active) {
1793
+ throw new Error("This composer is no longer active.");
1794
+ }
1795
+ if (composerText.trim() === "") {
1796
+ throw new Error("Type a message before scheduling it.");
1797
+ }
1798
+ if (!Number.isFinite(sendAt) || sendAt <= Date.now()) {
1799
+ throw new Error("Pick a time in the future.");
1800
+ }
1801
+ composerLog.submits.push({ sendAt });
1802
+ commitComposerText("");
1771
1803
  }
1772
1804
  }
1773
1805
  };
@@ -6,7 +6,6 @@ import { join } from "node:path";
6
6
  import Database from "better-sqlite3";
7
7
  import { CronExpressionParser } from "cron-parser";
8
8
  import { Hono } from "hono";
9
- import { z as z4 } from "zod";
10
9
 
11
10
  // ../domain/src/plugin-interaction-limits.ts
12
11
  var PLUGIN_INTERACTION_MAX_TITLE_LENGTH = 160;
@@ -307,12 +306,19 @@ var settingsBaseFields = {
307
306
  label: z3.string().min(1),
308
307
  description: z3.string().min(1).optional()
309
308
  };
309
+ var stringSettingSchemaSchema = z3.custom(
310
+ (value) => isStandardSchema(value)
311
+ );
312
+ var booleanSettingSchemaSchema = z3.custom(
313
+ (value) => isStandardSchema(value)
314
+ );
310
315
  var settingDescriptorSchema = z3.discriminatedUnion("type", [
311
316
  z3.object({
312
317
  type: z3.literal("string"),
313
318
  ...settingsBaseFields,
314
319
  secret: z3.literal(true).optional(),
315
320
  experimental_multiline: z3.boolean().optional(),
321
+ experimental_schema: stringSettingSchemaSchema.optional(),
316
322
  default: z3.string().optional()
317
323
  }).strict().refine(
318
324
  (descriptor) => !(descriptor.secret === true && descriptor.experimental_multiline === true),
@@ -324,17 +330,20 @@ var settingDescriptorSchema = z3.discriminatedUnion("type", [
324
330
  z3.object({
325
331
  type: z3.literal("boolean"),
326
332
  ...settingsBaseFields,
333
+ experimental_schema: booleanSettingSchemaSchema.optional(),
327
334
  default: z3.boolean().optional()
328
335
  }).strict(),
329
336
  z3.object({
330
337
  type: z3.literal("select"),
331
338
  ...settingsBaseFields,
332
339
  options: z3.array(z3.string().min(1)).min(1),
340
+ experimental_schema: stringSettingSchemaSchema.optional(),
333
341
  default: z3.string().optional()
334
342
  }).strict(),
335
343
  z3.object({
336
344
  type: z3.literal("project"),
337
345
  ...settingsBaseFields,
346
+ experimental_schema: stringSettingSchemaSchema.optional(),
338
347
  default: z3.string().optional()
339
348
  }).strict()
340
349
  ]);
@@ -363,11 +372,39 @@ function registerSettingDescriptors(target, added) {
363
372
  `default for setting "${key}" must be one of its options`
364
373
  );
365
374
  }
375
+ if (descriptor.default !== void 0) {
376
+ const errors = validateSettingsUpdate(
377
+ { [key]: descriptor },
378
+ { [key]: descriptor.default }
379
+ );
380
+ if (errors.length > 0) {
381
+ throw new Error(`invalid default for setting "${key}": ${errors[0]}`);
382
+ }
383
+ }
366
384
  validated[key] = descriptor;
367
385
  }
368
386
  Object.assign(target, validated);
369
387
  return validated;
370
388
  }
389
+ function settingSchemaError(key, schema, value) {
390
+ if (schema === void 0) return null;
391
+ try {
392
+ const result = schema["~standard"].validate(value);
393
+ if (result instanceof Promise) {
394
+ return `schema for setting "${key}" must validate synchronously`;
395
+ }
396
+ if (result.issues !== void 0) {
397
+ return result.issues[0]?.message ?? `schema for setting "${key}" rejected the value`;
398
+ }
399
+ if (result.value !== value) {
400
+ return `schema for setting "${key}" must not transform its value`;
401
+ }
402
+ return null;
403
+ } catch (error) {
404
+ const detail = error instanceof Error ? error.message : String(error);
405
+ return `schema for setting "${key}" failed: ${detail}`;
406
+ }
407
+ }
371
408
  function validateSettingsUpdate(descriptors, values) {
372
409
  const errors = [];
373
410
  for (const [key, value] of Object.entries(values)) {
@@ -380,6 +417,15 @@ function validateSettingsUpdate(descriptors, values) {
380
417
  if (descriptor.type === "boolean") {
381
418
  if (typeof value !== "boolean") {
382
419
  errors.push(`setting "${key}" expects a boolean`);
420
+ continue;
421
+ }
422
+ const validationError2 = settingSchemaError(
423
+ key,
424
+ descriptor.experimental_schema,
425
+ value
426
+ );
427
+ if (validationError2 !== null) {
428
+ errors.push(validationError2);
383
429
  }
384
430
  continue;
385
431
  }
@@ -391,6 +437,15 @@ function validateSettingsUpdate(descriptors, values) {
391
437
  errors.push(
392
438
  `setting "${key}" must be one of: ${descriptor.options.join(", ")}`
393
439
  );
440
+ continue;
441
+ }
442
+ const validationError = settingSchemaError(
443
+ key,
444
+ descriptor.experimental_schema,
445
+ value
446
+ );
447
+ if (validationError !== null) {
448
+ errors.push(validationError);
394
449
  }
395
450
  }
396
451
  return errors;
@@ -1294,6 +1349,12 @@ function readRpcMethodContract(method, value) {
1294
1349
  function isZodSchemaLike(value) {
1295
1350
  return typeof value === "object" && value !== null && typeof value.safeParse === "function";
1296
1351
  }
1352
+ function zodSchemaToJsonSchema(schema) {
1353
+ if (typeof schema.toJSONSchema === "function") {
1354
+ return schema.toJSONSchema({ io: "input" });
1355
+ }
1356
+ return z3.toJSONSchema(schema, { io: "input" });
1357
+ }
1297
1358
  var SINGLE_SCHEMA_KEYWORDS = [
1298
1359
  "additionalItems",
1299
1360
  "additionalProperties",
@@ -1612,6 +1673,12 @@ function agentToolIconRefusalMessage(toolName, problem) {
1612
1673
  function providerWithoutBridgeMessage(providerId) {
1613
1674
  return `provider "${providerId}" has no bridge to run on: this plugin declares no "bb.host" entry in its manifest`;
1614
1675
  }
1676
+ function storePluginHook(records, hook, handler) {
1677
+ records[hook] = handler;
1678
+ }
1679
+ function pluginHookAlreadyRegisteredMessage(hook) {
1680
+ return `a "${hook}" hook handler is already registered by this plugin`;
1681
+ }
1615
1682
 
1616
1683
  // src/testing/fake-sdk.ts
1617
1684
  function withSpawnAttribution(pluginId, args) {
@@ -2076,7 +2143,9 @@ function createFakePluginHostInternal(options, sharedState) {
2076
2143
  "ALTER TABLE _bb_migrations ADD COLUMN statement_hash TEXT"
2077
2144
  );
2078
2145
  }
2079
- const rows = database.prepare("SELECT id, statement_hash FROM _bb_migrations ORDER BY id").all();
2146
+ const rows = database.prepare(
2147
+ "SELECT id, statement_hash FROM _bb_migrations ORDER BY id"
2148
+ ).all();
2080
2149
  const applied = /* @__PURE__ */ new Map();
2081
2150
  for (const row of rows) applied.set(row.id, row.statement_hash);
2082
2151
  const statementHashes = statements.map(migrationStatementHash);
@@ -2113,20 +2182,57 @@ function createFakePluginHostInternal(options, sharedState) {
2113
2182
  const settingsDescriptors = {};
2114
2183
  const settingsListeners = [];
2115
2184
  const storedSettings = persistentState.storedSettings;
2185
+ async function setSettingsValues(values) {
2186
+ const errors = validateSettingsUpdate(settingsDescriptors, values);
2187
+ if (errors.length > 0) {
2188
+ throw new Error(errors.join("; "));
2189
+ }
2190
+ const prev = readSettingsValues(settingsDescriptors, storedSettings);
2191
+ for (const [key, value] of Object.entries(values)) {
2192
+ if (value === null) storedSettings.delete(key);
2193
+ else if (typeof value === "string" || typeof value === "boolean") {
2194
+ storedSettings.set(key, value);
2195
+ } else {
2196
+ throw new Error(`setting "${key}" has an unsupported value`);
2197
+ }
2198
+ }
2199
+ const next = readSettingsValues(settingsDescriptors, storedSettings);
2200
+ if (JSON.stringify(next) === JSON.stringify(prev)) return;
2201
+ for (const listener of settingsListeners) {
2202
+ try {
2203
+ listener(next, prev);
2204
+ } catch (error) {
2205
+ emitLog(
2206
+ "warn",
2207
+ `settings onChange listener failed: ${errorMessage(error)}`
2208
+ );
2209
+ }
2210
+ }
2211
+ }
2116
2212
  const settings = {
2117
2213
  define(descriptors) {
2118
2214
  assertLive();
2119
- registerSettingDescriptors(
2215
+ const validated = registerSettingDescriptors(
2120
2216
  settingsDescriptors,
2121
2217
  descriptors
2122
2218
  );
2123
2219
  return {
2124
2220
  async get() {
2125
2221
  assertLive();
2126
- return readSettingsValues(
2127
- settingsDescriptors,
2128
- storedSettings
2129
- );
2222
+ return readSettingsValues(validated, storedSettings);
2223
+ },
2224
+ async experimental_set(values) {
2225
+ assertLive();
2226
+ const rawValues = {};
2227
+ for (const [key, value] of Object.entries(values)) {
2228
+ rawValues[key] = value;
2229
+ }
2230
+ const errors = validateSettingsUpdate(validated, rawValues);
2231
+ if (errors.length > 0) {
2232
+ throw new Error(errors.join("; "));
2233
+ }
2234
+ await setSettingsValues(rawValues);
2235
+ return readSettingsValues(validated, storedSettings);
2130
2236
  },
2131
2237
  onChange(listener) {
2132
2238
  assertLive();
@@ -2456,9 +2562,7 @@ function createFakePluginHostInternal(options, sharedState) {
2456
2562
  let parse;
2457
2563
  if (isZodSchemaLike(parameters)) {
2458
2564
  try {
2459
- inputSchema = z4.toJSONSchema(parameters, {
2460
- io: "input"
2461
- });
2565
+ inputSchema = zodSchemaToJsonSchema(parameters);
2462
2566
  } catch (error) {
2463
2567
  throw new Error(
2464
2568
  `tool "${name}" parameters look like a zod schema but could not be converted to JSON Schema (${errorMessage(error)}) \u2014 use zod 4, or pass a plain JSON-schema object`
@@ -2538,6 +2642,7 @@ function createFakePluginHostInternal(options, sharedState) {
2538
2642
  });
2539
2643
  }
2540
2644
  };
2645
+ let requestedDrains = 0;
2541
2646
  const needsConfigurationMessages = [];
2542
2647
  const status = {
2543
2648
  needsConfiguration(message) {
@@ -2569,7 +2674,13 @@ function createFakePluginHostInternal(options, sharedState) {
2569
2674
  "thread.idle": [],
2570
2675
  "thread.failed": [],
2571
2676
  "thread.archived": [],
2572
- "thread.deleted": []
2677
+ "thread.deleted": [],
2678
+ "message.queued": [],
2679
+ "message.dispatched": [],
2680
+ "turn.failed": []
2681
+ };
2682
+ const hooks = {
2683
+ "message.dispatch": null
2573
2684
  };
2574
2685
  const disposeHooks = [];
2575
2686
  const serviceControllers = [];
@@ -2787,6 +2898,18 @@ function createFakePluginHostInternal(options, sharedState) {
2787
2898
  return registerProviderDeclaration(declaration);
2788
2899
  }
2789
2900
  };
2901
+ const experimental_hooks = {
2902
+ on(hook, handler) {
2903
+ if (hooks[hook] !== null) {
2904
+ throw new Error(pluginHookAlreadyRegisteredMessage(hook));
2905
+ }
2906
+ storePluginHook(hooks, hook, handler);
2907
+ },
2908
+ async recheck(_hook) {
2909
+ assertLive();
2910
+ requestedDrains += 1;
2911
+ }
2912
+ };
2790
2913
  const bb = {
2791
2914
  pluginId,
2792
2915
  log,
@@ -2801,6 +2924,7 @@ function createFakePluginHostInternal(options, sharedState) {
2801
2924
  providers,
2802
2925
  ui,
2803
2926
  events,
2927
+ experimental_hooks,
2804
2928
  status,
2805
2929
  server,
2806
2930
  hosts,
@@ -2858,6 +2982,9 @@ function createFakePluginHostInternal(options, sharedState) {
2858
2982
  logEntries,
2859
2983
  realtimeSignals,
2860
2984
  needsConfigurationMessages,
2985
+ get recheckCount() {
2986
+ return requestedDrains;
2987
+ },
2861
2988
  sharedPortDeclarations,
2862
2989
  experimental_hostRpcCalls: hostRpcCalls,
2863
2990
  sdk: sdkHarness,
@@ -2886,9 +3013,15 @@ function createFakePluginHostInternal(options, sharedState) {
2886
3013
  "thread.idle": threadEventHandlers["thread.idle"].length,
2887
3014
  "thread.failed": threadEventHandlers["thread.failed"].length,
2888
3015
  "thread.archived": threadEventHandlers["thread.archived"].length,
2889
- "thread.deleted": threadEventHandlers["thread.deleted"].length
3016
+ "thread.deleted": threadEventHandlers["thread.deleted"].length,
3017
+ "message.queued": threadEventHandlers["message.queued"].length,
3018
+ "message.dispatched": threadEventHandlers["message.dispatched"].length,
3019
+ "turn.failed": threadEventHandlers["turn.failed"].length
2890
3020
  };
2891
3021
  },
3022
+ get hooks() {
3023
+ return { ...hooks };
3024
+ },
2892
3025
  mentionProviders,
2893
3026
  providerRegistrations,
2894
3027
  aiServiceRegistrations
@@ -2943,27 +3076,7 @@ function createFakePluginHostInternal(options, sharedState) {
2943
3076
  pending.resolve({ outcome: "cancelled", reason: "user" });
2944
3077
  },
2945
3078
  async setSettings(values) {
2946
- const errors = validateSettingsUpdate(settingsDescriptors, values);
2947
- if (errors.length > 0) {
2948
- throw new Error(errors.join("; "));
2949
- }
2950
- const prev = readSettingsValues(settingsDescriptors, storedSettings);
2951
- for (const [key, value] of Object.entries(values)) {
2952
- if (value === null) storedSettings.delete(key);
2953
- else storedSettings.set(key, value);
2954
- }
2955
- const next = readSettingsValues(settingsDescriptors, storedSettings);
2956
- if (JSON.stringify(next) === JSON.stringify(prev)) return;
2957
- for (const listener of settingsListeners) {
2958
- try {
2959
- listener(next, prev);
2960
- } catch (error) {
2961
- emitLog(
2962
- "warn",
2963
- `settings onChange listener failed: ${errorMessage(error)}`
2964
- );
2965
- }
2966
- }
3079
+ await setSettingsValues(values);
2967
3080
  },
2968
3081
  async callRpc(method, input) {
2969
3082
  const record = rpcHandlers.get(method);
@@ -3195,6 +3308,43 @@ function makeThreadResponse(overrides = {}) {
3195
3308
  runtime: { displayStatus: "idle", hostReconnectGraceExpiresAt: null },
3196
3309
  activeBackgroundAgentCount: 0,
3197
3310
  canSpawnChild: true,
3311
+ queuedMessageCount: 0,
3312
+ ...overrides
3313
+ };
3314
+ }
3315
+ function makeQueueEntry(overrides = {}) {
3316
+ return {
3317
+ id: "queued_1",
3318
+ threadId: "thread-1",
3319
+ content: [{ type: "text", text: "Queued turn", mentions: [] }],
3320
+ model: "test-model",
3321
+ reasoningLevel: "medium",
3322
+ permissionMode: "auto",
3323
+ serviceTier: "default",
3324
+ groupWithNext: false,
3325
+ sendAt: null,
3326
+ waitingOn: {
3327
+ kind: "plugin",
3328
+ pluginId: "test-plugin",
3329
+ reason: "Waiting"
3330
+ },
3331
+ failureReason: null,
3332
+ payload: { kind: "inline" },
3333
+ editable: true,
3334
+ createdAt: 0,
3335
+ updatedAt: 0,
3336
+ ...overrides
3337
+ };
3338
+ }
3339
+ function makeTurnFailedEvent(overrides = {}) {
3340
+ return {
3341
+ threadId: "thread-1",
3342
+ requestId: "creq_2222222222",
3343
+ turnId: "turn-1",
3344
+ errorInfo: null,
3345
+ inputAccepted: true,
3346
+ rateLimits: null,
3347
+ attemptNumber: 1,
3198
3348
  ...overrides
3199
3349
  };
3200
3350
  }
@@ -3299,5 +3449,7 @@ export {
3299
3449
  createFakePluginHost,
3300
3450
  createFakeSdk,
3301
3451
  scanPublicSdkOnly as experimental_scanPublicSdkOnly,
3302
- makeThreadResponse
3452
+ makeQueueEntry,
3453
+ makeThreadResponse,
3454
+ makeTurnFailedEvent
3303
3455
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@get-bb/plugin-sdk",
3
- "version": "0.4.30",
3
+ "version": "0.4.35",
4
4
  "homepage": "https://github.com/get-bb/bb#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/get-bb/bb/issues"