@stackable-labs/mcp-app-extension 1.29.0 → 2.0.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 (3) hide show
  1. package/dist/index.js +427 -38
  2. package/dist/server.js +427 -38
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
- import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, PERMISSIONS, CAPABILITY_PERMISSION_MAP, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
4
+ import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP, HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, EVENT_HOOK_PERMISSION_MAP, MESSAGING_SEND_EXAMPLES } from '@stackable-labs/sdk-extension-contracts';
5
5
  import { validatePatternFormat } from '@stackable-labs/lib-contracts';
6
6
  import fs4, { readFile, mkdir, writeFile, constants } from 'fs/promises';
7
7
  import path, { join } from 'path';
@@ -348,6 +348,11 @@ var CORE_CONTENT = `# Extension SDK \u2014 Editorial Notes
348
348
  - Always check the \`loading\` flag before using context values
349
349
  - Context is only available when the host provides it (e.g., when viewing a ticket)
350
350
 
351
+ ### \`messaging.send\` resolves to null without throwing
352
+ - The host silently handles \`no_conversation\` / \`reauth_required\` / \`forbidden\` \u2014 \`send()\` returns null and \`error\` stays unset on these codes
353
+ - These are operational/admin concerns (no active conversation, instance disconnected from messaging, permission revoked) \u2014 surfaced to admins via dashboard remediation UI, not user-actionable from the extension
354
+ - Actionable codes (\`invalid_message\`, \`rate_limited\`, \`upstream_error\`) DO populate \`error\` and throw \u2014 handle in UI
355
+
351
356
  ## Best Practices
352
357
 
353
358
  ### Performance
@@ -506,7 +511,12 @@ useExtendIdentity((claims) => ({
506
511
  external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
507
512
  loyalty_tier: 'bronze', // custom \u2014 sync, known at login
508
513
  verified: false, // custom \u2014 default; updated async post-verification
509
- }))`
514
+ }))`,
515
+ "messaging.send": `import { useMessaging } from '@stackable-labs/sdk-extension-react'
516
+
517
+ // manifest.json: { "permissions": ["messaging:send"] }
518
+ const [send, { enabled, loading, error }] = useMessaging()
519
+ await send({ kind: 'text', body: 'Hello from the extension' })`
510
520
  };
511
521
  var HOOK_SNIPPETS_MEMOIZED = {
512
522
  "events:messaging": `import { useCallback } from 'react'
@@ -541,7 +551,7 @@ var generateCapabilities = () => {
541
551
  const fm = frontmatter({
542
552
  root: false,
543
553
  targets: ["*"],
544
- description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity",
554
+ description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, messaging.send, events:identity, events:messaging, events:activity",
545
555
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
546
556
  });
547
557
  return `${fm}
@@ -798,7 +808,7 @@ await capabilities.identity.extend({
798
808
  // \u2192 identity:refresh broadcast to all extensions with events:identity
799
809
  \`\`\`
800
810
 
801
- Per-extension calls are serialized to prevent concurrent Zendesk \`loginUser\` callbacks from orphaning each other (empirically verified in the loginUser re-auth spike).
811
+ Per-extension calls are serialized to prevent concurrent \`loginUser\` callbacks from orphaning each other.
802
812
 
803
813
  ### Consuming enriched state from another extension
804
814
 
@@ -831,6 +841,122 @@ The publisher-side bundle scan validates your declaration at submission time:
831
841
  | \`identityClaims_reserved_key\` | error | Collides with a reserved JWT / Zendesk claim |
832
842
  | \`identityClaims_standard_key\` | warning | Redundant \u2014 standard claims (\`external_id\`, \`email\`, \`name\`) are exempt |
833
843
  | \`identityClaims_too_many\` | error | More than 20 entries |
844
+
845
+ ## messaging.send \u2014 Send Messages to Conversations
846
+
847
+ Post a message into the **active conversation** bound to the current Instance. The host attributes each message to the extension via the per-Instance author label set by the admin (see "Author label" below). Two complementary APIs:
848
+
849
+ 1. **\`useMessaging()\`** \u2014 React hook returning tuple \`[send, { enabled, loading, error, data }]\` \u2014 lets callers rename \`send\` per-instance when used multiple times in one component. Preferred for UI components; narrows the error surface to actionable codes only. Use \`enabled\` to pre-empt the silent \`no_conversation\` case **without** declaring \`context:read\`.
850
+ 2. **\`messagingSendCapability(payload)\`** / **\`capabilities.messaging.send(payload)\`** \u2014 imperative; useful outside React render. Exposes the full wire-level error taxonomy.
851
+
852
+ ### Manifest contract
853
+
854
+ \`\`\`json
855
+ {
856
+ "permissions": ["messaging:send"]
857
+ }
858
+ \`\`\`
859
+
860
+ - **\`messaging:send\` permission** \u2014 without it the host gate rejects the call before any network request.
861
+
862
+ ### Author label (host-resolved, per-Instance)
863
+
864
+ Extensions do **not** set the author label. The host resolves it from:
865
+ 1. \`instance.config.settings.messagingDisplayName\` \u2014 admin sets via the dashboard Instance form (visible only after OAuth completes; gated on \`messagingAppId\` being populated)
866
+ 2. **Fallback**: \`extension.manifest.name\` \u2014 used when admin hasn't set a label
867
+
868
+ This prevents impersonation (extension A can't author as extension B) and gives admins per-deployment branding control.
869
+
870
+ ### Runtime requirements
871
+
872
+ 1. **Instance connected to a messaging provider** \u2014 bearer token stored, ready to post
873
+ 2. **An active conversation** when \`send()\` is called (else host-handled \`no_conversation\` \u2014 \`send()\` returns \`null\`)
874
+ 3. **Instance not disconnected** from the messaging provider (else host-handled \`reauth_required\` \u2014 admin reconnect surfaced in dashboard)
875
+
876
+ ### Payload \u2014 discriminated by \`kind\`
877
+
878
+ Four kinds:
879
+
880
+ | kind | Required fields | Optional |
881
+ |---|---|---|
882
+ | \`'text'\` | \`body: string\` | \`actions?: MessageItemAction[]\`, \`metadata?\`, \`htmlText?\`, \`markdownText?\`, \`disableUserInput?\` |
883
+ | \`'image'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`actions?\`, \`metadata?\` |
884
+ | \`'file'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`metadata?\` (no \`actions\` \u2014 file kind doesn't accept item-level actions) |
885
+ | \`'carousel'\` | \`items: [MessageItem, ...MessageItem[]]\` (1\u201310 items, each with \`title\`, \`description?\`, \`imageUrl?\`, \`actions?\`) | \`displaySettings?: { imageAspectRatio?: 'square' \\| 'horizontal' }\` |
886
+
887
+ **Action types** (per-item or per-message): \`reply\` (quick-reply button \u2014 emits postback to \`useMessagingEvent\`), \`link\` (opens URL), \`postback\` (custom payload to your handler), \`locationRequest\` (asks user for location).
888
+
889
+ ### Hook usage
890
+
891
+ \`\`\`tsx
892
+ import { useMessaging } from '@stackable-labs/sdk-extension-react'
893
+
894
+ const [send, { enabled, loading, error }] = useMessaging()
895
+
896
+ const onApprove = async () => {
897
+ try {
898
+ await send({
899
+ kind: 'text',
900
+ body: 'Order approved \u2713',
901
+ actions: [{ type: 'reply', label: 'Got it', payload: 'ACK' }],
902
+ })
903
+ } catch {
904
+ // error holds the typed SendMessageActionableErrorCode
905
+ }
906
+ }
907
+
908
+ return (
909
+ <button disabled={!enabled || loading} onClick={onApprove}>
910
+ {error === 'rate_limited' ? 'Slow down\u2026' : 'Approve'}
911
+ </button>
912
+ )
913
+ \`\`\`
914
+
915
+ The \`enabled\` flag is a permission-free, host-pushed signal \u2014 \`true\` whenever an active conversation exists. Wiring buttons to \`disabled={!enabled || loading}\` pre-empts the silent \`no_conversation\` case without forcing the extension to declare \`context:read\`. If the extension already has \`context:read\` for other reasons, \`useContextData().messaging?.conversationId\` is an equivalent gate.
916
+
917
+ ### Imperative usage
918
+
919
+ \`\`\`tsx
920
+ const capabilities = useCapabilities()
921
+ const result = await capabilities.messaging.send({ kind: 'text', body: 'Hello' })
922
+ // result is either { messageId, receivedAt } OR { error: SendMessageErrorCode }
923
+ \`\`\`
924
+
925
+ ### Typed error codes \u2014 split by who acts
926
+
927
+ The wire taxonomy has 6 codes. The hook (\`useMessaging\`) narrows them into two groups:
928
+
929
+ **Actionable (extension catches + renders UI)** \u2014 \`send()\` throws and \`state.error\` is populated:
930
+
931
+ | Code | When |
932
+ | --- | --- |
933
+ | \`invalid_message\` | Payload failed validation (e.g. empty \`body\` for text, carousel >10 items, list kind sent) |
934
+ | \`rate_limited\` | Upstream rate limit hit \u2014 back off + retry |
935
+ | \`upstream_error\` | Provider returned 5xx \u2014 transient; retry with backoff |
936
+
937
+ **Host-handled (extension ignores)** \u2014 \`send()\` resolves to \`null\`, \`state.error\` stays \`null\`, SDK logs a breadcrumb:
938
+
939
+ | Code | What the framework does |
940
+ | --- | --- |
941
+ | \`no_conversation\` | \`console.info\` \u2014 pre-empt via the \`enabled\` flag from \`useMessaging()\` (no permission needed) |
942
+ | \`reauth_required\` | \`console.warn\` \u2014 admin sees a "Reconnect" CTA in the dashboard (server flips \`messagingDisconnected: true\`) |
943
+ | \`forbidden\` | \`console.warn\` \u2014 should not reach in production with correct manifest |
944
+
945
+ The imperative \`messagingSendCapability\` path returns the full taxonomy without the actionable/host-handled split \u2014 useful when you need every code (e.g. dynamic dispatch).
946
+
947
+ ### Defense-in-depth gates
948
+
949
+ Three gates fire in series, surfacing the same \`forbidden\` error if any blocks:
950
+
951
+ 1. **Embeddable host gate** (\`CapabilityRPCHandler\`) \u2014 checks \`sandbox.manifest.permissions.includes('messaging:send')\` before the call leaves the sandbox.
952
+ 2. **Backend handler gate** \u2014 checks \`claims.permissions?.includes('messaging:send')\` on the proxy-token JWT.
953
+ 3. **Server-side instance check** \u2014 \`instance.config.messagingDisconnected\` short-circuits with \`reauth_required\` before any upstream call.
954
+
955
+ ### Receiving replies \u2014 pair with \`events:messaging\`
956
+
957
+ \`reply\` and \`postback\` action clicks fire on extensions with the \`events:messaging\` permission via \`useMessagingEvent\` \u2014 see the \`events:messaging\` section above.
958
+
959
+ > **Note:** the postback \`payload\` field is currently not surfaced by the Web Widget (only the button text \`actionName\`). Until that gap is closed, design action labels to be self-describing or pair sends with a follow-up \`data.fetch\` lookup.
834
960
  `;
835
961
  };
836
962
 
@@ -1192,6 +1318,39 @@ export function Content(): React.ReactElement {
1192
1318
  <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
1193
1319
  </Surface>
1194
1320
  )
1321
+ }`,
1322
+ "messaging.send": `import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
1323
+
1324
+ // Requires 'messaging:send' permission. send() throws on actionable errors
1325
+ // (state.error holds the typed code); resolves to null on host-handled cases.
1326
+ export function Content(): React.ReactElement {
1327
+ const [send, { enabled, loading, error }] = useMessaging()
1328
+
1329
+ const onSayHello = async () => {
1330
+ try {
1331
+ await send({
1332
+ kind: 'text',
1333
+ body: 'Hello from the extension',
1334
+ actions: [
1335
+ { type: 'reply', label: 'Sounds good', payload: 'ACK' },
1336
+ { type: 'reply', label: 'Maybe later', payload: 'DEFER' },
1337
+ ],
1338
+ })
1339
+ } catch {
1340
+ // error holds the typed SendMessageActionableErrorCode
1341
+ }
1342
+ }
1343
+
1344
+ return (
1345
+ <Surface id="slot.content">
1346
+ <ui.Stack direction="column" gap="2" className="p-3">
1347
+ <ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
1348
+ {(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
1349
+ {(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
1350
+ {(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
1351
+ </ui.Stack>
1352
+ </Surface>
1353
+ )
1195
1354
  }`
1196
1355
  };
1197
1356
 
@@ -1221,10 +1380,6 @@ Bootstrap the extension runtime. Call once in \`src/index.tsx\`.
1221
1380
  ${EXAMPLE_SNIPPETS.bootstrap}
1222
1381
  \`\`\`
1223
1382
 
1224
- ## Surface component
1225
- Wraps content for a target slot. The \`id\` must match a target in \`manifest.json\`.
1226
- - \`<Surface id="slot.content">...</Surface>\`
1227
-
1228
1383
  ## useCapabilities()
1229
1384
  Returns the capabilities object for calling host-mediated APIs.
1230
1385
  \`\`\`tsx
@@ -1235,6 +1390,7 @@ const capabilities = useCapabilities()
1235
1390
  // capabilities.actions.toast(payload)
1236
1391
  // capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
1237
1392
  // capabilities.identity.extend(patch) \u2014 push enrichment claims to user.metadata + JWT custom_claims (imperative; for handler-style at login, use useExtendIdentity hook). Each patch key MUST be declared in manifest.identityClaims or the host filter drops it.
1393
+ // capabilities.messaging.send(payload) \u2014 post a message into the active conversation bound to this Instance. Prefer the useMessaging hook instead. Requires messaging:send permission. Author label is admin-set per-Instance (instance.config.settings.messagingDisplayName) with extension.manifest.name fallback.
1238
1394
  \`\`\`
1239
1395
 
1240
1396
  ## useStore(store, selector?)
@@ -1243,6 +1399,17 @@ Subscribe to a shared store. Re-renders when the selected state changes.
1243
1399
  const viewState = useStore(appStore, (s) => s.viewState)
1244
1400
  \`\`\`
1245
1401
 
1402
+ ## createStore(initialState)
1403
+ Create a shared store for cross-surface state coordination.
1404
+ \`\`\`tsx
1405
+ const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
1406
+ \`\`\`
1407
+
1408
+ **Store\\<T\\> interface**
1409
+ - \`get(): T\` \u2014 read current state
1410
+ - \`set(partial: Partial<T>): void\` \u2014 merge partial state update
1411
+ - \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
1412
+
1246
1413
  ## useContextData()
1247
1414
  Reads host-provided context including extension settings. Returns \`{ loading, customerId, customerEmail, messaging, settings, ... }\`. \`messaging.conversationId\` is the active Messaging conversation ID (or \`null\` until one exists).
1248
1415
  \`\`\`tsx
@@ -1269,26 +1436,48 @@ Returns extension-level context.
1269
1436
  const { extensionId } = useExtension()
1270
1437
  \`\`\`
1271
1438
 
1272
- ## createStore(initialState)
1273
- Create a shared store for cross-surface state coordination.
1439
+ ## useEvent(eventType, handler)
1440
+ Generic cross-domain event hook (should not be used unless absolutely required). Subscribe to any event using fully-qualified event types.
1441
+ - \`eventType: EventType\` \u2014 fully-qualified (e.g., \`'activity:product_view'\`, \`'identity:login'\`, \`'messaging:postback'\`)
1442
+ - Domain wildcard (e.g., \`'activity'\`) receives all events in that domain
1443
+ - \`handler: (event: BaseEvent) => void\`
1444
+
1274
1445
  \`\`\`tsx
1275
- const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
1446
+ useEvent('activity', (event) => {
1447
+ console.log('Activity:', event.data)
1448
+ })
1276
1449
  \`\`\`
1277
1450
 
1278
- ### Store\\<T\\> interface
1279
- - \`get(): T\` \u2014 read current state
1280
- - \`set(partial: Partial<T>): void\` \u2014 merge partial state update
1281
- - \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
1451
+ ## useMessaging()
1452
+ Send messages into the active conversation bound to this Instance. Wraps the \`messaging.send\` capability with React state, tracking \`enabled\` / \`loading\` / \`error\` / \`data\`. Returns a tuple \`[send, state]\` so callers can rename \`send\` when the hook is used multiple times in one component. Requires \`messaging:send\` permission. The author label rendered above outbound messages is set per-Instance by the admin (Instance settings \`messagingDisplayName\`); falls back to \`extension.manifest.name\` when blank.
1282
1453
 
1283
- ## useIdentityEvent(eventType, handler)
1284
- Subscribe to identity events pushed from the host via the framework. Requires \`events:identity\` permission and matching entries in manifest \`events\` array.
1285
- - \`eventType: ${identityEventTypes}\`
1286
- - \`handler: (event: IdentityEvent) => void\`
1287
- - \`IdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }\`
1288
- - \`IdentityState: { authenticated: boolean, user: UserIdentity | null, expiresAt?: string }\`
1454
+ - **Returns:** \`readonly [send, { enabled, loading, error, data }]\`
1455
+ - **\`send(payload: SendMessagePayload): Promise<SendMessageResponse | null>\`** \u2014 returns the response on success; throws on **actionable** errors; resolves to \`null\` on host-handled errors (SDK logs a breadcrumb, host surfaces remediation)
1456
+ - **\`enabled: boolean\`** \u2014 advisory; \`true\` when an active conversation exists. Permission-free (no \`context:read\` needed). Wire to button \`disabled\` to pre-empt the silent \`no_conversation\` case. Starts \`false\`; flips reactively via host-pushed availability events.
1457
+ - **\`loading: boolean\`** \u2014 true while a call is in flight (matches \`useContextData\`'s \`loading\` for SDK-wide consistency)
1458
+ - **\`data: SendMessageResponse | null\`** \u2014 \`{ messageId, receivedAt }\` from last successful send
1459
+ - **\`error: SendMessageActionableErrorCode | null\`** \u2014 one of \`invalid_message\` / \`rate_limited\` / \`upstream_error\` after a failed send. Host-handled codes (\`no_conversation\` / \`reauth_required\` / \`forbidden\`) never surface here.
1460
+
1461
+ Payload is discriminated by \`kind\`: \`'text'\` / \`'image'\` / \`'file'\` / \`'carousel'\`. See the \`messaging.send\` capability section for the full payload table + action types.
1289
1462
 
1290
1463
  \`\`\`tsx
1291
- ${stripImports(HOOK_SNIPPETS["events:identity"])}
1464
+ import { useMessaging } from '@stackable-labs/sdk-extension-react'
1465
+
1466
+ const [send, { enabled, loading, error }] = useMessaging()
1467
+
1468
+ const onApprove = async () => {
1469
+ try {
1470
+ await send({ kind: 'text', body: 'Approved \u2713' })
1471
+ } catch {
1472
+ // error holds the typed SendMessageActionableErrorCode
1473
+ }
1474
+ }
1475
+
1476
+ return (
1477
+ <button disabled={!enabled || loading} onClick={onApprove}>
1478
+ Approve
1479
+ </button>
1480
+ )
1292
1481
  \`\`\`
1293
1482
 
1294
1483
  ## useMessagingEvent(eventType, handler)
@@ -1319,16 +1508,15 @@ Subscribe to activity events pushed from the host via the framework. Requires \`
1319
1508
  ${stripImports(HOOK_SNIPPETS["events:activity"])}
1320
1509
  \`\`\`
1321
1510
 
1322
- ## useEvent(eventType, handler)
1323
- Generic cross-domain event hook (should not be used unless absolutely required). Subscribe to any event using fully-qualified event types.
1324
- - \`eventType: EventType\` \u2014 fully-qualified (e.g., \`'activity:product_view'\`, \`'identity:login'\`, \`'messaging:postback'\`)
1325
- - Domain wildcard (e.g., \`'activity'\`) receives all events in that domain
1326
- - \`handler: (event: BaseEvent) => void\`
1511
+ ## useIdentityEvent(eventType, handler)
1512
+ Subscribe to identity events pushed from the host via the framework. Requires \`events:identity\` permission and matching entries in manifest \`events\` array.
1513
+ - \`eventType: ${identityEventTypes}\`
1514
+ - \`handler: (event: IdentityEvent) => void\`
1515
+ - \`IdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }\`
1516
+ - \`IdentityState: { authenticated: boolean, user: UserIdentity | null, expiresAt?: string }\`
1327
1517
 
1328
1518
  \`\`\`tsx
1329
- useEvent('activity', (event) => {
1330
- console.log('Activity:', event.data)
1331
- })
1519
+ ${stripImports(HOOK_SNIPPETS["events:identity"])}
1332
1520
  \`\`\`
1333
1521
 
1334
1522
  ## useExtendIdentity(handler)
@@ -1345,7 +1533,7 @@ With \`useCallback\` (for memoized handlers):
1345
1533
  ${HOOK_SNIPPETS_MEMOIZED["identity.extend"]}
1346
1534
  \`\`\`
1347
1535
 
1348
- ## Identity via context.read()
1536
+ **Identity via context.read()**
1349
1537
  Identity state is available in the \`context.read()\` response as an \`identity\` field. Requires \`context:read\` permission (no separate identity permission needed).
1350
1538
  \`\`\`tsx
1351
1539
  const context = await capabilities.context.read()
@@ -3612,6 +3800,27 @@ updating custom fields.
3612
3800
  \`\`\`tsx
3613
3801
  ${EXAMPLE_SNIPPETS["actions.invoke"]}
3614
3802
  \`\`\`
3803
+
3804
+ ## messaging.send \u2014 Sending Messages to Conversations
3805
+
3806
+ Post a message into the active conversation bound to this Instance via the
3807
+ \`useMessaging\` React hook. Returns the response on happy path and throws on
3808
+ **actionable** errors \u2014 inspect \`error\` for one of the typed
3809
+ \`SendMessageActionableErrorCode\` values (\`invalid_message\`, \`rate_limited\`,
3810
+ \`upstream_error\`). Host-handled cases (\`no_conversation\`, \`reauth_required\`,
3811
+ \`forbidden\`) resolve to \`null\` without throwing; the SDK logs a breadcrumb
3812
+ and the host surfaces remediation to admins (e.g. dashboard reconnect UI) \u2014
3813
+ extensions don't catch them.
3814
+
3815
+ **Permission:** \`messaging:send\`. The author label rendered above outbound
3816
+ messages is set **per-Instance by the admin** via the Instance settings field
3817
+ \`messagingDisplayName\` (visible on the dashboard once OAuth completes);
3818
+ falls back to \`extension.manifest.name\` when blank. Extensions do not set or
3819
+ think about the author identity.
3820
+
3821
+ \`\`\`tsx
3822
+ ${EXAMPLE_SNIPPETS["messaging.send"]}
3823
+ \`\`\`
3615
3824
  `;
3616
3825
  };
3617
3826
  var generateCookbookStructural = () => {
@@ -3695,6 +3904,108 @@ ${EXAMPLE_SNIPPETS.surfaceContext}
3695
3904
  \`\`\`
3696
3905
  `;
3697
3906
  };
3907
+ var renderPayload = (kind) => JSON.stringify(MESSAGING_SEND_EXAMPLES[kind], null, 2);
3908
+ var renderAction = (type) => JSON.stringify(MESSAGING_ACTION_EXAMPLES[type], null, 2);
3909
+ var ACTION_DESCRIPTIONS = {
3910
+ reply: "Inserts a visible user-reply bubble carrying `payload` back into the conversation, as if the user typed it. **Mutually exclusive** \u2014 a `reply` action cannot share an `actions[]` array with any other action type; the host validates and surfaces `invalid_message` if mixed.",
3911
+ link: "Opens `url` in a new tab/window. No bubble inserted. Freely mixable with other non-reply actions.",
3912
+ postback: "Fires a server-side `conversation:postback` webhook to the Stackable backend carrying the full `payload` + any `metadata`. NO visible bubble. Extensions with the `events:messaging` permission receive postback events via `useMessagingEvent` \u2014 see the **Events & Identity** cookbook.",
3913
+ locationRequest: "Prompts the user to share device location. Response arrives as a separate `location`-type message inbound to the conversation. Freely mixable."
3914
+ };
3915
+ var actionTypeSections = Object.keys(MESSAGING_ACTION_EXAMPLES).map((type) => `### type: '${type}'
3916
+
3917
+ ${ACTION_DESCRIPTIONS[type]}
3918
+
3919
+ \`\`\`tsx
3920
+ ${renderAction(type)}
3921
+ \`\`\``).join("\n\n");
3922
+ var generateCookbookMessaging = () => {
3923
+ const fm = frontmatter({
3924
+ root: false,
3925
+ targets: ["*"],
3926
+ description: "Cookbook: example payloads per-kind for the messaging.send capability + action type shapes",
3927
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
3928
+ });
3929
+ return `${fm}
3930
+
3931
+ # Messaging Examples
3932
+
3933
+ Example payloads for the \`messaging.send\` capability \u2014 one per supported message
3934
+ kind, plus the action types you can attach to messages. Each payload is the
3935
+ exact shape the \`useMessaging\` hook accepts; copy and adapt as needed.
3936
+
3937
+ **Permission:** \`messaging:send\` in your \`manifest.json\`. The author label
3938
+ above outbound messages is set **per-Instance by the admin** via the Instance
3939
+ settings field \`messagingDisplayName\`; falls back to \`extension.manifest.name\`
3940
+ when blank. Extensions do not set or think about the author identity.
3941
+
3942
+ ---
3943
+
3944
+ ## kind: 'text' \u2014 Plain text + reply actions
3945
+
3946
+ Plain conversational message. Optional \`actions\` array attaches quick-reply
3947
+ buttons, links, postbacks, or location requests.
3948
+
3949
+ \`\`\`tsx
3950
+ const [send] = useMessaging()
3951
+
3952
+ await send(${renderPayload("text")})
3953
+ \`\`\`
3954
+
3955
+ ## kind: 'image' \u2014 Embedded image + optional caption
3956
+
3957
+ Product photos, screenshots, visual help. \`url\` must respond with a
3958
+ \`Content-Type: image/*\` header (Sunco enforces; redirecting URLs like
3959
+ \`picsum.photos\` will fail). \`altText\` is optional \u2014 defaults to filename.
3960
+ Optional \`body\` renders a caption alongside the image.
3961
+
3962
+ \`\`\`tsx
3963
+ const [send] = useMessaging()
3964
+
3965
+ await send(${renderPayload("image")})
3966
+ \`\`\`
3967
+
3968
+ ## kind: 'file' \u2014 Embedded file attachment
3969
+
3970
+ Return labels, receipts, NDAs, warranty paperwork. Sunco's \`fileMessage\`
3971
+ schema does NOT support \`actions\` \u2014 file messages can only carry an
3972
+ optional text caption.
3973
+
3974
+ \`\`\`tsx
3975
+ const [send] = useMessaging()
3976
+
3977
+ await send(${renderPayload("file")})
3978
+ \`\`\`
3979
+
3980
+ ## kind: 'carousel' \u2014 Horizontally scrolling cards
3981
+
3982
+ The conversational-commerce primitive: product recommendations, size/color
3983
+ pickers, search results with images, multi-option selection.
3984
+
3985
+ - 1\u201310 items, each with required \`title\` + required \`actions\` (1\u20133 per item)
3986
+ - Item actions are a subset: \`link\` + \`postback\` only (no \`reply\`, no
3987
+ \`locationRequest\` \u2014 Sunco rejects those at the item level)
3988
+ - Carousel messages have NO message-level \`actions\` field \u2014 actions live
3989
+ per-card
3990
+ - Use a separate \`text\` message before the carousel if you need an intro
3991
+
3992
+ \`\`\`tsx
3993
+ const [send] = useMessaging()
3994
+
3995
+ await send(${renderPayload("carousel")})
3996
+ \`\`\`
3997
+
3998
+ ---
3999
+
4000
+ ## Action types
4001
+
4002
+ Attach to message-level \`actions\` (text / image) or item-level \`actions\`
4003
+ (carousel). All actions share \`label\` + optional \`metadata\` (primitives only,
4004
+ \u22644KB total).
4005
+
4006
+ ${actionTypeSections}
4007
+ `;
4008
+ };
3698
4009
  var identityEventTypes2 = Object.values(IDENTITY_EVENT).map((e) => `\`${e}\``).join(", ");
3699
4010
  var activityEventTypes2 = Object.values(ACTIVITY_EVENT).map((e) => `\`${e}\``).join(", ");
3700
4011
  var generateCookbookEvents = () => {
@@ -3716,7 +4027,7 @@ supports both a login-time hook (\`useExtendIdentity\`) and an imperative post-l
3716
4027
 
3717
4028
  ## Messaging Events
3718
4029
 
3719
- Subscribe to postback button clicks from the Zendesk messaging widget.
4030
+ Subscribe to postback button clicks from the Messaging widget.
3720
4031
  The \`actionName\` is the button's display text, not a programmatic identifier.
3721
4032
 
3722
4033
  **Permission:** \`events:messaging\`
@@ -3853,7 +4164,7 @@ alongside existing surfaces.
3853
4164
  Add the target to the \`targets\` array in \`packages/extension/public/manifest.json\`.
3854
4165
  Also add any required permissions based on the target-permission mapping:
3855
4166
  - \`slot.header\` \u2192 \`context:read\`
3856
- - \`slot.content\` \u2192 \`context:read\`, \`data:query\`, \`actions:toast\`, \`actions:invoke\`
4167
+ - \`slot.content\` \u2192 \`context:read\`, \`data:query\`, \`actions:toast\`, \`actions:invoke\`, \`messaging:send\`
3857
4168
  - \`slot.footer\` \u2192 (none)
3858
4169
  - \`slot.footer-links\` \u2192 (none)
3859
4170
 
@@ -3929,6 +4240,18 @@ await capabilities.identity.extend({ verified: true })
3929
4240
  useIdentityEvent('refresh', (event) => {
3930
4241
  console.log('verified =>', event.data.state.user?.metadata?.verified)
3931
4242
  })
4243
+ `,
4244
+ "messaging.send": `${HOOK_SNIPPETS["messaging.send"]}
4245
+
4246
+ // Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
4247
+ // 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
4248
+ // catch / on state.error to show UX.
4249
+ // Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
4250
+ // resolve to null without throwing; the SDK logs a breadcrumb and the host
4251
+ // surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
4252
+ // can pre-empt 'no_conversation' via the 'enabled' flag from useMessaging()
4253
+ // (permission-free) \u2014 wire to button disabled={!enabled || loading}.
4254
+ if (error === 'rate_limited') { /* show "slow down" UI */ }
3932
4255
  `
3933
4256
  };
3934
4257
  var EVENT_SNIPPETS = {
@@ -4006,7 +4329,7 @@ export function Content(): React.ReactElement {
4006
4329
  // ../../sdk/extension/ai-docs/src/commands/add-capability.ts
4007
4330
  var generateAddCapabilityCommand = () => {
4008
4331
  const fm = frontmatter({
4009
- description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity) in this extension",
4332
+ description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, messaging.send, identity.extend, events:identity, events:messaging, events:activity) in this extension",
4010
4333
  targets: ["*"]
4011
4334
  });
4012
4335
  return `${fm}
@@ -4022,6 +4345,7 @@ Ask which capability to add. Valid capabilities:
4022
4345
  - \`context.read\` \u2014 read host-provided context (customerId, customerEmail, messaging.conversationId, etc.)
4023
4346
  - \`actions.toast\` \u2014 show toast notifications (success, error, info, warning)
4024
4347
  - \`actions.invoke\` \u2014 invoke host actions (e.g., open new conversation)
4348
+ - \`messaging.send\` \u2014 send messages into the active conversation bound to this Instance (text/image/file/carousel + reply/link/postback actions)
4025
4349
  - \`identity.extend\` \u2014 enrich identity JWT claims before signing
4026
4350
  - \`events:identity\` \u2014 subscribe to identity events (login, logout, refresh, expired)
4027
4351
  - \`events:messaging\` \u2014 subscribe to messaging events (postback button clicks)
@@ -4034,6 +4358,7 @@ Add the corresponding permission to \`packages/extension/public/manifest.json\`:
4034
4358
  - \`context.read\` \u2192 \`"context:read"\`
4035
4359
  - \`actions.toast\` \u2192 \`"actions:toast"\`
4036
4360
  - \`actions.invoke\` \u2192 \`"actions:invoke"\`
4361
+ - \`messaging.send\` \u2192 \`"messaging:send"\`
4037
4362
  - \`identity.extend\` \u2192 \`"identity:extend"\`
4038
4363
  - \`events:identity\` \u2192 \`"events:identity"\` (also add entries to \`events\` array, e.g. \`["identity:login", "identity:logout"]\`)
4039
4364
  - \`events:messaging\` \u2192 \`"events:messaging"\` (also add entries to \`events\` array, e.g. \`["messaging:postback:Buy Now"]\`)
@@ -4076,12 +4401,15 @@ const capabilities = useCapabilities()
4076
4401
  // actions.invoke: capabilities.actions.invoke('newConversation', { tags: ['order'], fields: [{ id: 'field_id', value: 'val' }] })
4077
4402
  \`\`\`
4078
4403
 
4079
- ### Special handling (events + identity.extend):
4404
+ ### Special handling (events + identity.extend + messaging.send):
4080
4405
 
4081
4406
  #### For events \u2014 ALWAYS use dedicated hooks (INSTEAD of useCapabilities direct):
4082
4407
  Events are subscribed via \`useIdentityEvent\` / \`useMessagingEvent\` / \`useActivityEvent\` \u2014 never use \`capabilities.events.*\` directly (events are not part of the \`capabilities\` object).
4083
4408
 
4084
- #### For identity.extend \u2014 choose the CORRECT option:
4409
+ #### For messaging.send \u2014 ALWAYS use the \`useMessaging\` hook:
4410
+ \`useMessaging\` returns a \`[send, { enabled, loading, error }]\` tuple. The \`send\` function is the imperative call wrapped with React state + typed errors. ALWAYS use the hook \u2014 do NOT call \`capabilities.messaging.send(...)\` directly from \`useCapabilities()\`. The hook is the canonical pattern: state management (\`enabled\`/\`loading\`) + typed errors (\`error\`) + correct payload shape (\`{ kind, body }\` with the \`kind\` discriminator). Wire send buttons to \`disabled={!enabled || loading}\` \u2014 the \`enabled\` flag pre-empts the silent \`no_conversation\` case without declaring \`context:read\`. See the snippet below for the full error-handling surface (actionable codes vs. host-handled codes).
4411
+
4412
+ #### For identity.extend \u2014 CHOOSE the correct option:
4085
4413
  - **\`useExtendIdentity(handler)\`** \u2014 synchronous hook, fires only once at initial login. ALWAYS use for known-at-login enrichment.
4086
4414
  - **\`capabilities.identity.extend(patch)\`** \u2014 imperative call via \`useCapabilities()\`, fires post-login (after async verification, webhook callbacks, user-triggered flows). Re-signs the JWT and broadcasts \`identity:refresh\`.
4087
4415
 
@@ -4100,6 +4428,11 @@ ${HOOK_SNIPPETS["events:messaging"]}
4100
4428
  ${HOOK_SNIPPETS["events:activity"]}
4101
4429
  \`\`\`
4102
4430
 
4431
+ \`\`\`tsx
4432
+ // messaging.send \u2014 use useMessaging hook (returns [send, { enabled, loading, error }] tuple)
4433
+ ${HOOK_SNIPPETS["messaging.send"]}
4434
+ \`\`\`
4435
+
4103
4436
  \`\`\`tsx
4104
4437
  // identity.extend \u2014 login-time useExtendIdentity hook OR post-login imperative capabilities.identity.extend
4105
4438
  ${CAPABILITY_SNIPPETS["identity.extend"]}
@@ -4111,6 +4444,7 @@ ${CAPABILITY_SNIPPETS["identity.extend"]}
4111
4444
  - For data/context/actions: confirm accessed via \`useCapabilities()\` hook
4112
4445
  - For events: confirm using \`useIdentityEvent\`, \`useMessagingEvent\`, or \`useActivityEvent\` hooks
4113
4446
  - For identity.extend: confirm using \`useExtendIdentity\` hook (login-time) and/or \`capabilities.identity.extend(patch)\` (imperative post-login)
4447
+ - For messaging.send: confirm using \`useMessaging\` hook
4114
4448
  `;
4115
4449
  };
4116
4450
 
@@ -4582,6 +4916,13 @@ var SKILLS = [
4582
4916
  scopes: ["docs"],
4583
4917
  content: () => generateCookbookEvents()
4584
4918
  },
4919
+ {
4920
+ id: "cookbook-messaging",
4921
+ description: "Cookbook: per-kind example payloads (text / image / file / carousel) and action types (reply / link / postback / locationRequest) for the messaging.send capability. Use when wiring the useMessaging hook or composing outbound conversation messages.",
4922
+ type: "knowledge",
4923
+ scopes: ["docs"],
4924
+ content: () => generateCookbookMessaging()
4925
+ },
4585
4926
  {
4586
4927
  id: "marketplace-listing",
4587
4928
  description: "Marketplace listing reference: the listing fields (icon, screenshots, description, categories), Public vs Protected visibility, and the two-stage review process (bundle scan + security review + approval). Use when publishing a deployed extension to the marketplace.",
@@ -5051,6 +5392,39 @@ export function Content() {
5051
5392
  <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
5052
5393
  </Surface>
5053
5394
  )
5395
+ }`,
5396
+ "messaging.send": `import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
5397
+
5398
+ // Requires 'messaging:send' permission. send() throws on actionable errors
5399
+ // (state.error holds the typed code); resolves to null on host-handled cases.
5400
+ export function Content() {
5401
+ const [send, { enabled, loading, error }] = useMessaging()
5402
+
5403
+ const onSayHello = async () => {
5404
+ try {
5405
+ await send({
5406
+ kind: 'text',
5407
+ body: 'Hello from the extension',
5408
+ actions: [
5409
+ { type: 'reply', label: 'Sounds good', payload: 'ACK' },
5410
+ { type: 'reply', label: 'Maybe later', payload: 'DEFER' },
5411
+ ],
5412
+ })
5413
+ } catch {
5414
+ // error holds the typed SendMessageActionableErrorCode
5415
+ }
5416
+ }
5417
+
5418
+ return (
5419
+ <Surface id="slot.content">
5420
+ <ui.Stack direction="column" gap="2" className="p-3">
5421
+ <ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
5422
+ {(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
5423
+ {(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
5424
+ {(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
5425
+ </ui.Stack>
5426
+ </Surface>
5427
+ )
5054
5428
  }`
5055
5429
  };
5056
5430
 
@@ -5142,7 +5516,22 @@ await capabilities.identity.extend({ verified: true })
5142
5516
  // Consumer side \u2014 same or sibling extension reacts via identity:refresh
5143
5517
  useIdentityEvent('refresh', (event) => {
5144
5518
  console.log('verified =>', event.data.state.user?.metadata?.verified)
5145
- })`
5519
+ })`,
5520
+ "messaging.send": `import { useMessaging } from '@stackable-labs/sdk-extension-react'
5521
+
5522
+ // manifest.json: { "permissions": ["messaging:send"] }
5523
+ const [send, { enabled, loading, error }] = useMessaging()
5524
+ await send({ kind: 'text', body: 'Hello from the extension' })
5525
+
5526
+ // Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
5527
+ // 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
5528
+ // catch / on state.error to show UX.
5529
+ // Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
5530
+ // resolve to null without throwing; the SDK logs a breadcrumb and the host
5531
+ // surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
5532
+ // can pre-empt 'no_conversation' via the 'enabled' flag from useMessaging()
5533
+ // (permission-free) \u2014 wire to button disabled={!enabled || loading}.
5534
+ if (error === 'rate_limited') { /* show "slow down" UI */ }`
5146
5535
  };
5147
5536
  var EVENT_SNIPPETS2 = {
5148
5537
  "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
@@ -5518,7 +5907,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
5518
5907
  usedPermissions.add(permission);
5519
5908
  }
5520
5909
  }
5521
- for (const [hook, permission] of Object.entries(EVENT_HOOK_PERMISSION_MAP)) {
5910
+ for (const [hook, permission] of Object.entries(HOOK_PERMISSION_MAP)) {
5522
5911
  if (allSource.includes(hook)) {
5523
5912
  usedPermissions.add(permission);
5524
5913
  }