@stackable-labs/mcp-app-extension 1.28.0 → 1.30.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.
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, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, 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';
@@ -541,7 +541,7 @@ var generateCapabilities = () => {
541
541
  const fm = frontmatter({
542
542
  root: false,
543
543
  targets: ["*"],
544
- description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity",
544
+ description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, messaging.send, events:identity, events:messaging, events:activity",
545
545
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
546
546
  });
547
547
  return `${fm}
@@ -798,7 +798,7 @@ await capabilities.identity.extend({
798
798
  // \u2192 identity:refresh broadcast to all extensions with events:identity
799
799
  \`\`\`
800
800
 
801
- Per-extension calls are serialized to prevent concurrent Zendesk \`loginUser\` callbacks from orphaning each other (empirically verified in the loginUser re-auth spike).
801
+ Per-extension calls are serialized to prevent concurrent \`loginUser\` callbacks from orphaning each other.
802
802
 
803
803
  ### Consuming enriched state from another extension
804
804
 
@@ -831,6 +831,123 @@ The publisher-side bundle scan validates your declaration at submission time:
831
831
  | \`identityClaims_reserved_key\` | error | Collides with a reserved JWT / Zendesk claim |
832
832
  | \`identityClaims_standard_key\` | warning | Redundant \u2014 standard claims (\`external_id\`, \`email\`, \`name\`) are exempt |
833
833
  | \`identityClaims_too_many\` | error | More than 20 entries |
834
+
835
+ ## messaging.send \u2014 Send Messages to Conversations
836
+
837
+ 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:
838
+
839
+ 1. **\`useMessaging()\`** \u2014 React hook returning tuple \`[send, { 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.
840
+ 2. **\`messagingSendCapability(payload)\`** / **\`capabilities.messaging.send(payload)\`** \u2014 imperative; useful outside React render. Exposes the full wire-level error taxonomy.
841
+
842
+ ### Manifest contract
843
+
844
+ \`\`\`json
845
+ {
846
+ "permissions": ["messaging:send"]
847
+ }
848
+ \`\`\`
849
+
850
+ - **\`messaging:send\` permission** \u2014 without it the host gate rejects the call before any network request.
851
+
852
+ ### Author label (host-resolved, per-Instance)
853
+
854
+ Extensions do **not** set the author label. The host resolves it from:
855
+ 1. \`instance.config.settings.messagingDisplayName\` \u2014 admin sets via the dashboard Instance form (visible only after OAuth completes; gated on \`messagingAppId\` being populated)
856
+ 2. **Fallback**: \`extension.manifest.name\` \u2014 used when admin hasn't set a label
857
+
858
+ This prevents impersonation (extension A can't author as extension B) and gives admins per-deployment branding control.
859
+
860
+ ### Runtime requirements
861
+
862
+ 1. **Instance connected to a messaging provider** \u2014 bearer token stored, ready to post
863
+ 2. **An active conversation** when \`send()\` is called (else host-handled \`no_conversation\` \u2014 \`send()\` returns \`null\`)
864
+ 3. **Instance not disconnected** from the messaging provider (else host-handled \`reauth_required\` \u2014 admin reconnect surfaced in dashboard)
865
+
866
+ ### Payload \u2014 discriminated by \`kind\`
867
+
868
+ Four kinds:
869
+
870
+ | kind | Required fields | Optional |
871
+ |---|---|---|
872
+ | \`'text'\` | \`body: string\` | \`actions?: MessageItemAction[]\`, \`metadata?\`, \`htmlText?\`, \`markdownText?\`, \`disableUserInput?\` |
873
+ | \`'image'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`actions?\`, \`metadata?\` |
874
+ | \`'file'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`metadata?\` (no \`actions\` \u2014 file kind doesn't accept item-level actions) |
875
+ | \`'carousel'\` | \`items: [MessageItem, ...MessageItem[]]\` (1\u201310 items, each with \`title\`, \`description?\`, \`imageUrl?\`, \`actions?\`) | \`displaySettings?: { imageAspectRatio?: 'square' \\| 'horizontal' }\` |
876
+
877
+ **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).
878
+
879
+ ### Hook usage
880
+
881
+ \`\`\`tsx
882
+ import { useMessaging, useContextData } from '@stackable-labs/sdk-extension-react'
883
+
884
+ const { messaging } = useContextData()
885
+ const [send, { loading, error }] = useMessaging()
886
+
887
+ // Proactive gate: skip the call when there's no conversation \u2014 avoids the
888
+ // host-handled no_conversation log + null return.
889
+ const canSend = !!messaging?.conversationId
890
+
891
+ const onApprove = async () => {
892
+ try {
893
+ await send({
894
+ kind: 'text',
895
+ body: 'Order approved \u2713',
896
+ actions: [{ type: 'reply', label: 'Got it', payload: 'ACK' }],
897
+ })
898
+ } catch {
899
+ // error holds the typed SendMessageActionableErrorCode
900
+ }
901
+ }
902
+
903
+ if (error === 'rate_limited') {
904
+ // Render a "slow down" notice
905
+ }
906
+ \`\`\`
907
+
908
+ ### Imperative usage
909
+
910
+ \`\`\`tsx
911
+ const capabilities = useCapabilities()
912
+ const result = await capabilities.messaging.send({ kind: 'text', body: 'Hello' })
913
+ // result is either { messageId, receivedAt } OR { error: SendMessageErrorCode }
914
+ \`\`\`
915
+
916
+ ### Typed error codes \u2014 split by who acts
917
+
918
+ The wire taxonomy has 6 codes. The hook (\`useMessaging\`) narrows them into two groups:
919
+
920
+ **Actionable (extension catches + renders UI)** \u2014 \`send()\` throws and \`state.error\` is populated:
921
+
922
+ | Code | When |
923
+ | --- | --- |
924
+ | \`invalid_message\` | Payload failed validation (e.g. empty \`body\` for text, carousel >10 items, list kind sent) |
925
+ | \`rate_limited\` | Upstream rate limit hit \u2014 back off + retry |
926
+ | \`upstream_error\` | Provider returned 5xx \u2014 transient; retry with backoff |
927
+
928
+ **Host-handled (extension ignores)** \u2014 \`send()\` resolves to \`null\`, \`state.error\` stays \`null\`, SDK logs a breadcrumb:
929
+
930
+ | Code | What the framework does |
931
+ | --- | --- |
932
+ | \`no_conversation\` | \`console.info\` \u2014 pre-empt via \`useContextData().messaging?.conversationId\` |
933
+ | \`reauth_required\` | \`console.warn\` \u2014 admin sees a "Reconnect" CTA in the dashboard (server flips \`messagingDisconnected: true\`) |
934
+ | \`forbidden\` | \`console.warn\` \u2014 should not reach in production with correct manifest |
935
+
936
+ 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).
937
+
938
+ ### Defense-in-depth gates
939
+
940
+ Three gates fire in series, surfacing the same \`forbidden\` error if any blocks:
941
+
942
+ 1. **Embeddable host gate** (\`CapabilityRPCHandler\`) \u2014 checks \`sandbox.manifest.permissions.includes('messaging:send')\` before the call leaves the sandbox.
943
+ 2. **Backend handler gate** \u2014 checks \`claims.permissions?.includes('messaging:send')\` on the proxy-token JWT.
944
+ 3. **Server-side instance check** \u2014 \`instance.config.messagingDisconnected\` short-circuits with \`reauth_required\` before any upstream call.
945
+
946
+ ### Receiving replies \u2014 pair with \`events:messaging\`
947
+
948
+ \`reply\` and \`postback\` action clicks fire on extensions with the \`events:messaging\` permission via \`useMessagingEvent\` \u2014 see the \`events:messaging\` section above.
949
+
950
+ > **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.query\` lookup.
834
951
  `;
835
952
  };
836
953
 
@@ -1192,6 +1309,43 @@ export function Content(): React.ReactElement {
1192
1309
  <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
1193
1310
  </Surface>
1194
1311
  )
1312
+ }`,
1313
+ "messaging.send": `import { useMessaging, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
1314
+
1315
+ // Requires 'messaging:send' permission. send() throws on actionable errors
1316
+ // (state.error holds the typed code); resolves to null on host-handled cases.
1317
+ export function Content(): React.ReactElement {
1318
+ const { messaging } = useContextData()
1319
+ const [send, { loading, error }] = useMessaging()
1320
+ // Proactive gate: skip the call when there's no conversation \u2014 avoids the
1321
+ // host-handled no_conversation log + null return.
1322
+ const canSend = !!messaging?.conversationId
1323
+
1324
+ const onSayHello = async () => {
1325
+ try {
1326
+ await send({
1327
+ kind: 'text',
1328
+ body: 'Hello from the extension',
1329
+ actions: [
1330
+ { type: 'reply', label: 'Sounds good', payload: 'ACK' },
1331
+ { type: 'reply', label: 'Maybe later', payload: 'DEFER' },
1332
+ ],
1333
+ })
1334
+ } catch {
1335
+ // error holds the typed SendMessageActionableErrorCode
1336
+ }
1337
+ }
1338
+
1339
+ return (
1340
+ <Surface id="slot.content">
1341
+ <ui.Stack direction="column" gap="2" className="p-3">
1342
+ <ui.Button onClick={onSayHello} disabled={loading || !canSend}>Say hello</ui.Button>
1343
+ {(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
1344
+ {(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
1345
+ {(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
1346
+ </ui.Stack>
1347
+ </Surface>
1348
+ )
1195
1349
  }`
1196
1350
  };
1197
1351
 
@@ -1235,6 +1389,7 @@ const capabilities = useCapabilities()
1235
1389
  // capabilities.actions.toast(payload)
1236
1390
  // capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
1237
1391
  // 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.
1392
+ // capabilities.messaging.send(payload) \u2014 post a message into the active conversation bound to this Instance. Requires messaging:send permission. Author label is admin-set per-Instance (instance.config.settings.messagingDisplayName) with extension.manifest.name fallback. For React state ([send, { loading, error, data }]), prefer the useMessaging hook.
1238
1393
  \`\`\`
1239
1394
 
1240
1395
  ## useStore(store, selector?)
@@ -1351,6 +1506,35 @@ Identity state is available in the \`context.read()\` response as an \`identity\
1351
1506
  const context = await capabilities.context.read()
1352
1507
  // context.identity \u2014 { authenticated, user, expiresAt? }
1353
1508
  \`\`\`
1509
+
1510
+ ## useMessaging()
1511
+ Send messages into the active conversation bound to this Instance. Wraps the \`messaging.send\` capability with React state, tracking \`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.
1512
+
1513
+ - **Returns:** \`readonly [send, { loading, error, data }]\`
1514
+ - **\`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)
1515
+ - **\`loading: boolean\`** \u2014 true while a call is in flight (matches \`useContextData\`'s \`loading\` for SDK-wide consistency)
1516
+ - **\`data: SendMessageResponse | null\`** \u2014 \`{ messageId, receivedAt }\` from last successful send
1517
+ - **\`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.
1518
+
1519
+ Payload is discriminated by \`kind\`: \`'text'\` / \`'image'\` / \`'file'\` / \`'carousel'\`. See the \`messaging.send\` capability section for the full payload table + action types.
1520
+
1521
+ \`\`\`tsx
1522
+ import { useMessaging, useContextData } from '@stackable-labs/sdk-extension-react'
1523
+
1524
+ const { messaging } = useContextData()
1525
+ const [send, { loading, error }] = useMessaging()
1526
+
1527
+ // Proactive gate: skip the call when there's no conversation
1528
+ const canSend = !!messaging?.conversationId
1529
+
1530
+ const onApprove = async () => {
1531
+ try {
1532
+ await send({ kind: 'text', body: 'Approved \u2713' })
1533
+ } catch {
1534
+ // error holds the typed SendMessageActionableErrorCode
1535
+ }
1536
+ }
1537
+ \`\`\`
1354
1538
  `;
1355
1539
  };
1356
1540
 
@@ -3612,6 +3796,27 @@ updating custom fields.
3612
3796
  \`\`\`tsx
3613
3797
  ${EXAMPLE_SNIPPETS["actions.invoke"]}
3614
3798
  \`\`\`
3799
+
3800
+ ## messaging.send \u2014 Sending Messages to Conversations
3801
+
3802
+ Post a message into the active conversation bound to this Instance via the
3803
+ \`useMessaging\` React hook. Returns the response on happy path and throws on
3804
+ **actionable** errors \u2014 inspect \`error\` for one of the typed
3805
+ \`SendMessageActionableErrorCode\` values (\`invalid_message\`, \`rate_limited\`,
3806
+ \`upstream_error\`). Host-handled cases (\`no_conversation\`, \`reauth_required\`,
3807
+ \`forbidden\`) resolve to \`null\` without throwing; the SDK logs a breadcrumb
3808
+ and the host surfaces remediation to admins (e.g. dashboard reconnect UI) \u2014
3809
+ extensions don't catch them.
3810
+
3811
+ **Permission:** \`messaging:send\`. The author label rendered above outbound
3812
+ messages is set **per-Instance by the admin** via the Instance settings field
3813
+ \`messagingDisplayName\` (visible on the dashboard once OAuth completes);
3814
+ falls back to \`extension.manifest.name\` when blank. Extensions do not set or
3815
+ think about the author identity.
3816
+
3817
+ \`\`\`tsx
3818
+ ${EXAMPLE_SNIPPETS["messaging.send"]}
3819
+ \`\`\`
3615
3820
  `;
3616
3821
  };
3617
3822
  var generateCookbookStructural = () => {
@@ -3695,6 +3900,108 @@ ${EXAMPLE_SNIPPETS.surfaceContext}
3695
3900
  \`\`\`
3696
3901
  `;
3697
3902
  };
3903
+ var renderPayload = (kind) => JSON.stringify(MESSAGING_SEND_EXAMPLES[kind], null, 2);
3904
+ var renderAction = (type) => JSON.stringify(MESSAGING_ACTION_EXAMPLES[type], null, 2);
3905
+ var ACTION_DESCRIPTIONS = {
3906
+ 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.",
3907
+ link: "Opens `url` in a new tab/window. No bubble inserted. Freely mixable with other non-reply actions.",
3908
+ 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.",
3909
+ locationRequest: "Prompts the user to share device location. Response arrives as a separate `location`-type message inbound to the conversation. Freely mixable."
3910
+ };
3911
+ var actionTypeSections = Object.keys(MESSAGING_ACTION_EXAMPLES).map((type) => `### type: '${type}'
3912
+
3913
+ ${ACTION_DESCRIPTIONS[type]}
3914
+
3915
+ \`\`\`tsx
3916
+ ${renderAction(type)}
3917
+ \`\`\``).join("\n\n");
3918
+ var generateCookbookMessaging = () => {
3919
+ const fm = frontmatter({
3920
+ root: false,
3921
+ targets: ["*"],
3922
+ description: "Cookbook: example payloads per-kind for the messaging.send capability + action type shapes",
3923
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
3924
+ });
3925
+ return `${fm}
3926
+
3927
+ # Messaging Examples
3928
+
3929
+ Example payloads for the \`messaging.send\` capability \u2014 one per supported message
3930
+ kind, plus the action types you can attach to messages. Each payload is the
3931
+ exact shape the \`useMessaging\` hook accepts; copy and adapt as needed.
3932
+
3933
+ **Permission:** \`messaging:send\` in your \`manifest.json\`. The author label
3934
+ above outbound messages is set **per-Instance by the admin** via the Instance
3935
+ settings field \`messagingDisplayName\`; falls back to \`extension.manifest.name\`
3936
+ when blank. Extensions do not set or think about the author identity.
3937
+
3938
+ ---
3939
+
3940
+ ## kind: 'text' \u2014 Plain text + reply actions
3941
+
3942
+ Plain conversational message. Optional \`actions\` array attaches quick-reply
3943
+ buttons, links, postbacks, or location requests.
3944
+
3945
+ \`\`\`tsx
3946
+ const [send] = useMessaging()
3947
+
3948
+ await send(${renderPayload("text")})
3949
+ \`\`\`
3950
+
3951
+ ## kind: 'image' \u2014 Embedded image + optional caption
3952
+
3953
+ Product photos, screenshots, visual help. \`url\` must respond with a
3954
+ \`Content-Type: image/*\` header (Sunco enforces; redirecting URLs like
3955
+ \`picsum.photos\` will fail). \`altText\` is optional \u2014 defaults to filename.
3956
+ Optional \`body\` renders a caption alongside the image.
3957
+
3958
+ \`\`\`tsx
3959
+ const [send] = useMessaging()
3960
+
3961
+ await send(${renderPayload("image")})
3962
+ \`\`\`
3963
+
3964
+ ## kind: 'file' \u2014 Embedded file attachment
3965
+
3966
+ Return labels, receipts, NDAs, warranty paperwork. Sunco's \`fileMessage\`
3967
+ schema does NOT support \`actions\` \u2014 file messages can only carry an
3968
+ optional text caption.
3969
+
3970
+ \`\`\`tsx
3971
+ const [send] = useMessaging()
3972
+
3973
+ await send(${renderPayload("file")})
3974
+ \`\`\`
3975
+
3976
+ ## kind: 'carousel' \u2014 Horizontally scrolling cards
3977
+
3978
+ The conversational-commerce primitive: product recommendations, size/color
3979
+ pickers, search results with images, multi-option selection.
3980
+
3981
+ - 1\u201310 items, each with required \`title\` + required \`actions\` (1\u20133 per item)
3982
+ - Item actions are a subset: \`link\` + \`postback\` only (no \`reply\`, no
3983
+ \`locationRequest\` \u2014 Sunco rejects those at the item level)
3984
+ - Carousel messages have NO message-level \`actions\` field \u2014 actions live
3985
+ per-card
3986
+ - Use a separate \`text\` message before the carousel if you need an intro
3987
+
3988
+ \`\`\`tsx
3989
+ const [send] = useMessaging()
3990
+
3991
+ await send(${renderPayload("carousel")})
3992
+ \`\`\`
3993
+
3994
+ ---
3995
+
3996
+ ## Action types
3997
+
3998
+ Attach to message-level \`actions\` (text / image) or item-level \`actions\`
3999
+ (carousel). All actions share \`label\` + optional \`metadata\` (primitives only,
4000
+ \u22644KB total).
4001
+
4002
+ ${actionTypeSections}
4003
+ `;
4004
+ };
3698
4005
  var identityEventTypes2 = Object.values(IDENTITY_EVENT).map((e) => `\`${e}\``).join(", ");
3699
4006
  var activityEventTypes2 = Object.values(ACTIVITY_EVENT).map((e) => `\`${e}\``).join(", ");
3700
4007
  var generateCookbookEvents = () => {
@@ -3716,7 +4023,7 @@ supports both a login-time hook (\`useExtendIdentity\`) and an imperative post-l
3716
4023
 
3717
4024
  ## Messaging Events
3718
4025
 
3719
- Subscribe to postback button clicks from the Zendesk messaging widget.
4026
+ Subscribe to postback button clicks from the Messaging widget.
3720
4027
  The \`actionName\` is the button's display text, not a programmatic identifier.
3721
4028
 
3722
4029
  **Permission:** \`events:messaging\`
@@ -3929,6 +4236,23 @@ await capabilities.identity.extend({ verified: true })
3929
4236
  useIdentityEvent('refresh', (event) => {
3930
4237
  console.log('verified =>', event.data.state.user?.metadata?.verified)
3931
4238
  })
4239
+ `,
4240
+ "messaging.send": `
4241
+ // Send a message into the active conversation bound to this Instance.
4242
+ // Requires the 'messaging:send' permission. The author label rendered above
4243
+ // outbound messages is set per-Instance by the admin (Instance settings
4244
+ // messagingDisplayName); falls back to extension.manifest.name when blank.
4245
+ const [send, { loading, error }] = useMessaging()
4246
+ const result = await send({ kind: 'text', body: 'Hello from the extension' })
4247
+
4248
+ // Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
4249
+ // 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
4250
+ // catch / on state.error to show UX.
4251
+ // Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
4252
+ // resolve to null without throwing; the SDK logs a breadcrumb and the host
4253
+ // surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
4254
+ // can pre-empt 'no_conversation' via useContextData().messaging?.conversationId.
4255
+ if (error === 'rate_limited') { /* show "slow down" UI */ }
3932
4256
  `
3933
4257
  };
3934
4258
  var EVENT_SNIPPETS = {
@@ -4582,6 +4906,13 @@ var SKILLS = [
4582
4906
  scopes: ["docs"],
4583
4907
  content: () => generateCookbookEvents()
4584
4908
  },
4909
+ {
4910
+ id: "cookbook-messaging",
4911
+ 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.",
4912
+ type: "knowledge",
4913
+ scopes: ["docs"],
4914
+ content: () => generateCookbookMessaging()
4915
+ },
4585
4916
  {
4586
4917
  id: "marketplace-listing",
4587
4918
  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 +5382,43 @@ export function Content() {
5051
5382
  <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
5052
5383
  </Surface>
5053
5384
  )
5385
+ }`,
5386
+ "messaging.send": `import { useMessaging, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
5387
+
5388
+ // Requires 'messaging:send' permission. send() throws on actionable errors
5389
+ // (state.error holds the typed code); resolves to null on host-handled cases.
5390
+ export function Content() {
5391
+ const { messaging } = useContextData()
5392
+ const [send, { loading, error }] = useMessaging()
5393
+ // Proactive gate: skip the call when there's no conversation \u2014 avoids the
5394
+ // host-handled no_conversation log + null return.
5395
+ const canSend = !!messaging?.conversationId
5396
+
5397
+ const onSayHello = async () => {
5398
+ try {
5399
+ await send({
5400
+ kind: 'text',
5401
+ body: 'Hello from the extension',
5402
+ actions: [
5403
+ { type: 'reply', label: 'Sounds good', payload: 'ACK' },
5404
+ { type: 'reply', label: 'Maybe later', payload: 'DEFER' },
5405
+ ],
5406
+ })
5407
+ } catch {
5408
+ // error holds the typed SendMessageActionableErrorCode
5409
+ }
5410
+ }
5411
+
5412
+ return (
5413
+ <Surface id="slot.content">
5414
+ <ui.Stack direction="column" gap="2" className="p-3">
5415
+ <ui.Button onClick={onSayHello} disabled={loading || !canSend}>Say hello</ui.Button>
5416
+ {(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
5417
+ {(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
5418
+ {(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
5419
+ </ui.Stack>
5420
+ </Surface>
5421
+ )
5054
5422
  }`
5055
5423
  };
5056
5424
 
@@ -5142,7 +5510,22 @@ await capabilities.identity.extend({ verified: true })
5142
5510
  // Consumer side \u2014 same or sibling extension reacts via identity:refresh
5143
5511
  useIdentityEvent('refresh', (event) => {
5144
5512
  console.log('verified =>', event.data.state.user?.metadata?.verified)
5145
- })`
5513
+ })`,
5514
+ "messaging.send": `// Send a message into the active conversation bound to this Instance.
5515
+ // Requires the 'messaging:send' permission. The author label rendered above
5516
+ // outbound messages is set per-Instance by the admin (Instance settings
5517
+ // messagingDisplayName); falls back to extension.manifest.name when blank.
5518
+ const [send, { loading, error }] = useMessaging()
5519
+ const result = await send({ kind: 'text', body: 'Hello from the extension' })
5520
+
5521
+ // Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
5522
+ // 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
5523
+ // catch / on state.error to show UX.
5524
+ // Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
5525
+ // resolve to null without throwing; the SDK logs a breadcrumb and the host
5526
+ // surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
5527
+ // can pre-empt 'no_conversation' via useContextData().messaging?.conversationId.
5528
+ if (error === 'rate_limited') { /* show "slow down" UI */ }`
5146
5529
  };
5147
5530
  var EVENT_SNIPPETS2 = {
5148
5531
  "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- 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';
2
+ import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, MESSAGING_SEND_EXAMPLES } from '@stackable-labs/sdk-extension-contracts';
3
3
  import { validatePatternFormat } from '@stackable-labs/lib-contracts';
4
4
  import { readFile } from 'fs/promises';
5
5
  import { join } from 'path';
@@ -534,7 +534,7 @@ var generateCapabilities = () => {
534
534
  const fm = frontmatter({
535
535
  root: false,
536
536
  targets: ["*"],
537
- description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity",
537
+ description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, messaging.send, events:identity, events:messaging, events:activity",
538
538
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
539
539
  });
540
540
  return `${fm}
@@ -791,7 +791,7 @@ await capabilities.identity.extend({
791
791
  // \u2192 identity:refresh broadcast to all extensions with events:identity
792
792
  \`\`\`
793
793
 
794
- Per-extension calls are serialized to prevent concurrent Zendesk \`loginUser\` callbacks from orphaning each other (empirically verified in the loginUser re-auth spike).
794
+ Per-extension calls are serialized to prevent concurrent \`loginUser\` callbacks from orphaning each other.
795
795
 
796
796
  ### Consuming enriched state from another extension
797
797
 
@@ -824,6 +824,123 @@ The publisher-side bundle scan validates your declaration at submission time:
824
824
  | \`identityClaims_reserved_key\` | error | Collides with a reserved JWT / Zendesk claim |
825
825
  | \`identityClaims_standard_key\` | warning | Redundant \u2014 standard claims (\`external_id\`, \`email\`, \`name\`) are exempt |
826
826
  | \`identityClaims_too_many\` | error | More than 20 entries |
827
+
828
+ ## messaging.send \u2014 Send Messages to Conversations
829
+
830
+ 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:
831
+
832
+ 1. **\`useMessaging()\`** \u2014 React hook returning tuple \`[send, { 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.
833
+ 2. **\`messagingSendCapability(payload)\`** / **\`capabilities.messaging.send(payload)\`** \u2014 imperative; useful outside React render. Exposes the full wire-level error taxonomy.
834
+
835
+ ### Manifest contract
836
+
837
+ \`\`\`json
838
+ {
839
+ "permissions": ["messaging:send"]
840
+ }
841
+ \`\`\`
842
+
843
+ - **\`messaging:send\` permission** \u2014 without it the host gate rejects the call before any network request.
844
+
845
+ ### Author label (host-resolved, per-Instance)
846
+
847
+ Extensions do **not** set the author label. The host resolves it from:
848
+ 1. \`instance.config.settings.messagingDisplayName\` \u2014 admin sets via the dashboard Instance form (visible only after OAuth completes; gated on \`messagingAppId\` being populated)
849
+ 2. **Fallback**: \`extension.manifest.name\` \u2014 used when admin hasn't set a label
850
+
851
+ This prevents impersonation (extension A can't author as extension B) and gives admins per-deployment branding control.
852
+
853
+ ### Runtime requirements
854
+
855
+ 1. **Instance connected to a messaging provider** \u2014 bearer token stored, ready to post
856
+ 2. **An active conversation** when \`send()\` is called (else host-handled \`no_conversation\` \u2014 \`send()\` returns \`null\`)
857
+ 3. **Instance not disconnected** from the messaging provider (else host-handled \`reauth_required\` \u2014 admin reconnect surfaced in dashboard)
858
+
859
+ ### Payload \u2014 discriminated by \`kind\`
860
+
861
+ Four kinds:
862
+
863
+ | kind | Required fields | Optional |
864
+ |---|---|---|
865
+ | \`'text'\` | \`body: string\` | \`actions?: MessageItemAction[]\`, \`metadata?\`, \`htmlText?\`, \`markdownText?\`, \`disableUserInput?\` |
866
+ | \`'image'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`actions?\`, \`metadata?\` |
867
+ | \`'file'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`metadata?\` (no \`actions\` \u2014 file kind doesn't accept item-level actions) |
868
+ | \`'carousel'\` | \`items: [MessageItem, ...MessageItem[]]\` (1\u201310 items, each with \`title\`, \`description?\`, \`imageUrl?\`, \`actions?\`) | \`displaySettings?: { imageAspectRatio?: 'square' \\| 'horizontal' }\` |
869
+
870
+ **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).
871
+
872
+ ### Hook usage
873
+
874
+ \`\`\`tsx
875
+ import { useMessaging, useContextData } from '@stackable-labs/sdk-extension-react'
876
+
877
+ const { messaging } = useContextData()
878
+ const [send, { loading, error }] = useMessaging()
879
+
880
+ // Proactive gate: skip the call when there's no conversation \u2014 avoids the
881
+ // host-handled no_conversation log + null return.
882
+ const canSend = !!messaging?.conversationId
883
+
884
+ const onApprove = async () => {
885
+ try {
886
+ await send({
887
+ kind: 'text',
888
+ body: 'Order approved \u2713',
889
+ actions: [{ type: 'reply', label: 'Got it', payload: 'ACK' }],
890
+ })
891
+ } catch {
892
+ // error holds the typed SendMessageActionableErrorCode
893
+ }
894
+ }
895
+
896
+ if (error === 'rate_limited') {
897
+ // Render a "slow down" notice
898
+ }
899
+ \`\`\`
900
+
901
+ ### Imperative usage
902
+
903
+ \`\`\`tsx
904
+ const capabilities = useCapabilities()
905
+ const result = await capabilities.messaging.send({ kind: 'text', body: 'Hello' })
906
+ // result is either { messageId, receivedAt } OR { error: SendMessageErrorCode }
907
+ \`\`\`
908
+
909
+ ### Typed error codes \u2014 split by who acts
910
+
911
+ The wire taxonomy has 6 codes. The hook (\`useMessaging\`) narrows them into two groups:
912
+
913
+ **Actionable (extension catches + renders UI)** \u2014 \`send()\` throws and \`state.error\` is populated:
914
+
915
+ | Code | When |
916
+ | --- | --- |
917
+ | \`invalid_message\` | Payload failed validation (e.g. empty \`body\` for text, carousel >10 items, list kind sent) |
918
+ | \`rate_limited\` | Upstream rate limit hit \u2014 back off + retry |
919
+ | \`upstream_error\` | Provider returned 5xx \u2014 transient; retry with backoff |
920
+
921
+ **Host-handled (extension ignores)** \u2014 \`send()\` resolves to \`null\`, \`state.error\` stays \`null\`, SDK logs a breadcrumb:
922
+
923
+ | Code | What the framework does |
924
+ | --- | --- |
925
+ | \`no_conversation\` | \`console.info\` \u2014 pre-empt via \`useContextData().messaging?.conversationId\` |
926
+ | \`reauth_required\` | \`console.warn\` \u2014 admin sees a "Reconnect" CTA in the dashboard (server flips \`messagingDisconnected: true\`) |
927
+ | \`forbidden\` | \`console.warn\` \u2014 should not reach in production with correct manifest |
928
+
929
+ 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).
930
+
931
+ ### Defense-in-depth gates
932
+
933
+ Three gates fire in series, surfacing the same \`forbidden\` error if any blocks:
934
+
935
+ 1. **Embeddable host gate** (\`CapabilityRPCHandler\`) \u2014 checks \`sandbox.manifest.permissions.includes('messaging:send')\` before the call leaves the sandbox.
936
+ 2. **Backend handler gate** \u2014 checks \`claims.permissions?.includes('messaging:send')\` on the proxy-token JWT.
937
+ 3. **Server-side instance check** \u2014 \`instance.config.messagingDisconnected\` short-circuits with \`reauth_required\` before any upstream call.
938
+
939
+ ### Receiving replies \u2014 pair with \`events:messaging\`
940
+
941
+ \`reply\` and \`postback\` action clicks fire on extensions with the \`events:messaging\` permission via \`useMessagingEvent\` \u2014 see the \`events:messaging\` section above.
942
+
943
+ > **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.query\` lookup.
827
944
  `;
828
945
  };
829
946
 
@@ -1185,6 +1302,43 @@ export function Content(): React.ReactElement {
1185
1302
  <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
1186
1303
  </Surface>
1187
1304
  )
1305
+ }`,
1306
+ "messaging.send": `import { useMessaging, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
1307
+
1308
+ // Requires 'messaging:send' permission. send() throws on actionable errors
1309
+ // (state.error holds the typed code); resolves to null on host-handled cases.
1310
+ export function Content(): React.ReactElement {
1311
+ const { messaging } = useContextData()
1312
+ const [send, { loading, error }] = useMessaging()
1313
+ // Proactive gate: skip the call when there's no conversation \u2014 avoids the
1314
+ // host-handled no_conversation log + null return.
1315
+ const canSend = !!messaging?.conversationId
1316
+
1317
+ const onSayHello = async () => {
1318
+ try {
1319
+ await send({
1320
+ kind: 'text',
1321
+ body: 'Hello from the extension',
1322
+ actions: [
1323
+ { type: 'reply', label: 'Sounds good', payload: 'ACK' },
1324
+ { type: 'reply', label: 'Maybe later', payload: 'DEFER' },
1325
+ ],
1326
+ })
1327
+ } catch {
1328
+ // error holds the typed SendMessageActionableErrorCode
1329
+ }
1330
+ }
1331
+
1332
+ return (
1333
+ <Surface id="slot.content">
1334
+ <ui.Stack direction="column" gap="2" className="p-3">
1335
+ <ui.Button onClick={onSayHello} disabled={loading || !canSend}>Say hello</ui.Button>
1336
+ {(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
1337
+ {(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
1338
+ {(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
1339
+ </ui.Stack>
1340
+ </Surface>
1341
+ )
1188
1342
  }`
1189
1343
  };
1190
1344
 
@@ -1228,6 +1382,7 @@ const capabilities = useCapabilities()
1228
1382
  // capabilities.actions.toast(payload)
1229
1383
  // capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
1230
1384
  // 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.
1385
+ // capabilities.messaging.send(payload) \u2014 post a message into the active conversation bound to this Instance. Requires messaging:send permission. Author label is admin-set per-Instance (instance.config.settings.messagingDisplayName) with extension.manifest.name fallback. For React state ([send, { loading, error, data }]), prefer the useMessaging hook.
1231
1386
  \`\`\`
1232
1387
 
1233
1388
  ## useStore(store, selector?)
@@ -1344,6 +1499,35 @@ Identity state is available in the \`context.read()\` response as an \`identity\
1344
1499
  const context = await capabilities.context.read()
1345
1500
  // context.identity \u2014 { authenticated, user, expiresAt? }
1346
1501
  \`\`\`
1502
+
1503
+ ## useMessaging()
1504
+ Send messages into the active conversation bound to this Instance. Wraps the \`messaging.send\` capability with React state, tracking \`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.
1505
+
1506
+ - **Returns:** \`readonly [send, { loading, error, data }]\`
1507
+ - **\`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)
1508
+ - **\`loading: boolean\`** \u2014 true while a call is in flight (matches \`useContextData\`'s \`loading\` for SDK-wide consistency)
1509
+ - **\`data: SendMessageResponse | null\`** \u2014 \`{ messageId, receivedAt }\` from last successful send
1510
+ - **\`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.
1511
+
1512
+ Payload is discriminated by \`kind\`: \`'text'\` / \`'image'\` / \`'file'\` / \`'carousel'\`. See the \`messaging.send\` capability section for the full payload table + action types.
1513
+
1514
+ \`\`\`tsx
1515
+ import { useMessaging, useContextData } from '@stackable-labs/sdk-extension-react'
1516
+
1517
+ const { messaging } = useContextData()
1518
+ const [send, { loading, error }] = useMessaging()
1519
+
1520
+ // Proactive gate: skip the call when there's no conversation
1521
+ const canSend = !!messaging?.conversationId
1522
+
1523
+ const onApprove = async () => {
1524
+ try {
1525
+ await send({ kind: 'text', body: 'Approved \u2713' })
1526
+ } catch {
1527
+ // error holds the typed SendMessageActionableErrorCode
1528
+ }
1529
+ }
1530
+ \`\`\`
1347
1531
  `;
1348
1532
  };
1349
1533
 
@@ -3605,6 +3789,27 @@ updating custom fields.
3605
3789
  \`\`\`tsx
3606
3790
  ${EXAMPLE_SNIPPETS["actions.invoke"]}
3607
3791
  \`\`\`
3792
+
3793
+ ## messaging.send \u2014 Sending Messages to Conversations
3794
+
3795
+ Post a message into the active conversation bound to this Instance via the
3796
+ \`useMessaging\` React hook. Returns the response on happy path and throws on
3797
+ **actionable** errors \u2014 inspect \`error\` for one of the typed
3798
+ \`SendMessageActionableErrorCode\` values (\`invalid_message\`, \`rate_limited\`,
3799
+ \`upstream_error\`). Host-handled cases (\`no_conversation\`, \`reauth_required\`,
3800
+ \`forbidden\`) resolve to \`null\` without throwing; the SDK logs a breadcrumb
3801
+ and the host surfaces remediation to admins (e.g. dashboard reconnect UI) \u2014
3802
+ extensions don't catch them.
3803
+
3804
+ **Permission:** \`messaging:send\`. The author label rendered above outbound
3805
+ messages is set **per-Instance by the admin** via the Instance settings field
3806
+ \`messagingDisplayName\` (visible on the dashboard once OAuth completes);
3807
+ falls back to \`extension.manifest.name\` when blank. Extensions do not set or
3808
+ think about the author identity.
3809
+
3810
+ \`\`\`tsx
3811
+ ${EXAMPLE_SNIPPETS["messaging.send"]}
3812
+ \`\`\`
3608
3813
  `;
3609
3814
  };
3610
3815
  var generateCookbookStructural = () => {
@@ -3688,6 +3893,108 @@ ${EXAMPLE_SNIPPETS.surfaceContext}
3688
3893
  \`\`\`
3689
3894
  `;
3690
3895
  };
3896
+ var renderPayload = (kind) => JSON.stringify(MESSAGING_SEND_EXAMPLES[kind], null, 2);
3897
+ var renderAction = (type) => JSON.stringify(MESSAGING_ACTION_EXAMPLES[type], null, 2);
3898
+ var ACTION_DESCRIPTIONS = {
3899
+ 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.",
3900
+ link: "Opens `url` in a new tab/window. No bubble inserted. Freely mixable with other non-reply actions.",
3901
+ 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.",
3902
+ locationRequest: "Prompts the user to share device location. Response arrives as a separate `location`-type message inbound to the conversation. Freely mixable."
3903
+ };
3904
+ var actionTypeSections = Object.keys(MESSAGING_ACTION_EXAMPLES).map((type) => `### type: '${type}'
3905
+
3906
+ ${ACTION_DESCRIPTIONS[type]}
3907
+
3908
+ \`\`\`tsx
3909
+ ${renderAction(type)}
3910
+ \`\`\``).join("\n\n");
3911
+ var generateCookbookMessaging = () => {
3912
+ const fm = frontmatter({
3913
+ root: false,
3914
+ targets: ["*"],
3915
+ description: "Cookbook: example payloads per-kind for the messaging.send capability + action type shapes",
3916
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
3917
+ });
3918
+ return `${fm}
3919
+
3920
+ # Messaging Examples
3921
+
3922
+ Example payloads for the \`messaging.send\` capability \u2014 one per supported message
3923
+ kind, plus the action types you can attach to messages. Each payload is the
3924
+ exact shape the \`useMessaging\` hook accepts; copy and adapt as needed.
3925
+
3926
+ **Permission:** \`messaging:send\` in your \`manifest.json\`. The author label
3927
+ above outbound messages is set **per-Instance by the admin** via the Instance
3928
+ settings field \`messagingDisplayName\`; falls back to \`extension.manifest.name\`
3929
+ when blank. Extensions do not set or think about the author identity.
3930
+
3931
+ ---
3932
+
3933
+ ## kind: 'text' \u2014 Plain text + reply actions
3934
+
3935
+ Plain conversational message. Optional \`actions\` array attaches quick-reply
3936
+ buttons, links, postbacks, or location requests.
3937
+
3938
+ \`\`\`tsx
3939
+ const [send] = useMessaging()
3940
+
3941
+ await send(${renderPayload("text")})
3942
+ \`\`\`
3943
+
3944
+ ## kind: 'image' \u2014 Embedded image + optional caption
3945
+
3946
+ Product photos, screenshots, visual help. \`url\` must respond with a
3947
+ \`Content-Type: image/*\` header (Sunco enforces; redirecting URLs like
3948
+ \`picsum.photos\` will fail). \`altText\` is optional \u2014 defaults to filename.
3949
+ Optional \`body\` renders a caption alongside the image.
3950
+
3951
+ \`\`\`tsx
3952
+ const [send] = useMessaging()
3953
+
3954
+ await send(${renderPayload("image")})
3955
+ \`\`\`
3956
+
3957
+ ## kind: 'file' \u2014 Embedded file attachment
3958
+
3959
+ Return labels, receipts, NDAs, warranty paperwork. Sunco's \`fileMessage\`
3960
+ schema does NOT support \`actions\` \u2014 file messages can only carry an
3961
+ optional text caption.
3962
+
3963
+ \`\`\`tsx
3964
+ const [send] = useMessaging()
3965
+
3966
+ await send(${renderPayload("file")})
3967
+ \`\`\`
3968
+
3969
+ ## kind: 'carousel' \u2014 Horizontally scrolling cards
3970
+
3971
+ The conversational-commerce primitive: product recommendations, size/color
3972
+ pickers, search results with images, multi-option selection.
3973
+
3974
+ - 1\u201310 items, each with required \`title\` + required \`actions\` (1\u20133 per item)
3975
+ - Item actions are a subset: \`link\` + \`postback\` only (no \`reply\`, no
3976
+ \`locationRequest\` \u2014 Sunco rejects those at the item level)
3977
+ - Carousel messages have NO message-level \`actions\` field \u2014 actions live
3978
+ per-card
3979
+ - Use a separate \`text\` message before the carousel if you need an intro
3980
+
3981
+ \`\`\`tsx
3982
+ const [send] = useMessaging()
3983
+
3984
+ await send(${renderPayload("carousel")})
3985
+ \`\`\`
3986
+
3987
+ ---
3988
+
3989
+ ## Action types
3990
+
3991
+ Attach to message-level \`actions\` (text / image) or item-level \`actions\`
3992
+ (carousel). All actions share \`label\` + optional \`metadata\` (primitives only,
3993
+ \u22644KB total).
3994
+
3995
+ ${actionTypeSections}
3996
+ `;
3997
+ };
3691
3998
  var identityEventTypes2 = Object.values(IDENTITY_EVENT).map((e) => `\`${e}\``).join(", ");
3692
3999
  var activityEventTypes2 = Object.values(ACTIVITY_EVENT).map((e) => `\`${e}\``).join(", ");
3693
4000
  var generateCookbookEvents = () => {
@@ -3709,7 +4016,7 @@ supports both a login-time hook (\`useExtendIdentity\`) and an imperative post-l
3709
4016
 
3710
4017
  ## Messaging Events
3711
4018
 
3712
- Subscribe to postback button clicks from the Zendesk messaging widget.
4019
+ Subscribe to postback button clicks from the Messaging widget.
3713
4020
  The \`actionName\` is the button's display text, not a programmatic identifier.
3714
4021
 
3715
4022
  **Permission:** \`events:messaging\`
@@ -3922,6 +4229,23 @@ await capabilities.identity.extend({ verified: true })
3922
4229
  useIdentityEvent('refresh', (event) => {
3923
4230
  console.log('verified =>', event.data.state.user?.metadata?.verified)
3924
4231
  })
4232
+ `,
4233
+ "messaging.send": `
4234
+ // Send a message into the active conversation bound to this Instance.
4235
+ // Requires the 'messaging:send' permission. The author label rendered above
4236
+ // outbound messages is set per-Instance by the admin (Instance settings
4237
+ // messagingDisplayName); falls back to extension.manifest.name when blank.
4238
+ const [send, { loading, error }] = useMessaging()
4239
+ const result = await send({ kind: 'text', body: 'Hello from the extension' })
4240
+
4241
+ // Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
4242
+ // 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
4243
+ // catch / on state.error to show UX.
4244
+ // Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
4245
+ // resolve to null without throwing; the SDK logs a breadcrumb and the host
4246
+ // surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
4247
+ // can pre-empt 'no_conversation' via useContextData().messaging?.conversationId.
4248
+ if (error === 'rate_limited') { /* show "slow down" UI */ }
3925
4249
  `
3926
4250
  };
3927
4251
  var EVENT_SNIPPETS = {
@@ -4575,6 +4899,13 @@ var SKILLS = [
4575
4899
  scopes: ["docs"],
4576
4900
  content: () => generateCookbookEvents()
4577
4901
  },
4902
+ {
4903
+ id: "cookbook-messaging",
4904
+ 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.",
4905
+ type: "knowledge",
4906
+ scopes: ["docs"],
4907
+ content: () => generateCookbookMessaging()
4908
+ },
4578
4909
  {
4579
4910
  id: "marketplace-listing",
4580
4911
  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.",
@@ -5044,6 +5375,43 @@ export function Content() {
5044
5375
  <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
5045
5376
  </Surface>
5046
5377
  )
5378
+ }`,
5379
+ "messaging.send": `import { useMessaging, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
5380
+
5381
+ // Requires 'messaging:send' permission. send() throws on actionable errors
5382
+ // (state.error holds the typed code); resolves to null on host-handled cases.
5383
+ export function Content() {
5384
+ const { messaging } = useContextData()
5385
+ const [send, { loading, error }] = useMessaging()
5386
+ // Proactive gate: skip the call when there's no conversation \u2014 avoids the
5387
+ // host-handled no_conversation log + null return.
5388
+ const canSend = !!messaging?.conversationId
5389
+
5390
+ const onSayHello = async () => {
5391
+ try {
5392
+ await send({
5393
+ kind: 'text',
5394
+ body: 'Hello from the extension',
5395
+ actions: [
5396
+ { type: 'reply', label: 'Sounds good', payload: 'ACK' },
5397
+ { type: 'reply', label: 'Maybe later', payload: 'DEFER' },
5398
+ ],
5399
+ })
5400
+ } catch {
5401
+ // error holds the typed SendMessageActionableErrorCode
5402
+ }
5403
+ }
5404
+
5405
+ return (
5406
+ <Surface id="slot.content">
5407
+ <ui.Stack direction="column" gap="2" className="p-3">
5408
+ <ui.Button onClick={onSayHello} disabled={loading || !canSend}>Say hello</ui.Button>
5409
+ {(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
5410
+ {(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
5411
+ {(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
5412
+ </ui.Stack>
5413
+ </Surface>
5414
+ )
5047
5415
  }`
5048
5416
  };
5049
5417
 
@@ -5135,7 +5503,22 @@ await capabilities.identity.extend({ verified: true })
5135
5503
  // Consumer side \u2014 same or sibling extension reacts via identity:refresh
5136
5504
  useIdentityEvent('refresh', (event) => {
5137
5505
  console.log('verified =>', event.data.state.user?.metadata?.verified)
5138
- })`
5506
+ })`,
5507
+ "messaging.send": `// Send a message into the active conversation bound to this Instance.
5508
+ // Requires the 'messaging:send' permission. The author label rendered above
5509
+ // outbound messages is set per-Instance by the admin (Instance settings
5510
+ // messagingDisplayName); falls back to extension.manifest.name when blank.
5511
+ const [send, { loading, error }] = useMessaging()
5512
+ const result = await send({ kind: 'text', body: 'Hello from the extension' })
5513
+
5514
+ // Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
5515
+ // 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
5516
+ // catch / on state.error to show UX.
5517
+ // Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
5518
+ // resolve to null without throwing; the SDK logs a breadcrumb and the host
5519
+ // surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
5520
+ // can pre-empt 'no_conversation' via useContextData().messaging?.conversationId.
5521
+ if (error === 'rate_limited') { /* show "slow down" UI */ }`
5139
5522
  };
5140
5523
  var EVENT_SNIPPETS2 = {
5141
5524
  "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackable-labs/mcp-app-extension",
3
- "version": "1.28.0",
3
+ "version": "1.30.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mcp-app-extension": "./dist/index.js"