@stackable-labs/mcp-app-extension 0.2.0 → 0.3.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 +191 -182
  2. package/dist/server.js +3985 -0
  3. package/package.json +11 -1
package/dist/server.js ADDED
@@ -0,0 +1,3985 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { IDENTITY_EVENTS, ACTIVITY_EVENTS, SURFACE_TARGETS, PERMISSIONS, CAPABILITY_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
3
+ import { z } from 'zod';
4
+
5
+ // src/server.ts
6
+
7
+ // ../../sdk/extension/ai-docs/src/generated/template-content.ts
8
+ var PATTERN_SECTIONS = [
9
+ {
10
+ path: "index.tsx",
11
+ title: "Entry Point",
12
+ code: `import { createExtension } from '@stackable-labs/sdk-extension-react'
13
+ import { Header } from './surfaces/Header'
14
+ import { Content } from './surfaces/Content'
15
+ import { Footer } from './surfaces/Footer'
16
+
17
+ const Extension = () => (
18
+ <>
19
+ <Header />
20
+ <Content />
21
+ <Footer />
22
+ </>
23
+ )
24
+
25
+ createExtension(() => <Extension />, { extensionId: '__EXTENSION_ID__' })`
26
+ },
27
+ {
28
+ path: "store.ts",
29
+ title: "Store-Based Navigation",
30
+ code: `import { createStore } from '@stackable-labs/sdk-extension-react'
31
+
32
+ export type ViewState = { type: 'menu' }
33
+
34
+ export interface AppState {
35
+ viewState: ViewState
36
+ }
37
+
38
+ export const appStore = createStore<AppState>({
39
+ viewState: { type: 'menu' },
40
+ })`
41
+ },
42
+ {
43
+ path: "surfaces/Content.tsx",
44
+ title: "Content Surface with Loading State",
45
+ code: `import { ui, useStore, useContextData, Surface } from '@stackable-labs/sdk-extension-react'
46
+ import { appStore } from '../store'
47
+
48
+ export function Content() {
49
+ const viewState = useStore(appStore, (s) => s.viewState)
50
+ const { loading } = useContextData()
51
+
52
+ if (loading) {
53
+ return (
54
+ <Surface id="slot.content">
55
+ <ui.Stack direction="column" gap="2" className="animate-pulse">
56
+ <ui.Card className="h-24" />
57
+ <ui.Card className="h-32" />
58
+ </ui.Stack>
59
+ </Surface>
60
+ )
61
+ }
62
+
63
+ return (
64
+ <Surface id="slot.content">
65
+ {viewState.type === 'menu' && (
66
+ <ui.Menu>
67
+ {/* Add ui.MenuItem entries here */}
68
+ </ui.Menu>
69
+ )}
70
+ </Surface>
71
+ )
72
+ }`
73
+ },
74
+ {
75
+ path: "surfaces/Footer.tsx",
76
+ title: "Surface Composition",
77
+ code: `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
78
+
79
+ export function Footer() {
80
+ return (
81
+ <>
82
+ <Surface id="slot.footer">
83
+ <ui.Text className="text-xs">Powered by My Extension</ui.Text>
84
+ </Surface>
85
+ <Surface id="slot.footer-links">
86
+ <ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>
87
+ </Surface>
88
+ </>
89
+ )
90
+ }`
91
+ },
92
+ {
93
+ path: "surfaces/Header.tsx",
94
+ title: "Simple Surface",
95
+ code: `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
96
+
97
+ export function Header() {
98
+ return (
99
+ <Surface id="slot.header">
100
+ <ui.Text>Header content goes here</ui.Text>
101
+ </Surface>
102
+ )
103
+ }`
104
+ },
105
+ {
106
+ path: "lib/api.ts",
107
+ title: "API Wrapper (data.query + data.fetch)",
108
+ code: `/**
109
+ * API wrapper patterns \u2014 choose one based on your integration model.
110
+ *
111
+ * data.query \u2192 host-mediated: the host handles the API call, extension sends
112
+ * an action name + params, host returns data.
113
+ * Permission: "data:query"
114
+ *
115
+ * data.fetch \u2192 direct HTTP: the extension calls external APIs through the
116
+ * platform proxy. Domains must be in allowedDomains in manifest.
117
+ * Permission: "data:fetch"
118
+ *
119
+ * Usage in a surface:
120
+ * const capabilities = useCapabilities()
121
+ * const api = createApi(capabilities.data.query)
122
+ * // or: const api = createFetchApi(capabilities.data.fetch)
123
+ */
124
+
125
+ import type { ApiRequest, FetchRequestInit, FetchResponse } from '@stackable-labs/sdk-extension-contracts'
126
+
127
+ type QueryFn = <T = unknown>(payload: ApiRequest) => Promise<T>
128
+ type FetchFn = (url: string, init?: FetchRequestInit) => Promise<FetchResponse>
129
+
130
+ // \u2500\u2500 data.query wrapper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
131
+
132
+ export function createApi(query: QueryFn) {
133
+ return {
134
+ async getItems(): Promise<unknown[]> {
135
+ return query<unknown[]>({ action: 'getItems' })
136
+ },
137
+
138
+ async getItem(itemId: string): Promise<unknown> {
139
+ return query<unknown>({ action: 'getItem', itemId })
140
+ },
141
+ }
142
+ }
143
+
144
+ // \u2500\u2500 data.fetch wrapper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
145
+
146
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string
147
+
148
+ export function createFetchApi(fetch: FetchFn) {
149
+ return {
150
+ async getItems(): Promise<unknown[]> {
151
+ const result = await fetch(\`\${API_BASE_URL}/items\`, { method: 'GET' })
152
+ if (!result.ok) throw new Error(\`getItems failed: \${result.status}\`)
153
+ return result.data as unknown[]
154
+ },
155
+
156
+ async getItem(itemId: string): Promise<unknown> {
157
+ const result = await fetch(\`\${API_BASE_URL}/items/\${itemId}\`, { method: 'GET' })
158
+ if (!result.ok) throw new Error(\`getItem failed: \${result.status}\`)
159
+ return result.data as unknown
160
+ },
161
+ }
162
+ }`
163
+ }
164
+ ];
165
+ var RECIPE_SECTIONS = [
166
+ {
167
+ path: "store.ts",
168
+ title: "Current Store & View State",
169
+ code: `import { createStore } from '@stackable-labs/sdk-extension-react'
170
+
171
+ export type ViewState = { type: 'menu' }
172
+
173
+ export interface AppState {
174
+ viewState: ViewState
175
+ }
176
+
177
+ export const appStore = createStore<AppState>({
178
+ viewState: { type: 'menu' },
179
+ })`
180
+ },
181
+ {
182
+ path: "surfaces/Content.tsx",
183
+ title: "Current Content Surface",
184
+ code: `import { ui, useStore, useContextData, Surface } from '@stackable-labs/sdk-extension-react'
185
+ import { appStore } from '../store'
186
+
187
+ export function Content() {
188
+ const viewState = useStore(appStore, (s) => s.viewState)
189
+ const { loading } = useContextData()
190
+
191
+ if (loading) {
192
+ return (
193
+ <Surface id="slot.content">
194
+ <ui.Stack direction="column" gap="2" className="animate-pulse">
195
+ <ui.Card className="h-24" />
196
+ <ui.Card className="h-32" />
197
+ </ui.Stack>
198
+ </Surface>
199
+ )
200
+ }
201
+
202
+ return (
203
+ <Surface id="slot.content">
204
+ {viewState.type === 'menu' && (
205
+ <ui.Menu>
206
+ {/* Add ui.MenuItem entries here */}
207
+ </ui.Menu>
208
+ )}
209
+ </Surface>
210
+ )
211
+ }`
212
+ },
213
+ {
214
+ path: "lib/api.ts",
215
+ title: "Current API Wrapper",
216
+ code: `/**
217
+ * API wrapper patterns \u2014 choose one based on your integration model.
218
+ *
219
+ * data.query \u2192 host-mediated: the host handles the API call, extension sends
220
+ * an action name + params, host returns data.
221
+ * Permission: "data:query"
222
+ *
223
+ * data.fetch \u2192 direct HTTP: the extension calls external APIs through the
224
+ * platform proxy. Domains must be in allowedDomains in manifest.
225
+ * Permission: "data:fetch"
226
+ *
227
+ * Usage in a surface:
228
+ * const capabilities = useCapabilities()
229
+ * const api = createApi(capabilities.data.query)
230
+ * // or: const api = createFetchApi(capabilities.data.fetch)
231
+ */
232
+
233
+ import type { ApiRequest, FetchRequestInit, FetchResponse } from '@stackable-labs/sdk-extension-contracts'
234
+
235
+ type QueryFn = <T = unknown>(payload: ApiRequest) => Promise<T>
236
+ type FetchFn = (url: string, init?: FetchRequestInit) => Promise<FetchResponse>
237
+
238
+ // \u2500\u2500 data.query wrapper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
239
+
240
+ export function createApi(query: QueryFn) {
241
+ return {
242
+ async getItems(): Promise<unknown[]> {
243
+ return query<unknown[]>({ action: 'getItems' })
244
+ },
245
+
246
+ async getItem(itemId: string): Promise<unknown> {
247
+ return query<unknown>({ action: 'getItem', itemId })
248
+ },
249
+ }
250
+ }
251
+
252
+ // \u2500\u2500 data.fetch wrapper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
253
+
254
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string
255
+
256
+ export function createFetchApi(fetch: FetchFn) {
257
+ return {
258
+ async getItems(): Promise<unknown[]> {
259
+ const result = await fetch(\`\${API_BASE_URL}/items\`, { method: 'GET' })
260
+ if (!result.ok) throw new Error(\`getItems failed: \${result.status}\`)
261
+ return result.data as unknown[]
262
+ },
263
+
264
+ async getItem(itemId: string): Promise<unknown> {
265
+ const result = await fetch(\`\${API_BASE_URL}/items/\${itemId}\`, { method: 'GET' })
266
+ if (!result.ok) throw new Error(\`getItem failed: \${result.status}\`)
267
+ return result.data as unknown
268
+ },
269
+ }
270
+ }`
271
+ }
272
+ ];
273
+ var CORE_CONTENT = `# Extension SDK \u2014 Editorial Notes
274
+
275
+ > This file is merged into the generated AI editor config files.
276
+ > Keep it minimal \u2014 the generation script handles most content automatically.
277
+
278
+ ## Key Requirements
279
+
280
+ ### Fetching Data
281
+ - No direct API calls using standard \`fetch\` allowed (blocked for security)
282
+ - Any external API calls must be made using the \`data.fetch\` capability
283
+ - Domain for any API call needs to be safelisted via \`allowedDomains\` in \`manifest.json\`
284
+
285
+ ## Troubleshooting
286
+
287
+ ### "Permission denied" errors
288
+ - Verify the capability's permission is declared in \`manifest.json\`
289
+ - Check that the permission string matches exactly (e.g., \`data:fetch\`, not \`data.fetch\`)
290
+
291
+ ### data.fetch returns network errors
292
+ - Confirm the domain is in \`allowedDomains\` in \`manifest.json\`
293
+ - Only exact hostnames are allowed \u2014 no wildcards, no paths
294
+ - Check that the URL uses HTTPS
295
+
296
+ ### Surface not rendering
297
+ - Confirm the \`<Surface id="...">\` matches a target in \`manifest.json\`
298
+ - Confirm the surface component is imported and rendered in \`index.tsx\`
299
+ - Check the browser console for errors
300
+
301
+ ### Context data is undefined
302
+ - Ensure \`context:read\` permission is declared
303
+ - Always check the \`loading\` flag before using context values
304
+ - Context is only available when the host provides it (e.g., when viewing a ticket)
305
+
306
+ ## Best Practices
307
+
308
+ ### Performance
309
+ - Keep bundle size under 500KB
310
+ - Use \`useStore(store, selector)\` with narrow selectors to minimize re-renders
311
+ - Avoid fetching data in multiple surfaces \u2014 fetch once and share via store
312
+
313
+ ### Code Organization
314
+ - One file per surface in \`src/surfaces/\`
315
+ - Shared state in \`src/store.ts\`
316
+ - API wrappers in \`src/lib/\`
317
+ - Feature components in \`src/components/\`
318
+ `;
319
+
320
+ // ../../sdk/extension/ai-docs/src/utils.ts
321
+ var frontmatter = (meta) => {
322
+ const lines = ["---"];
323
+ for (const [key, value] of Object.entries(meta)) {
324
+ if (Array.isArray(value)) {
325
+ lines.push(`${key}: ${JSON.stringify(value)}`);
326
+ } else if (typeof value === "object" && value !== null) {
327
+ lines.push(`${key}:`);
328
+ for (const [subKey, subValue] of Object.entries(value)) {
329
+ lines.push(` ${subKey}: ${JSON.stringify(subValue)}`);
330
+ }
331
+ } else {
332
+ lines.push(`${key}: ${JSON.stringify(value)}`);
333
+ }
334
+ }
335
+ lines.push("---");
336
+ return lines.join("\n");
337
+ };
338
+ var generateCoreReference = () => CORE_CONTENT;
339
+
340
+ // ../../sdk/extension/ai-docs/src/generators/components.ts
341
+ var generateComponents = () => {
342
+ const fm = frontmatter({
343
+ root: false,
344
+ targets: ["*"],
345
+ description: "Available UI components with allowed attributes per tag",
346
+ globs: ["packages/extension/src/**/*.tsx"]
347
+ });
348
+ const componentLines = [];
349
+ for (const tag of UI_TAGS) {
350
+ const attrs = UI_TAG_ATTRIBUTES[tag];
351
+ const name = tagToComponentName(tag);
352
+ componentLines.push(`### \`<ui.${name}>\` (\`${tag}\`)`);
353
+ componentLines.push(`Allowed attributes: ${attrs.map((a2) => `\`${a2}\``).join(", ")}`);
354
+ componentLines.push("");
355
+ }
356
+ const iconList = ALLOWED_ICONS.map((icon) => `\`${icon}\``).join(", ");
357
+ return `${fm}
358
+
359
+ # UI Components
360
+
361
+ Access all components via the \`ui.*\` namespace:
362
+ \`\`\`tsx
363
+ import { ui } from '@stackable-labs/sdk-extension-react'
364
+ \`\`\`
365
+
366
+ **${UI_TAGS.length} available components** \u2014 only use the attributes listed below for each component.
367
+
368
+ ${componentLines.join("\n")}
369
+ ## Available Icons (${ALLOWED_ICONS.length})
370
+
371
+ Use with \`<ui.Icon name="icon-name" />\`. Valid icon names:
372
+ ${iconList}
373
+ `;
374
+ };
375
+ var generateIconReference = () => {
376
+ const iconList = ALLOWED_ICONS.map((icon) => `- \`${icon}\``).join("\n");
377
+ return `# Available Icons
378
+
379
+ Use with \`<ui.Icon name="icon-name" />\`.
380
+
381
+ ${iconList}
382
+ `;
383
+ };
384
+
385
+ // ../../sdk/extension/ai-docs/src/snippets/hooks.ts
386
+ var HOOK_SNIPPETS = {
387
+ "events:identity": `import { useIdentityEvent } from '@stackable-labs/sdk-extension-react'
388
+
389
+ useIdentityEvent('login', (event) => {
390
+ console.log('User logged in:', event.data.state.user?.email)
391
+ })
392
+ useIdentityEvent('logout', () => {
393
+ console.log('User logged out')
394
+ })`,
395
+ "events:messaging": `import { useMessagingEvent } from '@stackable-labs/sdk-extension-react'
396
+
397
+ useMessagingEvent('postback:Buy Now', (event) => {
398
+ console.log('Postback:', event.data.actionName, event.data.conversationId)
399
+ })`,
400
+ "events:activity": `import { useActivityEvent } from '@stackable-labs/sdk-extension-react'
401
+
402
+ useActivityEvent('product_view', (event) => {
403
+ console.log('Activity:', event.eventName, event.data)
404
+ })`,
405
+ "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
406
+
407
+ useExtendIdentity((claims) => ({
408
+ external_id: \`custom_\${claims.external_id}\`,
409
+ loyalty_tier: 'gold',
410
+ }))`
411
+ };
412
+ var HOOK_SNIPPETS_MEMOIZED = {
413
+ "events:messaging": `import { useCallback } from 'react'
414
+ import { useMessagingEvent } from '@stackable-labs/sdk-extension-react'
415
+ import type { MessagingEventHandler } from '@stackable-labs/sdk-extension-contracts'
416
+
417
+ const handlePostback = useCallback<MessagingEventHandler>((event) => {
418
+ console.log('Postback:', event.data.actionName, event.data.conversationId)
419
+ }, [])
420
+ useMessagingEvent('postback:Buy Now', handlePostback)`,
421
+ "extend.identity": `import { useCallback } from 'react'
422
+ import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
423
+ import type { ExtendIdentityHandler } from '@stackable-labs/sdk-extension-contracts'
424
+
425
+ const handleExtend = useCallback<ExtendIdentityHandler>((claims) => ({
426
+ external_id: \`custom_\${claims.external_id}\`,
427
+ loyalty_tier: 'gold',
428
+ }), [])
429
+ useExtendIdentity(handleExtend)`
430
+ };
431
+
432
+ // ../../sdk/extension/ai-docs/src/generators/capabilities.ts
433
+ var identityEventTypes = Object.values(IDENTITY_EVENTS).map((e) => `'${e}'`).join(" | ");
434
+ var activityEventTypes = Object.values(ACTIVITY_EVENTS).map((e) => `'${e}'`).join(" | ");
435
+ var generateCapabilities = () => {
436
+ const fm = frontmatter({
437
+ root: false,
438
+ targets: ["*"],
439
+ description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, extend:identity, events:identity, events:messaging, events:activity",
440
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
441
+ });
442
+ return `${fm}
443
+
444
+ # Capabilities
445
+
446
+ Access capabilities via the \`useCapabilities()\` hook:
447
+ \`\`\`tsx
448
+ const capabilities = useCapabilities()
449
+ \`\`\`
450
+
451
+ ## data.query \u2014 Host-Mediated Requests
452
+ The host handles the API call. Extension sends an action name + params, host returns data.
453
+ - **Permission required:** \`data:query\`
454
+ - **Usage:** \`capabilities.data.query<T>(payload: ApiRequest): Promise<T>\`
455
+ - **ApiRequest shape:** \`{ action: string; [key: string]: unknown }\`
456
+ - **When to use:** When the host application handles the API integration
457
+
458
+ \`\`\`tsx
459
+ const result = await capabilities.data.query<Customer>({
460
+ action: 'getCustomer',
461
+ customerId: '123',
462
+ })
463
+ \`\`\`
464
+
465
+ ## data.fetch \u2014 Direct HTTP from Sandbox
466
+ Extension makes HTTP requests directly. Domain must be in \`allowedDomains\` in manifest.
467
+ - **Permission required:** \`data:fetch\`
468
+ - **Usage:** \`capabilities.data.fetch(url: string, init?: FetchRequestInit): Promise<FetchResponse>\`
469
+ - **FetchRequestInit:** \`{ method?: 'GET'|'POST'|'PUT'|'PATCH'|'DELETE', headers?: Record<string,string>, body?: unknown }\`
470
+ - **FetchResponse:** \`{ status: number, ok: boolean, data: unknown }\`
471
+ - **When to use:** When the extension calls external APIs directly
472
+
473
+ \`\`\`tsx
474
+ const result = await capabilities.data.fetch('https://api.example.com/data', {
475
+ method: 'GET',
476
+ headers: { 'Authorization': 'Bearer token' },
477
+ })
478
+ if (!result.ok) throw new Error(\`Request failed: \${result.status}\`)
479
+ const data = result.data as MyType
480
+ \`\`\`
481
+
482
+ ## context.read \u2014 Read Host Context
483
+ Read host-provided context (customer ID, email, etc.).
484
+ - **Permission required:** \`context:read\`
485
+ - **Usage:** \`capabilities.context.read(): Promise<ContextData>\`
486
+ - **ContextData shape:** \`{ customerId?: string, customerEmail?: string, [key: string]: unknown }\`
487
+ - **Convenience hook:** \`useContextData()\` returns \`ContextData & { loading: boolean }\`
488
+
489
+ \`\`\`tsx
490
+ // Preferred: use the hook
491
+ const { loading, customerId, customerEmail } = useContextData()
492
+
493
+ // Alternative: use the capability directly
494
+ const context = await capabilities.context.read()
495
+ \`\`\`
496
+
497
+ ## actions.toast \u2014 Show Toast Notifications
498
+ Display a toast notification in the host UI.
499
+ - **Permission required:** \`actions:toast\`
500
+ - **Usage:** \`capabilities.actions.toast(payload: ToastPayload): Promise<void>\`
501
+ - **ToastPayload:** \`{ message: string, type?: 'success'|'error'|'info'|'warning', duration?: number }\`
502
+
503
+ \`\`\`tsx
504
+ capabilities.actions.toast({ message: 'Saved!', type: 'success' })
505
+ \`\`\`
506
+
507
+ ## actions.invoke \u2014 Invoke Host Actions
508
+ Trigger host-defined actions (e.g., open a new conversation, set conversation tags/fields).
509
+ - **Permission required:** \`actions:invoke\`
510
+ - **Usage:** \`capabilities.actions.invoke<T>(action: string, payload?: Record<string, unknown>): Promise<T>\`
511
+ - **Available actions:**
512
+ - \`'newConversation'\` \u2014 start a new Zendesk conversation (optionally with tags/fields)
513
+ - \`'setConversationTags'\` \u2014 set tags on the current/next conversation
514
+ - \`'setConversationFields'\` \u2014 set custom fields on the current/next conversation
515
+ - \`'open'\` / \`'close'\` / \`'show'\` / \`'hide'\` \u2014 control the Zendesk messenger widget
516
+
517
+ \`\`\`tsx
518
+ // New conversation with tags and fields
519
+ await capabilities.actions.invoke('newConversation', {
520
+ tags: ['stackable', 'order-lookup'],
521
+ fields: [{ id: 'stackable_action', value: 'order_status' }],
522
+ metadata: { orderId: '12345' },
523
+ })
524
+
525
+ // Standalone: set tags on current/next conversation
526
+ await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])
527
+
528
+ // Standalone: set custom fields
529
+ await capabilities.actions.invoke('setConversationFields', [
530
+ { id: 'order_status', value: 'shipped' },
531
+ ])
532
+ \`\`\`
533
+
534
+ **Zendesk constraints:** Tags max 20, auto-lowercased/sanitized. Fields require \`web_widget_conversation_ticket_metadata\` feature flag. Both \`conversationTags\` and \`conversationFields\` **replace** on each call (not additive).
535
+
536
+ ## events:identity \u2014 Identity Event Subscription
537
+ Subscribe to real-time identity events (login, logout, refresh, expired) pushed from the host.
538
+ - **Permission required:** \`events:identity\`
539
+ - **Manifest events array:** Declare specific events to listen for (e.g. \`["identity:login", "identity:logout"]\`)
540
+ - **Hook:** \`useIdentityEvent(eventType, handler)\`
541
+ - **Event types:** \`${identityEventTypes}\`
542
+
543
+ \`\`\`json
544
+ {
545
+ "permissions": ["events:identity"],
546
+ "events": ["identity:login", "identity:logout"]
547
+ }
548
+ \`\`\`
549
+
550
+ \`\`\`tsx
551
+ ${HOOK_SNIPPETS["events:identity"]}
552
+ \`\`\`
553
+
554
+ **Note:** Identity state is also available via \`context.read()\` \u2192 \`identity\` field (requires \`context:read\`, no separate permission needed).
555
+
556
+ ## events:messaging \u2014 Messaging Event Subscription
557
+ Subscribe to messaging events (e.g. postback button clicks) pushed from the host widget.
558
+ - **Permission required:** \`events:messaging\`
559
+ - **Manifest events array:** Declare specific events to listen for (e.g. \`["messaging:postback:Buy Now"]\`) or \`"messaging:postback"\` for all postbacks (requires elevated marketplace review)
560
+ - **Hook:** \`useMessagingEvent(eventType, handler)\` \u2014 \`MessagingEventHandler\` type exported for use with \`useCallback\`
561
+ - **Event types:** \`'postback'\` (all postbacks) or \`'postback:<actionName>'\` (specific postback)
562
+ - **Important:** Only \`postback\`-type buttons fire this event. The Zendesk bot builder's "Present options" creates \`reply\`-type buttons (no event). Use the Sunshine Conversations API with \`{ "type": "postback", "text": "Button Label", "payload": "..." }\` actions to create postback buttons.
563
+ - **actionName caveat:** The \`actionName\` in the event is the button's display **text** (e.g. \`"Add to cart"\`), NOT the postback \`payload\` string. The payload is not exposed by the Zendesk Web Widget. Design manifest \`events\` entries to match button text: \`"messaging:postback:Add to cart"\`.
564
+
565
+ \`\`\`json
566
+ {
567
+ "permissions": ["events:messaging"],
568
+ "events": ["messaging:postback:Add to cart", "messaging:postback:Check order"]
569
+ }
570
+ \`\`\`
571
+
572
+ \`\`\`tsx
573
+ ${HOOK_SNIPPETS["events:messaging"]}
574
+ \`\`\`
575
+
576
+ ## events:activity \u2014 Activity Event Subscription
577
+ Subscribe to host activity events (e.g. page views, clicks, purchases) pushed from the host application.
578
+ - **Permission required:** \`events:activity\`
579
+ - **Manifest events array:** Declare specific events to listen for (e.g. \`["activity:product_view", "activity:add_to_cart"]\`) \u2014 manifest uses fully-qualified strings
580
+ - **Hook:** \`useActivityEvent(eventType, handler)\` \u2014 \`ActivityEventHandler\` type exported for use with \`useCallback\`
581
+ - **Event types (domain-stripped):** \`${activityEventTypes} | '*'\`
582
+ - **Well-known event names:**
583
+
584
+ | Event | Example payload fields |
585
+ |---|---|
586
+ | \`page_view\` | \`{ url, title, referrer }\` |
587
+ | \`click\` | \`{ elementId, elementText, url }\` |
588
+ | \`product_view\` | \`{ productId, productName, price }\` |
589
+ | \`add_to_cart\` | \`{ productId, quantity, price }\` |
590
+ | \`purchase\` | \`{ orderId, total, currency, items }\` |
591
+ | \`search\` | \`{ query, resultCount }\` |
592
+ | \`form_submit\` | \`{ formId, formName, fields }\` |
593
+
594
+ - \`'*'\` receives ALL activity events
595
+
596
+ \`\`\`json
597
+ {
598
+ "permissions": ["events:activity"],
599
+ "events": ["activity:product_view", "activity:add_to_cart"]
600
+ }
601
+ \`\`\`
602
+
603
+ \`\`\`tsx
604
+ ${HOOK_SNIPPETS["events:activity"]}
605
+ \`\`\`
606
+
607
+ **Generic alternative:** \`useEvent('activity:product_view', handler)\` \u2014 a cross-domain hook that accepts fully-qualified event types. Domain wildcard (e.g., \`'activity'\`) receives all events in that domain.
608
+
609
+ ## extend:identity \u2014 Identity Claim Enrichment
610
+ Enrich identity JWT claims before signing. The host sends base claims to your extension, and you return additional claims to merge into the token.
611
+ - **Permission required:** \`extend:identity\`
612
+ - **Hook:** \`useExtendIdentity(handler)\` \u2014 \`ExtendIdentityHandler\` type exported for use with \`useCallback\`
613
+ - **Handler signature:** \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
614
+ - **IdentityBaseClaims:** \`{ external_id: string, email?: string, name?: string, [key: string]: unknown }\`
615
+
616
+ \`\`\`json
617
+ {
618
+ "permissions": ["extend:identity"]
619
+ }
620
+ \`\`\`
621
+
622
+ \`\`\`tsx
623
+ ${HOOK_SNIPPETS["extend.identity"]}
624
+ \`\`\`
625
+ `;
626
+ };
627
+
628
+ // ../../sdk/extension/ai-docs/src/snippets/examples.ts
629
+ var EXAMPLE_SNIPPETS = {
630
+ // ── Structural topics ───────────────────────────────────────────────────────
631
+ bootstrap: `import { createExtension } from '@stackable-labs/sdk-extension-react'
632
+ import { Header } from './surfaces/Header'
633
+ import { Content } from './surfaces/Content'
634
+ import { Footer } from './surfaces/Footer'
635
+
636
+ const Extension = () => (
637
+ <>
638
+ <Header />
639
+ <Content />
640
+ <Footer />
641
+ </>
642
+ )
643
+
644
+ // NOTE: extensionId is optional \u2014 used when connected to a registered extension
645
+ createExtension(() => <Extension />, { extensionId: 'my-extension' })`,
646
+ surfaces: `import { Surface, ui } from '@stackable-labs/sdk-extension-react'
647
+
648
+ export function Header(): React.ReactElement {
649
+ return (
650
+ <Surface id="slot.header">
651
+ <ui.Stack direction="column" gap="2" className="p-3">
652
+ <ui.Text className="text-sm font-medium">Hello from slot.header</ui.Text>
653
+ </ui.Stack>
654
+ </Surface>
655
+ )
656
+ }
657
+
658
+ export function Content(): React.ReactElement {
659
+ return (
660
+ <Surface id="slot.content">
661
+ <ui.Stack direction="column" gap="3" className="p-4">
662
+ <ui.Heading level="3">Content Surface</ui.Heading>
663
+ <ui.Text>Rendered inside the host at slot.content.</ui.Text>
664
+ </ui.Stack>
665
+ </Surface>
666
+ )
667
+ }`,
668
+ store: `import { createStore, useStore, Surface, ui } from '@stackable-labs/sdk-extension-react'
669
+
670
+ // store.ts
671
+ type ViewState = { type: 'list' } | { type: 'detail'; id: string }
672
+
673
+ interface AppState {
674
+ viewState: ViewState
675
+ }
676
+
677
+ export const appStore = createStore<AppState>({
678
+ viewState: { type: 'list' },
679
+ })
680
+
681
+ // Content.tsx
682
+ export function Content(): React.ReactElement {
683
+ const viewState = useStore(appStore, (s) => s.viewState)
684
+
685
+ const goToDetail = (id: string) => appStore.set({ viewState: { type: 'detail', id } })
686
+ const goBack = () => appStore.set({ viewState: { type: 'list' } })
687
+
688
+ return (
689
+ <Surface id="slot.content">
690
+ {viewState.type === 'list' && (
691
+ <ui.Button onClick={() => goToDetail('abc123')}>View Detail</ui.Button>
692
+ )}
693
+ {viewState.type === 'detail' && (
694
+ <ui.Stack direction="column" gap="2">
695
+ <ui.Text>Viewing: {viewState.id}</ui.Text>
696
+ <ui.Button onClick={goBack}>Back</ui.Button>
697
+ </ui.Stack>
698
+ )}
699
+ </Surface>
700
+ )
701
+ }`,
702
+ loading: `import { useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
703
+
704
+ export function Content(): React.ReactElement {
705
+ const { loading, customerId } = useContextData()
706
+
707
+ if (loading) {
708
+ return (
709
+ <Surface id="slot.content">
710
+ <ui.Skeleton />
711
+ </Surface>
712
+ )
713
+ }
714
+
715
+ if (!customerId) {
716
+ return (
717
+ <Surface id="slot.content">
718
+ <ui.Stack direction="column" gap="1" className="p-4 items-center">
719
+ <ui.Icon name="user" size="sm" />
720
+ <ui.Text className="text-xs text-muted-foreground">No customer selected</ui.Text>
721
+ </ui.Stack>
722
+ </Surface>
723
+ )
724
+ }
725
+
726
+ return (
727
+ <Surface id="slot.content">
728
+ <ui.Text className="text-sm">Customer: {customerId}</ui.Text>
729
+ </Surface>
730
+ )
731
+ }`,
732
+ surfaceContext: `import { useSurfaceContext, Surface, ui } from '@stackable-labs/sdk-extension-react'
733
+ import type { ContextData } from '@stackable-labs/sdk-extension-contracts'
734
+
735
+ export function Header(): React.ReactElement {
736
+ // Lower-level hook \u2014 reads host-pushed context for this specific surface
737
+ const context = useSurfaceContext() as ContextData
738
+
739
+ return (
740
+ <Surface id="slot.header">
741
+ <ui.Text className="text-xs">{context.customerId ?? 'No customer'}</ui.Text>
742
+ </Surface>
743
+ )
744
+ }`,
745
+ // ── Per-capability example snippets ─────────────────────────────────────────
746
+ "context.read": `import { useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
747
+
748
+ export function Header(): React.ReactElement {
749
+ const { loading, customerId, customerEmail } = useContextData()
750
+
751
+ if (loading) {
752
+ return (
753
+ <Surface id="slot.header">
754
+ <ui.Text className="text-xs text-muted-foreground">Loading...</ui.Text>
755
+ </Surface>
756
+ )
757
+ }
758
+
759
+ return (
760
+ <Surface id="slot.header">
761
+ <ui.Stack direction="column" gap="1">
762
+ <ui.Text className="text-xs font-semibold">{customerEmail}</ui.Text>
763
+ <ui.Text className="text-xs text-muted-foreground">ID: {customerId}</ui.Text>
764
+ </ui.Stack>
765
+ </Surface>
766
+ )
767
+ }`,
768
+ "data.query": `import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
769
+ import { useState } from 'react'
770
+ import type { ApiRequest } from '@stackable-labs/sdk-extension-contracts'
771
+
772
+ export function Content(): React.ReactElement {
773
+ const capabilities = useCapabilities()
774
+ const [data, setData] = useState<unknown>(null)
775
+
776
+ const handleQuery = async () => {
777
+ const payload: ApiRequest = {
778
+ action: 'getCustomer',
779
+ customerId: '123',
780
+ }
781
+ const result = await capabilities.data.query<{ name: string }>(payload)
782
+ setData(result)
783
+ }
784
+
785
+ return (
786
+ <Surface id="slot.content">
787
+ <ui.Button onClick={handleQuery}>Fetch Data</ui.Button>
788
+ </Surface>
789
+ )
790
+ }`,
791
+ "data.fetch": `import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
792
+ import { useState } from 'react'
793
+ import type { FetchResponse } from '@stackable-labs/sdk-extension-contracts'
794
+
795
+ export function Content(): React.ReactElement {
796
+ const capabilities = useCapabilities()
797
+ const [data, setData] = useState<unknown>(null)
798
+
799
+ const handleGet = async () => {
800
+ const result: FetchResponse = await capabilities.data.fetch('https://api.myservice.com/orders')
801
+ if (result.ok) setData(result.data)
802
+ }
803
+
804
+ const handlePost = async () => {
805
+ const result: FetchResponse = await capabilities.data.fetch(
806
+ 'https://api.myservice.com/orders',
807
+ { method: 'POST', body: { limit: 10 } }
808
+ )
809
+ if (result.ok) setData(result.data)
810
+ }
811
+
812
+ return (
813
+ <Surface id="slot.content">
814
+ <ui.Stack gap="2">
815
+ <ui.Button onClick={handleGet}>GET Orders</ui.Button>
816
+ <ui.Button onClick={handlePost}>POST Orders</ui.Button>
817
+ </ui.Stack>
818
+ </Surface>
819
+ )
820
+ }`,
821
+ "actions.toast": `import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
822
+
823
+ export function Content(): React.ReactElement {
824
+ const capabilities = useCapabilities()
825
+
826
+ const showToast = async () => {
827
+ await capabilities.actions.toast({ type: 'success', message: 'Done!' })
828
+ }
829
+
830
+ return (
831
+ <Surface id="slot.content">
832
+ <ui.Button onClick={showToast}>Show Toast</ui.Button>
833
+ </Surface>
834
+ )
835
+ }`,
836
+ "actions.invoke": `import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
837
+
838
+ export function Content(): React.ReactElement {
839
+ const capabilities = useCapabilities()
840
+
841
+ const newConversation = async () => {
842
+ await capabilities.actions.invoke('newConversation', {
843
+ tags: ['stackable', 'order-lookup'],
844
+ fields: [{ id: 'stackable_action', value: 'order_status' }],
845
+ metadata: { orderId: '12345' },
846
+ })
847
+ }
848
+
849
+ const setTags = async () => {
850
+ await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])
851
+ }
852
+
853
+ return (
854
+ <Surface id="slot.content">
855
+ <ui.Stack gap="2">
856
+ <ui.Button onClick={newConversation}>New Conversation</ui.Button>
857
+ <ui.Button onClick={setTags}>Set Tags</ui.Button>
858
+ </ui.Stack>
859
+ </Surface>
860
+ )
861
+ }`,
862
+ "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
863
+ import type { ExtendIdentityHandler } from '@stackable-labs/sdk-extension-contracts'
864
+
865
+ // Enrich identity JWT claims before signing.
866
+ // The host sends base claims (external_id, email, name),
867
+ // and your handler returns additional claims to merge.
868
+ useExtendIdentity((claims) => ({
869
+ external_id: \`shopify_\${claims.external_id}\`,
870
+ loyalty_tier: 'gold',
871
+ }))`,
872
+ // ── Per-event example snippets ─────────────────────────────────────────────
873
+ "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
874
+ import type { IdentityEvent } from '@stackable-labs/sdk-extension-contracts'
875
+ import { useState } from 'react'
876
+
877
+ export function Header(): React.ReactElement {
878
+ const [user, setUser] = useState<string | null>(null)
879
+
880
+ useIdentityEvent('login', (event: IdentityEvent) => {
881
+ setUser(event.data.state.user?.email ?? null)
882
+ })
883
+
884
+ useIdentityEvent('logout', () => {
885
+ setUser(null)
886
+ })
887
+
888
+ return (
889
+ <Surface id="slot.header">
890
+ <ui.Text className="text-xs">{user ?? 'Not logged in'}</ui.Text>
891
+ </Surface>
892
+ )
893
+ }`,
894
+ "events:messaging": `import { useMessagingEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
895
+ import type { MessagingEventHandler } from '@stackable-labs/sdk-extension-contracts'
896
+ import { useState } from 'react'
897
+
898
+ export function Content(): React.ReactElement {
899
+ const [lastPostback, setLastPostback] = useState<string | null>(null)
900
+
901
+ useMessagingEvent('postback', (event) => {
902
+ setLastPostback(event.data.actionName)
903
+ })
904
+
905
+ return (
906
+ <Surface id="slot.content">
907
+ <ui.Text className="text-xs">{lastPostback ?? 'No postbacks yet'}</ui.Text>
908
+ </Surface>
909
+ )
910
+ }`,
911
+ "events:activity": `import { useActivityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
912
+ import type { ActivityEventHandler } from '@stackable-labs/sdk-extension-contracts'
913
+ import { useState } from 'react'
914
+
915
+ export function Content(): React.ReactElement {
916
+ const [lastEvent, setLastEvent] = useState<string | null>(null)
917
+
918
+ useActivityEvent('page_view', (event) => {
919
+ setLastEvent(event.data.url as string)
920
+ })
921
+
922
+ return (
923
+ <Surface id="slot.content">
924
+ <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
925
+ </Surface>
926
+ )
927
+ }`
928
+ };
929
+
930
+ // ../../sdk/extension/ai-docs/src/generators/hooks-and-api.ts
931
+ var stripImports = (snippet) => snippet.split("\n").filter((line) => !line.startsWith("import ")).join("\n").trim();
932
+ var identityEventTypes2 = Object.values(IDENTITY_EVENTS).map((e) => `'${e}'`).join(" | ");
933
+ var activityEventTypes2 = Object.values(ACTIVITY_EVENTS).map((e) => `'${e}'`).join(" | ");
934
+ var generateHooksAndApi = () => {
935
+ const fm = frontmatter({
936
+ root: false,
937
+ targets: ["*"],
938
+ description: "Extension hooks and API reference: createExtension, Surface, useCapabilities, createStore",
939
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
940
+ });
941
+ return `${fm}
942
+
943
+ # Hooks & API Reference
944
+
945
+ All imports from \`@stackable-labs/sdk-extension-react\`.
946
+
947
+ ## createExtension(factory, options?)
948
+ Bootstrap the extension runtime. Call once in \`src/index.tsx\`.
949
+ - \`factory: () => React.ReactElement\` \u2014 render function returning all surfaces
950
+ - \`options?: { extensionId?: string }\` \u2014 extension identifier
951
+
952
+ \`\`\`tsx
953
+ ${EXAMPLE_SNIPPETS.bootstrap}
954
+ \`\`\`
955
+
956
+ ## Surface component
957
+ Wraps content for a target slot. The \`id\` must match a target in \`manifest.json\`.
958
+ - \`<Surface id="slot.content">...</Surface>\`
959
+
960
+ ## useCapabilities()
961
+ Returns the capabilities object for calling host-mediated APIs.
962
+ \`\`\`tsx
963
+ const capabilities = useCapabilities()
964
+ // capabilities.context.read() \u2014 includes identity state in response
965
+ // capabilities.data.query(payload)
966
+ // capabilities.data.fetch(url, init?)
967
+ // capabilities.actions.toast(payload)
968
+ // capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
969
+ // capabilities.extend.identity(payload) \u2014 enrich identity claims (prefer useExtendIdentity hook)
970
+ \`\`\`
971
+
972
+ ## useStore(store, selector?)
973
+ Subscribe to a shared store. Re-renders when the selected state changes.
974
+ \`\`\`tsx
975
+ const viewState = useStore(appStore, (s) => s.viewState)
976
+ \`\`\`
977
+
978
+ ## useContextData()
979
+ Reads host-provided context. Returns \`{ loading, customerId, customerEmail, ... }\`.
980
+ \`\`\`tsx
981
+ const { loading, customerId, customerEmail } = useContextData()
982
+ \`\`\`
983
+
984
+ ## useSurfaceContext()
985
+ Returns host-provided context specific to the current surface slot.
986
+ \`\`\`tsx
987
+ const surfaceContext = useSurfaceContext()
988
+ \`\`\`
989
+
990
+ ## useExtension()
991
+ Returns extension-level context.
992
+ \`\`\`tsx
993
+ const { extensionId } = useExtension()
994
+ \`\`\`
995
+
996
+ ## createStore(initialState)
997
+ Create a shared store for cross-surface state coordination.
998
+ \`\`\`tsx
999
+ const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
1000
+ \`\`\`
1001
+
1002
+ ### Store\\<T\\> interface
1003
+ - \`get(): T\` \u2014 read current state
1004
+ - \`set(partial: Partial<T>): void\` \u2014 merge partial state update
1005
+ - \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
1006
+
1007
+ ## useIdentityEvent(eventType, handler)
1008
+ Subscribe to identity events pushed from the host. Requires \`events:identity\` permission and matching entries in manifest \`events\` array.
1009
+ - \`eventType: ${identityEventTypes2}\`
1010
+ - \`handler: (event: IdentityEvent) => void\`
1011
+ - \`IdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }\`
1012
+ - \`IdentityState: { authenticated: boolean, user: UserIdentity | null, expiresAt?: string }\`
1013
+
1014
+ \`\`\`tsx
1015
+ ${stripImports(HOOK_SNIPPETS["events:identity"])}
1016
+ \`\`\`
1017
+
1018
+ ## useMessagingEvent(eventType, handler)
1019
+ Subscribe to messaging events (e.g. postback button clicks) pushed from the host widget. Requires \`events:messaging\` permission and matching entries in manifest \`events\` array.
1020
+ - \`eventType: 'postback' | 'postback:<actionName>'\`
1021
+ - \`handler: MessagingEventHandler\` \u2014 \`(event: MessagingEvent) => void\`
1022
+ - \`MessagingPostbackEvent: { eventName: 'postback', data: { actionName: string, conversationId: string, timestamp: string } }\`
1023
+ - \`'postback'\` receives ALL postback events (requires elevated marketplace review)
1024
+ - \`'postback:<actionName>'\` receives only events matching the specific actionName
1025
+
1026
+ \`\`\`tsx
1027
+ ${stripImports(HOOK_SNIPPETS["events:messaging"])}
1028
+ \`\`\`
1029
+
1030
+ With \`useCallback\` (for memoized handlers):
1031
+ \`\`\`tsx
1032
+ ${HOOK_SNIPPETS_MEMOIZED["events:messaging"]}
1033
+ \`\`\`
1034
+
1035
+ ## useActivityEvent(eventType, handler)
1036
+ Subscribe to host activity events. Requires \`events:activity\` permission and matching entries in manifest \`events\` array.
1037
+ - \`eventType: ${activityEventTypes2} | '*'\` (domain-stripped)
1038
+ - \`handler: ActivityEventHandler\` \u2014 \`(event: ActivityEvent) => void\`
1039
+ - \`ActivityEvent: { eventName: string, data: Record<string, unknown> }\`
1040
+ - \`'*'\` receives ALL activity events
1041
+
1042
+ \`\`\`tsx
1043
+ ${stripImports(HOOK_SNIPPETS["events:activity"])}
1044
+ \`\`\`
1045
+
1046
+ ## useEvent(eventType, handler)
1047
+ Generic cross-domain event hook (should not be used unless absolutely required). Subscribe to any event using fully-qualified event types.
1048
+ - \`eventType: EventType\` \u2014 fully-qualified (e.g., \`'activity:product_view'\`, \`'identity:login'\`, \`'messaging:postback'\`)
1049
+ - Domain wildcard (e.g., \`'activity'\`) receives all events in that domain
1050
+ - \`handler: (event: BaseEvent) => void\`
1051
+
1052
+ \`\`\`tsx
1053
+ useEvent('activity', (event) => {
1054
+ console.log('Activity:', event.data)
1055
+ })
1056
+ \`\`\`
1057
+
1058
+ ## useExtendIdentity(handler)
1059
+ Register a handler to enrich identity JWT claims before signing. Requires \`extend:identity\` permission.
1060
+ - \`handler: ExtendIdentityHandler\` \u2014 \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
1061
+ - \`IdentityBaseClaims: { external_id: string, email?: string, name?: string, [key: string]: unknown }\`
1062
+
1063
+ \`\`\`tsx
1064
+ ${stripImports(HOOK_SNIPPETS["extend.identity"])}
1065
+ \`\`\`
1066
+
1067
+ With \`useCallback\` (for memoized handlers):
1068
+ \`\`\`tsx
1069
+ ${HOOK_SNIPPETS_MEMOIZED["extend.identity"]}
1070
+ \`\`\`
1071
+
1072
+ ## Identity via context.read()
1073
+ Identity state is available in the \`context.read()\` response as an \`identity\` field. Requires \`context:read\` permission (no separate identity permission needed).
1074
+ \`\`\`tsx
1075
+ const context = await capabilities.context.read()
1076
+ // context.identity \u2014 { authenticated, user, expiresAt? }
1077
+ \`\`\`
1078
+ `;
1079
+ };
1080
+ var generatePermissions = () => {
1081
+ const fm = frontmatter({
1082
+ root: false,
1083
+ targets: ["*"],
1084
+ description: "Permission strings, capability mapping, and target-permission conventions",
1085
+ globs: ["packages/extension/public/manifest.json", "packages/extension/src/**/*.ts"]
1086
+ });
1087
+ const permissionList = PERMISSIONS.map((p) => `- \`${p}\``).join("\n");
1088
+ const mappingLines = Object.entries(CAPABILITY_PERMISSION_MAP).map(([cap, perm]) => `| \`${cap}\` | \`${perm}\` |`).join("\n");
1089
+ return `${fm}
1090
+
1091
+ # Permissions
1092
+
1093
+ Declare permissions in \`manifest.json\` before using the corresponding capability.
1094
+
1095
+ ## Available Permissions (${PERMISSIONS.length})
1096
+
1097
+ ${permissionList}
1098
+
1099
+ ## Capability \u2192 Permission Mapping
1100
+
1101
+ | Capability | Required Permission |
1102
+ |-----------|-------------------|
1103
+ ${mappingLines}
1104
+
1105
+ ## Target \u2192 Permission Conventions
1106
+
1107
+ Common permission sets for each surface target:
1108
+
1109
+ | Target | Typical Permissions |
1110
+ |--------|-------------------|
1111
+ | \`slot.header\` | \`context:read\` |
1112
+ | \`slot.content\` | \`context:read\`, \`data:query\`, \`actions:toast\`, \`actions:invoke\` |
1113
+ | \`slot.footer\` | (none) |
1114
+ | \`slot.footer-links\` | (none) |
1115
+
1116
+ Only declare permissions your extension actually uses.
1117
+ `;
1118
+ };
1119
+
1120
+ // ../../sdk/extension/ai-docs/src/generators/suggestions.ts
1121
+ var generateSuggestions = () => {
1122
+ const fm = frontmatter({
1123
+ root: false,
1124
+ targets: ["*"],
1125
+ description: "Contextual suggestion patterns for recommending next steps based on current extension code, capabilities, and surfaces",
1126
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
1127
+ });
1128
+ return `${fm}
1129
+
1130
+ # Next Steps \u2014 Suggestion Patterns
1131
+
1132
+ Use these patterns to analyze the current extension and suggest 2-3 specific, actionable next steps.
1133
+ Suggestions should reference actual data, components, and capabilities in the extension \u2014 never generic.
1134
+
1135
+ ## When to suggest
1136
+
1137
+ - After making code changes (\`updateCode\`, \`updateManifest\`)
1138
+ - After adding a surface, capability, or component
1139
+ - Do NOT suggest after answering a question or explaining code
1140
+
1141
+ ## Suggestion format
1142
+
1143
+ - **Label:** 2-5 word actionable verb phrase (e.g., "Show order total", "Add loading state")
1144
+ - **Prompt:** Specific enough that clicking it produces a useful result. Reference actual data or code in the extension (e.g., "Add the shipping address from the data.fetch response below the order total")
1145
+
1146
+ ## Pattern categories
1147
+
1148
+ ### Code-aware \u2014 analyze what's in the code vs. what's possible
1149
+
1150
+ | Signal | Suggestion direction |
1151
+ |--------|---------------------|
1152
+ | Data fields fetched but not rendered | Suggest rendering unused fields (e.g., "Show order total", "Display customer email") |
1153
+ | Component rendered without styling refinement | Suggest layout improvements (e.g., "Add spacing to header", "Style the card layout") |
1154
+ | Surface exists but is sparse (few child components) | Suggest adding content (e.g., "Add status badge to header", "Include action buttons") |
1155
+ | No error/loading handling for async operations | Suggest robustness (e.g., "Add loading state", "Handle fetch errors") |
1156
+ | Hardcoded values that could come from context | Suggest dynamic data (e.g., "Use customer name from context") |
1157
+
1158
+ ### Capability-aware \u2014 suggest capabilities the extension doesn't use yet
1159
+
1160
+ | Current state | Suggestion direction |
1161
+ |---------------|---------------------|
1162
+ | No capabilities wired | Suggest one relevant to the extension's apparent purpose |
1163
+ | Has \`data.fetch\` but no \`context.read\` | "Wire up customer context" |
1164
+ | Has \`data.query\` but no \`actions.toast\` | "Add success notifications" |
1165
+ | Uses \`context.read\` data but doesn't pass it to \`data.fetch\` | "Use context in API call" |
1166
+ | Has \`data.fetch\` but no error handling | "Handle API errors gracefully" |
1167
+
1168
+ ### Surface-aware \u2014 suggest additional UI surfaces
1169
+
1170
+ | Current surfaces | Suggestion direction |
1171
+ |-----------------|---------------------|
1172
+ | Only \`content\` | "Add a header surface" or "Add footer actions" |
1173
+ | Only \`header\` | "Add a content surface" |
1174
+ | Has content but no \`footer-links\` | "Add quick action links" |
1175
+ | Has header + content but no footer | "Add footer with action buttons" |
1176
+
1177
+ ### Platform-aware \u2014 suggest based on extension target
1178
+
1179
+ | Target platform | Suggestion direction |
1180
+ |----------------|---------------------|
1181
+ | Commerce (Shopify, BigCommerce) | Order data, product info, customer purchase history |
1182
+ | Support (Zendesk, Freshdesk) | Ticket context, conversation data, agent tools |
1183
+ | General | Configuration, settings, external API integrations |
1184
+
1185
+ ## Quality rules
1186
+
1187
+ - Never suggest something already present in the code
1188
+ - Prefer suggestions that build on what the user just did (e.g., after adding a surface, suggest populating it)
1189
+ - Mix suggestion types: one "enhance what's there", one "add something new", one "improve quality" (loading/error states, styling)
1190
+ - Labels must be short enough to fit in a chip button (2-5 words)
1191
+ - Prompts should be one sentence that clearly describes the change
1192
+
1193
+ ## Examples by context
1194
+
1195
+ | After this action | Label | Prompt |
1196
+ |---|---|---|
1197
+ | Added \`data.fetch\` to an orders API | "Show order total" | "Add the order total from the data.fetch response to the content surface" |
1198
+ | Added a header surface | "Add status badge" | "Add a status badge component to the header showing the current order status" |
1199
+ | Wired up \`context.read\` | "Use in API call" | "Pass the customerId from context.read as a parameter in the data.fetch call" |
1200
+ | Styled a component | "Add loading state" | "Add a loading skeleton that displays while the data.fetch call is in progress" |
1201
+ | Added \`data.query\` | "Show results in list" | "Render the query results as a list of cards in the content surface" |
1202
+ | Added a button | "Wire up action" | "Connect the button click to an actions.invoke call" |
1203
+ | Added error handling | "Add success toast" | "Show a success toast notification when the operation completes" |
1204
+ `;
1205
+ };
1206
+
1207
+ // ../../sdk/extension/ai-docs/src/generators/guardrails.ts
1208
+ var generateGuardrails = () => {
1209
+ const fm = frontmatter({
1210
+ root: false,
1211
+ targets: ["*"],
1212
+ description: "SDK constraints and rules that must always be followed",
1213
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"],
1214
+ cursor: { alwaysApply: true }
1215
+ });
1216
+ return `${fm}
1217
+
1218
+ # Guardrails
1219
+
1220
+ These rules must always be followed when writing extension code.
1221
+
1222
+ ## Sandbox
1223
+ - Never access \`document\` or \`window.location\` directly \u2014 the extension runs in a sandboxed iframe
1224
+ - Never modify files in \`packages/preview/\` \u2014 the preview host is pre-configured
1225
+
1226
+ ## Components
1227
+ - Use the \`ui.*\` namespace for components (\`<ui.Card>\`, \`<ui.Button>\`) \u2014 don't import components directly
1228
+ - Only use the attributes listed in the component reference \u2014 the host rejects unknown attributes
1229
+ - Use \`<ui.ScrollArea>\` for content that may overflow
1230
+
1231
+ ## Manifest
1232
+ - Always declare permissions in \`manifest.json\` before using capabilities
1233
+ - Don't use \`data.fetch\` without adding the domain to \`allowedDomains\` in manifest
1234
+ - \`allowedDomains\` must be exact hostnames \u2014 no wildcards, no paths
1235
+
1236
+ ## Entry Point
1237
+ - Don't modify \`index.html\` \u2014 the extension entry point is always \`src/index.tsx\` via \`createExtension\`
1238
+
1239
+ ## State Management
1240
+ - Use discriminated unions for view state types, not string constants
1241
+ - Always handle loading states when using capabilities or context data
1242
+
1243
+ ## Performance
1244
+ - Keep bundle size under 500KB
1245
+ - Use the API wrapper pattern \u2014 don't call \`data.fetch\` inline in components
1246
+ - Use narrow selectors with \`useStore(store, selector)\` to minimize re-renders
1247
+ `;
1248
+ };
1249
+
1250
+ // ../../sdk/extension/ai-docs/src/generators/overview.ts
1251
+ var generateOverview = () => {
1252
+ const fm = frontmatter({
1253
+ root: true,
1254
+ targets: ["*"],
1255
+ description: "Stackable Labs Extension SDK overview, packages, and import conventions",
1256
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"],
1257
+ cursor: { alwaysApply: true }
1258
+ });
1259
+ const baseBody = CORE_CONTENT.replace(/^#[^\n]*\n+(?:>[^\n]*\n+)?/, "");
1260
+ return `${fm}
1261
+
1262
+ # Stackable Labs Extension Development
1263
+
1264
+ ## SDK Packages
1265
+ This extension is built on the following npm packages. When generating code,
1266
+ read the installed source in node_modules to ensure type-accurate imports:
1267
+ - \`@stackable-labs/sdk-extension-react\` \u2014 hooks, createExtension, Surface, UI components
1268
+ - \`@stackable-labs/sdk-extension-contracts\` \u2014 TypeScript types, constants, manifest schema
1269
+
1270
+ ## Import Convention
1271
+ Components are accessed via the \`ui.*\` namespace:
1272
+ \`\`\`tsx
1273
+ import { ui, Surface, createExtension, useCapabilities } from '@stackable-labs/sdk-extension-react'
1274
+ // Usage: <ui.Card>, <ui.Button>, <ui.Text>, etc.
1275
+ // Do NOT import components directly (e.g., import { Card } will not work)
1276
+ \`\`\`
1277
+
1278
+ ## Project Structure
1279
+ \`\`\`
1280
+ packages/extension/
1281
+ public/
1282
+ manifest.json # Extension manifest (targets, permissions, allowedDomains)
1283
+ src/
1284
+ index.tsx # Entry point \u2014 createExtension bootstraps the runtime
1285
+ store.ts # Shared store for cross-surface state (createStore)
1286
+ surfaces/ # One file per surface (Header.tsx, Content.tsx, Footer.tsx)
1287
+ components/ # Feature components rendered within surfaces
1288
+ lib/ # API wrappers and utilities
1289
+ packages/preview/ # Local preview host \u2014 DO NOT MODIFY
1290
+ \`\`\`
1291
+
1292
+ ${baseBody}
1293
+ `;
1294
+ };
1295
+
1296
+ // ../../sdk/extension/ai-docs/src/generators/patterns.ts
1297
+ var generatePatterns = () => {
1298
+ const fm = frontmatter({
1299
+ root: false,
1300
+ targets: ["*"],
1301
+ description: "Code patterns: store navigation, API wrapper, surface composition",
1302
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
1303
+ });
1304
+ const sections = PATTERN_SECTIONS.map(({ path, title, code }) => `## ${title}
1305
+
1306
+ \`\`\`tsx
1307
+ // src/${path}
1308
+ ${code}
1309
+ \`\`\``).join("\n\n");
1310
+ const body = `# Code Patterns
1311
+
1312
+ ${sections}
1313
+ `;
1314
+ return `${fm}
1315
+
1316
+ ${body}`;
1317
+ };
1318
+
1319
+ // ../../sdk/extension/ai-docs/src/generators/recipes.ts
1320
+ var generateRecipes = () => {
1321
+ const fm = frontmatter({
1322
+ root: false,
1323
+ targets: ["*"],
1324
+ description: "Reference code and recipes for common extension development tasks",
1325
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
1326
+ });
1327
+ const references = RECIPE_SECTIONS.map(({ path, title, code }) => `## Reference: ${title}
1328
+
1329
+ \`\`\`tsx
1330
+ // src/${path}
1331
+ ${code}
1332
+ \`\`\``).join("\n\n");
1333
+ const body = `# Recipes
1334
+
1335
+ ${references}
1336
+ `;
1337
+ return `${fm}
1338
+
1339
+ ${body}`;
1340
+ };
1341
+ var SURFACE_SNIPPETS = {
1342
+ [SURFACE_TARGETS.HEADER]: `import { Surface, ui } from '@stackable-labs/sdk-extension-react'
1343
+
1344
+ export function Header() {
1345
+ return (
1346
+ <Surface id="slot.header">
1347
+ <ui.Text className="text-sm font-medium">Header content</ui.Text>
1348
+ </Surface>
1349
+ )
1350
+ }`,
1351
+ [SURFACE_TARGETS.CONTENT]: `import { Surface, ui } from '@stackable-labs/sdk-extension-react'
1352
+
1353
+ export function Content() {
1354
+ return (
1355
+ <Surface id="slot.content">
1356
+ <ui.Card>
1357
+ <ui.CardContent>
1358
+ <ui.Text>Extension content</ui.Text>
1359
+ </ui.CardContent>
1360
+ </ui.Card>
1361
+ </Surface>
1362
+ )
1363
+ }`,
1364
+ [SURFACE_TARGETS.FOOTER]: `import { Surface, ui } from '@stackable-labs/sdk-extension-react'
1365
+
1366
+ export function Footer() {
1367
+ return (
1368
+ <Surface id="slot.footer">
1369
+ <ui.Text className="text-xs">Powered by My Extension</ui.Text>
1370
+ </Surface>
1371
+ )
1372
+ }`,
1373
+ [SURFACE_TARGETS.FOOTER_LINKS]: `import { Surface, ui } from '@stackable-labs/sdk-extension-react'
1374
+
1375
+ export function FooterLinks() {
1376
+ return (
1377
+ <Surface id="slot.footer-links">
1378
+ <ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>
1379
+ </Surface>
1380
+ )
1381
+ }`
1382
+ };
1383
+
1384
+ // ../../sdk/extension/ai-docs/src/generators/surfaces.ts
1385
+ var generateSurfaces = () => {
1386
+ const fm = frontmatter({
1387
+ root: false,
1388
+ targets: ["*"],
1389
+ description: "Surface types, lifecycle, and layout slots for Stackable extensions",
1390
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
1391
+ });
1392
+ return `${fm}
1393
+
1394
+ # Surfaces
1395
+
1396
+ Surfaces are the UI slots where your extension renders content inside the host application.
1397
+ Each surface maps to a specific layout position and is declared as a React component using
1398
+ the \`<Surface>\` wrapper from \`@stackable-labs/sdk-extension-react\`.
1399
+
1400
+ ## Surface Slots
1401
+
1402
+ | Slot | Target String | Typical Use |
1403
+ |------|--------------|-------------|
1404
+ | Header | \`slot.header\` | Compact status bar, quick actions, summary info |
1405
+ | Content | \`slot.content\` | Main extension UI \u2014 forms, data display, workflows |
1406
+ | Footer | \`slot.footer\` | Actions, save buttons, contextual controls |
1407
+ | Footer Links | \`slot.footer-links\` | Navigation links, external references |
1408
+
1409
+ ## Declaring a Surface
1410
+
1411
+ Each surface is a React component wrapped in \`<Surface>\`:
1412
+
1413
+ \`\`\`tsx
1414
+ ${SURFACE_SNIPPETS[SURFACE_TARGETS.CONTENT]}
1415
+ \`\`\`
1416
+
1417
+ The \`id\` prop must match a target declared in your \`manifest.json\`:
1418
+ \`\`\`json
1419
+ {
1420
+ "targets": ["slot.content"]
1421
+ }
1422
+ \`\`\`
1423
+
1424
+ ## Registering Surfaces
1425
+
1426
+ Surfaces are composed in the entry point (\`src/index.tsx\`) via \`createExtension\`:
1427
+
1428
+ \`\`\`tsx
1429
+ ${EXAMPLE_SNIPPETS.bootstrap}
1430
+ \`\`\`
1431
+
1432
+ \`createExtension\` bootstraps the extension runtime \u2014 it handles the sandboxed iframe
1433
+ communication, capability injection, and surface registration with the host.
1434
+
1435
+ ## Surface Lifecycle
1436
+
1437
+ 1. **Mount** \u2014 Host creates an iframe for the extension and loads the entry point
1438
+ 2. **Register** \u2014 \`createExtension\` scans the rendered tree for \`<Surface>\` components
1439
+ 3. **Render** \u2014 Each \`<Surface>\` renders its children into the matching host slot
1440
+ 4. **Update** \u2014 Surfaces re-render when props, state, or context data changes
1441
+ 5. **Unmount** \u2014 Host removes the extension iframe when no longer needed
1442
+
1443
+ ## Multi-Surface State
1444
+
1445
+ Surfaces share state via \`createStore\` (see Store & Navigation). Each surface can read
1446
+ and write to the same store, enabling coordinated behavior across layout slots:
1447
+
1448
+ \`\`\`tsx
1449
+ import { useStore } from '@stackable-labs/sdk-extension-react'
1450
+ import { appStore } from '../store'
1451
+
1452
+ export const Header = () => {
1453
+ const viewState = useStore(appStore, (s) => s.viewState)
1454
+ return (
1455
+ <Surface id="slot.header">
1456
+ <ui.Text>Current view: {viewState.type}</ui.Text>
1457
+ </Surface>
1458
+ )
1459
+ }
1460
+ \`\`\`
1461
+
1462
+ ## Best Practices
1463
+
1464
+ - **One surface per file** \u2014 keep surfaces in \`src/surfaces/\` with clear names (Header.tsx, Content.tsx)
1465
+ - **Minimal surface components** \u2014 surfaces should compose feature components from \`src/components/\`
1466
+ - **Always declare targets** \u2014 every \`<Surface id="...">\` must have a matching target in manifest.json
1467
+ - **Use ScrollArea** \u2014 wrap content surfaces in \`<ui.ScrollArea>\` for overflow handling
1468
+ `;
1469
+ };
1470
+
1471
+ // ../../sdk/extension/ai-docs/src/cli-commands.ts
1472
+ var DLX = "pnpm --config.dlx-cache-max-age=0 dlx";
1473
+ var CLI = {
1474
+ /** Scaffold a new extension project */
1475
+ create: (name = "<project-name>") => `${DLX} @stackable-labs/create-extension ${name}`,
1476
+ /** Start dev servers with hot reload */
1477
+ dev: `${DLX} @stackable-labs/cli-app-extension@latest dev`,
1478
+ /** Deploy the extension */
1479
+ deploy: `${DLX} @stackable-labs/cli-app-extension@latest deploy`,
1480
+ /** Validate the extension for common errors */
1481
+ validate: `${DLX} @stackable-labs/cli-app-extension@latest validate`,
1482
+ /** Scaffold from an existing extension */
1483
+ scaffold: `${DLX} @stackable-labs/cli-app-extension@latest scaffold`,
1484
+ /** Update an existing extension */
1485
+ update: `${DLX} @stackable-labs/cli-app-extension@latest update`
1486
+ };
1487
+ var TEMPLATE_FLAVORS = [
1488
+ { name: "minimal", label: "Minimal", description: "Bare minimum \u2014 single surface, hello-world component" },
1489
+ { name: "starter", label: "Starter", description: "Common patterns \u2014 store, api helpers, menu (default)" },
1490
+ { name: "kitchen-sink", label: "Kitchen Sink", description: "Everything \u2014 every component, capability, surface, and hook" }
1491
+ ];
1492
+
1493
+ // ../../sdk/extension/ai-docs/src/generators/quick-start.ts
1494
+ var generateQuickStart = () => {
1495
+ const fm = frontmatter({
1496
+ root: false,
1497
+ targets: ["*"],
1498
+ description: "Step-by-step guide to create, develop, and deploy your first Stackable extension",
1499
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
1500
+ });
1501
+ return `${fm}
1502
+
1503
+ # Quick Start
1504
+
1505
+ Create, develop, and deploy your first Stackable extension in minutes.
1506
+
1507
+ ## Prerequisites
1508
+
1509
+ - **Node.js** 22 or later
1510
+ - **pnpm** (recommended) or npm
1511
+
1512
+ ## 1. Create a New Extension
1513
+
1514
+ Scaffold a new extension project:
1515
+
1516
+ \`\`\`bash
1517
+ ${CLI.create("my-extension")}
1518
+ cd my-extension
1519
+ pnpm install
1520
+ \`\`\`
1521
+
1522
+ This creates a project with:
1523
+ - A working extension with header, content, and footer surfaces
1524
+ - A local preview host for development
1525
+ - TypeScript configuration
1526
+ - A manifest with default permissions and targets
1527
+
1528
+ ## 2. Preview Your Extension
1529
+
1530
+ Run the Stackable CLI dev server from your project directory:
1531
+
1532
+ \`\`\`bash
1533
+ ${CLI.dev}
1534
+ \`\`\`
1535
+
1536
+ > **Note:** This uses \`pnpm dlx\` to run the Stackable CLI without installing it
1537
+ > globally. The \`--config.dlx-cache-max-age=0\` flag ensures you always get the
1538
+ > latest version.
1539
+
1540
+ The dev command:
1541
+ 1. Starts a local Vite dev server for your extension with hot reload
1542
+ 2. Creates a public Cloudflare tunnel to your local server
1543
+ 3. Displays a **query parameter** you can use to preview your extension
1544
+
1545
+ ### Testing against your host app
1546
+
1547
+ The CLI outputs a query param like:
1548
+
1549
+ \`\`\`
1550
+ ?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1551
+ \`\`\`
1552
+
1553
+ Copy this and **append it to your host application URL** to load your local extension
1554
+ instead of the production bundle. For example:
1555
+
1556
+ \`\`\`
1557
+ https://your-app.com/dashboard?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1558
+ \`\`\`
1559
+
1560
+ This override is **browser-session only** \u2014 no database changes, no shared state.
1561
+ Each developer gets isolated overrides. Changes you make locally appear immediately
1562
+ in the host app via hot reload.
1563
+
1564
+ Use \`--no-tunnel\` if you only want to run the local Vite dev server without a tunnel.
1565
+
1566
+ ## 3. Explore the Project
1567
+
1568
+ \`\`\`
1569
+ my-extension/
1570
+ packages/
1571
+ extension/
1572
+ public/
1573
+ manifest.json # Targets, permissions, allowed domains
1574
+ src/
1575
+ index.tsx # Entry point \u2014 createExtension bootstraps the runtime
1576
+ store.ts # Shared state across surfaces
1577
+ surfaces/ # One component per surface slot
1578
+ Header.tsx
1579
+ Content.tsx
1580
+ Footer.tsx
1581
+ components/ # Feature components used by surfaces
1582
+ lib/ # API wrappers and utilities
1583
+ preview/ # Local preview host \u2014 DO NOT MODIFY
1584
+ \`\`\`
1585
+
1586
+ ## 4. Make Your First Change
1587
+
1588
+ Open \`packages/extension/src/surfaces/Content.tsx\` and modify the content:
1589
+
1590
+ \`\`\`tsx
1591
+ import { Surface, ui } from '@stackable-labs/sdk-extension-react'
1592
+
1593
+ export const Content = () => (
1594
+ <Surface id="slot.content">
1595
+ <ui.Card>
1596
+ <ui.CardContent>
1597
+ <ui.Heading level={3}>Hello, Stackable!</ui.Heading>
1598
+ <ui.Text>My first extension is working.</ui.Text>
1599
+ </ui.CardContent>
1600
+ </ui.Card>
1601
+ </Surface>
1602
+ )
1603
+ \`\`\`
1604
+
1605
+ Save the file \u2014 the preview updates automatically.
1606
+
1607
+ ## 5. Add a Capability
1608
+
1609
+ To read host context data (customer info, etc.):
1610
+
1611
+ 1. Add the permission to \`manifest.json\`:
1612
+ \`\`\`json
1613
+ {
1614
+ "permissions": ["context:read"]
1615
+ }
1616
+ \`\`\`
1617
+
1618
+ 2. Use it in a surface:
1619
+ \`\`\`tsx
1620
+ import { Surface, useContextData, ui } from '@stackable-labs/sdk-extension-react'
1621
+
1622
+ export const Content = () => {
1623
+ const { loading, customerId } = useContextData()
1624
+
1625
+ if (loading) {
1626
+ return (
1627
+ <Surface id="slot.content">
1628
+ <ui.Skeleton />
1629
+ </Surface>
1630
+ )
1631
+ }
1632
+
1633
+ return (
1634
+ <Surface id="slot.content">
1635
+ <ui.Card>
1636
+ <ui.CardContent>
1637
+ <ui.Text>Customer: {customerId}</ui.Text>
1638
+ </ui.CardContent>
1639
+ </ui.Card>
1640
+ </Surface>
1641
+ )
1642
+ }
1643
+ \`\`\`
1644
+
1645
+ ## 6. Validate & Deploy
1646
+
1647
+ Before deploying, validate your extension:
1648
+
1649
+ \`\`\`bash
1650
+ ${CLI.validate}
1651
+ \`\`\`
1652
+
1653
+ This checks manifest structure, permission usage, surface targets, and import patterns.
1654
+
1655
+ When ready, deploy:
1656
+
1657
+ \`\`\`bash
1658
+ ${CLI.deploy}
1659
+ \`\`\`
1660
+
1661
+ ## Next Steps
1662
+
1663
+ - **[Components](/docs/reference/components)** \u2014 browse the full UI component catalog
1664
+ - **[Capabilities](/docs/reference/capabilities)** \u2014 learn about data.query, data.fetch, and more
1665
+ - **[Surfaces](/docs/reference/surfaces)** \u2014 understand surface types and layout slots
1666
+ - **[Patterns](/docs/reference/patterns)** \u2014 see common extension code patterns
1667
+ `;
1668
+ };
1669
+
1670
+ // ../../sdk/extension/ai-docs/src/generators/cli-reference.ts
1671
+ var generateCliReference = () => {
1672
+ const fm = frontmatter({
1673
+ root: false,
1674
+ targets: ["*"],
1675
+ description: "CLI commands for creating, developing, and deploying Stackable extensions",
1676
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
1677
+ });
1678
+ const templateRows = TEMPLATE_FLAVORS.map((t) => `| \`${t.name}\` | ${t.description} |`).join("\n");
1679
+ return `${fm}
1680
+
1681
+ # CLI Reference
1682
+
1683
+ The Stackable CLI provides commands to create, develop, validate, and deploy extensions.
1684
+
1685
+ ## create
1686
+
1687
+ Scaffold a new extension project:
1688
+
1689
+ \`\`\`bash
1690
+ ${CLI.create()}
1691
+ \`\`\`
1692
+
1693
+ **Arguments:**
1694
+
1695
+ | Argument | Description |
1696
+ |----------|-------------|
1697
+ | \`project-name\` | Directory name for the new project |
1698
+
1699
+ **Options:**
1700
+
1701
+ | Flag | Description |
1702
+ |------|-------------|
1703
+ | \`--template <flavor>\` | Template flavor (see below) |
1704
+ | \`--app-id <id>\` | Skip App selection |
1705
+ | \`--extension-port <port>\` | Extension dev server port (default: 6543) |
1706
+ | \`--preview-port <port>\` | Preview dev server port |
1707
+ | \`--skip-install\` | Skip package manager install |
1708
+ | \`--skip-git\` | Skip git initialization |
1709
+
1710
+ **Template flavors:**
1711
+
1712
+ | Flavor | Description |
1713
+ |--------|-------------|
1714
+ ${templateRows}
1715
+
1716
+ **Example:**
1717
+ \`\`\`bash
1718
+ ${CLI.create("order-lookup")}
1719
+ cd order-lookup
1720
+ pnpm install
1721
+ \`\`\`
1722
+
1723
+ ## dev
1724
+
1725
+ Start dev servers with a public tunnel for live preview:
1726
+
1727
+ \`\`\`bash
1728
+ ${CLI.dev}
1729
+ \`\`\`
1730
+
1731
+ **Options:**
1732
+
1733
+ | Flag | Description |
1734
+ |------|-------------|
1735
+ | \`--dir <path>\` | Project root (default: cwd) |
1736
+ | \`--extension-port <port>\` | Override extension port |
1737
+ | \`--preview-port <port>\` | Override preview port |
1738
+ | \`--no-tunnel\` | Skip tunnel, just run vite dev |
1739
+
1740
+ **What it does:**
1741
+ 1. Reads \`.env.stackable\` for cached App/Extension context (prompts if missing)
1742
+ 2. Creates Cloudflare tunnels for the extension dev server
1743
+ 3. Starts Vite dev servers with hot reload
1744
+ 4. Displays a \`_stackable_dev\` query param to append to your host app URL
1745
+
1746
+ ### Host App Override
1747
+
1748
+ The CLI outputs a query param like:
1749
+
1750
+ \`\`\`
1751
+ ?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1752
+ \`\`\`
1753
+
1754
+ Append this to your deployed host app URL to load your local extension instead
1755
+ of the production bundle. The override is browser-session only \u2014 no DB changes,
1756
+ no shared state. Each developer gets isolated overrides.
1757
+
1758
+ ## validate
1759
+
1760
+ Check your extension for common errors before deploying:
1761
+
1762
+ \`\`\`bash
1763
+ ${CLI.validate}
1764
+ \`\`\`
1765
+
1766
+ **What it checks:**
1767
+
1768
+ | Check | Description |
1769
+ |-------|-------------|
1770
+ | Manifest structure | Valid JSON, required fields present |
1771
+ | Permission usage | Capabilities used in code have matching permissions in manifest |
1772
+ | Surface targets | Each \`<Surface id="...">\` has a matching target in manifest |
1773
+ | Import validation | Uses \`ui.*\` namespace, no direct component imports |
1774
+ | Domain configuration | \`data.fetch\` calls match \`allowedDomains\` entries |
1775
+ | Sandbox compliance | No \`document\` or \`window.location\` access |
1776
+
1777
+ **Exit codes:**
1778
+ - \`0\` \u2014 all checks pass
1779
+ - \`1\` \u2014 errors found (must fix before deploying)
1780
+
1781
+ ## deploy
1782
+
1783
+ Package and deploy the extension:
1784
+
1785
+ \`\`\`bash
1786
+ ${CLI.deploy}
1787
+ \`\`\`
1788
+
1789
+ **What it does:**
1790
+ 1. Runs validation checks (same as validate)
1791
+ 2. Builds the extension for production
1792
+ 3. Packages the build output
1793
+ 4. Uploads to the Stackable extension registry
1794
+
1795
+ **Prerequisites:**
1796
+ - All validation checks must pass
1797
+ - You must be authenticated (follow the prompts if not)
1798
+
1799
+ ## scaffold
1800
+
1801
+ Scaffold a local project from an existing extension:
1802
+
1803
+ \`\`\`bash
1804
+ ${CLI.scaffold} [extensionId]
1805
+ \`\`\`
1806
+
1807
+ **Options:**
1808
+
1809
+ | Flag | Description |
1810
+ |------|-------------|
1811
+ | \`--app-id <id>\` | App ID (auto-resolved from extensionId if omitted) |
1812
+ | \`--project-id <id>\` | Studio project ID (fetches files/manifest from Studio) |
1813
+ | \`--skip-install\` | Skip package manager install |
1814
+ | \`--skip-git\` | Skip git initialization |
1815
+
1816
+ ## update
1817
+
1818
+ Update an existing extension:
1819
+
1820
+ \`\`\`bash
1821
+ ${CLI.update} [extensionId]
1822
+ \`\`\`
1823
+
1824
+ **Options:**
1825
+
1826
+ | Flag | Description |
1827
+ |------|-------------|
1828
+ | \`--app-id <id>\` | App ID (auto-resolved from extensionId if omitted) |
1829
+ | \`--name <name>\` | New extension name |
1830
+ | \`--targets <targets>\` | Comma-separated target slots |
1831
+ | \`--bundle-url <url>\` | New bundle URL |
1832
+ | \`--enabled <bool>\` | Enable/disable extension |
1833
+ | \`--dir <path>\` | Project root (default: cwd) |
1834
+ `;
1835
+ };
1836
+
1837
+ // ../../sdk/extension/ai-docs/src/generators/external-apis.ts
1838
+ var generateExternalApis = () => {
1839
+ const fm = frontmatter({
1840
+ root: false,
1841
+ targets: ["*"],
1842
+ description: "Direct HTTP requests via data.fetch, allowedDomains configuration, and API wrapper patterns",
1843
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
1844
+ });
1845
+ return `${fm}
1846
+
1847
+ # External APIs
1848
+
1849
+ Extensions can make direct HTTP requests to external services using the \`data.fetch\`
1850
+ capability. All requests are proxied through the host for security \u2014 the extension
1851
+ never makes raw network calls from the sandbox.
1852
+
1853
+ ## Setup
1854
+
1855
+ ### 1. Add Permission
1856
+
1857
+ Add \`data:fetch\` to your manifest permissions:
1858
+
1859
+ \`\`\`json
1860
+ {
1861
+ "permissions": ["data:fetch"]
1862
+ }
1863
+ \`\`\`
1864
+
1865
+ ### 2. Configure Allowed Domains
1866
+
1867
+ Every domain your extension calls must be listed in \`allowedDomains\`:
1868
+
1869
+ \`\`\`json
1870
+ {
1871
+ "allowedDomains": ["api.example.com", "graphql.example.com"]
1872
+ }
1873
+ \`\`\`
1874
+
1875
+ **Rules:**
1876
+ - Exact hostnames only \u2014 no wildcards, no paths, no protocols
1877
+ - The host rejects requests to unlisted domains
1878
+ - Add each subdomain separately (e.g., \`api.example.com\` and \`cdn.example.com\`)
1879
+
1880
+ ### 3. Make Requests
1881
+
1882
+ Access \`data.fetch\` through the \`useCapabilities()\` hook:
1883
+
1884
+ \`\`\`tsx
1885
+ const capabilities = useCapabilities()
1886
+
1887
+ const result = await capabilities.data.fetch('https://api.example.com/data', {
1888
+ method: 'GET',
1889
+ headers: { 'Authorization': 'Bearer token' },
1890
+ })
1891
+
1892
+ if (!result.ok) throw new Error(\`Request failed: \${result.status}\`)
1893
+ const data = result.data as MyType
1894
+ \`\`\`
1895
+
1896
+ ## API Signature
1897
+
1898
+ \`\`\`tsx
1899
+ capabilities.data.fetch(url: string, init?: FetchRequestInit): Promise<FetchResponse>
1900
+ \`\`\`
1901
+
1902
+ **FetchRequestInit:**
1903
+ | Field | Type | Default |
1904
+ |-------|------|---------|
1905
+ | \`method\` | \`'GET' \\| 'POST' \\| 'PUT' \\| 'PATCH' \\| 'DELETE'\` | \`'GET'\` |
1906
+ | \`headers\` | \`Record<string, string>\` | \`{}\` |
1907
+ | \`body\` | \`unknown\` | \u2014 |
1908
+
1909
+ **FetchResponse:**
1910
+ | Field | Type | Description |
1911
+ |-------|------|-------------|
1912
+ | \`status\` | \`number\` | HTTP status code |
1913
+ | \`ok\` | \`boolean\` | \`true\` if status is 2xx |
1914
+ | \`data\` | \`unknown\` | Parsed response body |
1915
+
1916
+ ## API Wrapper Pattern
1917
+
1918
+ Create a typed wrapper in \`src/lib/api.ts\` to keep components clean:
1919
+
1920
+ \`\`\`tsx
1921
+ import type { Capabilities } from '@stackable-labs/sdk-extension-react'
1922
+
1923
+ const BASE_URL = 'https://api.example.com'
1924
+
1925
+ export async function fetchCustomer(
1926
+ capabilities: Capabilities,
1927
+ customerId: string
1928
+ ): Promise<Customer> {
1929
+ const result = await capabilities.data.fetch(
1930
+ \`\${BASE_URL}/customers/\${customerId}\`
1931
+ )
1932
+ if (!result.ok) throw new Error(\`Failed to fetch customer: \${result.status}\`)
1933
+ return result.data as Customer
1934
+ }
1935
+
1936
+ export async function updateCustomer(
1937
+ capabilities: Capabilities,
1938
+ customerId: string,
1939
+ data: Partial<Customer>
1940
+ ): Promise<Customer> {
1941
+ const result = await capabilities.data.fetch(
1942
+ \`\${BASE_URL}/customers/\${customerId}\`,
1943
+ {
1944
+ method: 'PATCH',
1945
+ headers: { 'Content-Type': 'application/json' },
1946
+ body: data,
1947
+ }
1948
+ )
1949
+ if (!result.ok) throw new Error(\`Failed to update customer: \${result.status}\`)
1950
+ return result.data as Customer
1951
+ }
1952
+ \`\`\`
1953
+
1954
+ Then use it in components:
1955
+
1956
+ \`\`\`tsx
1957
+ const capabilities = useCapabilities()
1958
+ const customer = await fetchCustomer(capabilities, customerId)
1959
+ \`\`\`
1960
+
1961
+ ## data.fetch vs data.query
1962
+
1963
+ | | data.fetch | data.query |
1964
+ |--|-----------|-----------|
1965
+ | **Who handles the request** | Extension (via proxy) | Host application |
1966
+ | **Permission** | \`data:fetch\` | \`data:query\` |
1967
+ | **Domain config** | Required (\`allowedDomains\`) | Not needed |
1968
+ | **Use when** | Calling external APIs directly | Host provides the API integration |
1969
+
1970
+ ## Error Handling
1971
+
1972
+ Always check \`result.ok\` before accessing \`result.data\`:
1973
+
1974
+ \`\`\`tsx
1975
+ const result = await capabilities.data.fetch(url)
1976
+ if (!result.ok) {
1977
+ capabilities.actions.toast({
1978
+ message: 'Request failed. Please try again.',
1979
+ type: 'error',
1980
+ })
1981
+ return
1982
+ }
1983
+ \`\`\`
1984
+ `;
1985
+ };
1986
+
1987
+ // ../../sdk/extension/ai-docs/src/generators/extension-studio.ts
1988
+ var generateExtensionStudio = () => {
1989
+ const fm = frontmatter({
1990
+ root: false,
1991
+ targets: ["*"],
1992
+ description: "Extension Studio: in-browser builder with AI assistant, component palette, and live preview",
1993
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
1994
+ });
1995
+ return `${fm}
1996
+
1997
+ # Extension Studio
1998
+
1999
+ Extension Studio is an in-browser development environment for building Stackable
2000
+ extensions without local tooling. It runs inside the admin dashboard and provides
2001
+ a code editor, live preview, component palette, and an AI assistant \u2014 everything
2002
+ needed to create, iterate on, and deploy extensions from the browser.
2003
+
2004
+ ## Layout
2005
+
2006
+ Studio is organized as a 3-pane workspace:
2007
+
2008
+ | Pane | Position | Purpose |
2009
+ |------|----------|---------|
2010
+ | **Shelf** | Left (collapsible) | Component palette, surface picker, capability list |
2011
+ | **Stage** | Center | Code editor (CodeMirror) or live preview \u2014 toggle between modes |
2012
+ | **Sidekick** | Right (collapsible, resizable) | AI chat assistant for code generation and SDK guidance |
2013
+
2014
+ ## The Shelf
2015
+
2016
+ The Shelf is a collapsible sidebar with three sections:
2017
+
2018
+ ### Components
2019
+
2020
+ Browse all available \`ui.*\` components grouped by category (Layout, Text, Input,
2021
+ Feedback, Navigation, Composite). Click a component to insert it into your code \u2014
2022
+ in Code mode it inserts at the cursor, in Preview mode or with Shift+Click it uses
2023
+ AI-powered smart insertion to place the component in the correct location.
2024
+
2025
+ ### Surfaces
2026
+
2027
+ Lists the surface targets available for your app (e.g., \`slot.header\`,
2028
+ \`slot.content\`, \`slot.footer\`). Surfaces already present in your code are
2029
+ filtered out. Clicking a surface adds it to your manifest and inserts a
2030
+ \`<Surface>\` block via AI smart insertion.
2031
+
2032
+ ### Capabilities
2033
+
2034
+ The SDK capabilities your extension can use: \`data.query\`, \`data.fetch\`,
2035
+ \`context.read\`, \`actions.toast\`, \`actions.invoke\`, \`extend.identity\`,
2036
+ \`events:identity\`, \`events:messaging\`, and \`events:activity\`. Clicking a
2037
+ capability adds the permission to your manifest and AI-inserts the hook usage.
2038
+
2039
+ ## The Stage
2040
+
2041
+ The center pane switches between two modes:
2042
+
2043
+ - **Code mode** \u2014 a CodeMirror 6 editor with TypeScript/JSX syntax highlighting,
2044
+ auto-save, and keyboard shortcuts (Cmd+S to save, Cmd+Z/Shift+Z for undo/redo,
2045
+ Tab for indentation)
2046
+ - **Preview mode** \u2014 a live preview that renders your extension exactly as it
2047
+ would appear in production, using the same Remote DOM pipeline as deployed
2048
+ extensions
2049
+
2050
+ Changes in Code mode recompile automatically via in-browser esbuild and update
2051
+ the preview. The toolbar shows the current status: Ready, Saving, Compiling,
2052
+ Thinking (AI), or Error.
2053
+
2054
+ ## The Sidekick
2055
+
2056
+ The Sidekick is an AI chat assistant that understands the Stackable Extension SDK.
2057
+ It can:
2058
+
2059
+ - **Write and modify code** \u2014 ask it to add features, fix bugs, or refactor
2060
+ - **Update the manifest** \u2014 it can add permissions, targets, and allowed domains
2061
+ - **Look up SDK reference** \u2014 it queries the full SDK documentation on demand
2062
+ - **Suggest next steps** \u2014 after making changes, it offers contextual suggestions
2063
+ as clickable chips
2064
+
2065
+ The Sidekick adapts multi-file SDK documentation to Studio's single-file context
2066
+ automatically \u2014 you don't need to worry about file structure when working in Studio.
2067
+
2068
+ ## Getting Started
2069
+
2070
+ 1. Navigate to the **Studio** section in the admin dashboard
2071
+ 2. Select (or create) an App
2072
+ 3. Create a new project \u2014 starts with a blank template
2073
+ 4. Use the Shelf to add components, surfaces, and capabilities
2074
+ 5. Chat with the Sidekick to build out your extension
2075
+ 6. Toggle to Preview mode to see your extension rendered live
2076
+
2077
+ ## Exporting to a Local Project
2078
+
2079
+ When your extension outgrows single-file editing or you want to use your own
2080
+ IDE and version control, export the Studio project to a local CLI project:
2081
+
2082
+ \`\`\`bash
2083
+ ${CLI.scaffold} --project-id <projectId>
2084
+ \`\`\`
2085
+
2086
+ This scaffolds a full multi-file extension project from your Studio code,
2087
+ including surfaces split into separate files, a store, and a configured manifest.
2088
+
2089
+ You can also download the project as a zip file from the export menu in the
2090
+ Studio toolbar.
2091
+
2092
+ ## Studio vs CLI
2093
+
2094
+ | | Studio | CLI |
2095
+ |---|---|---|
2096
+ | **Best for** | Prototyping, learning, quick iterations | Production extensions, team workflows |
2097
+ | **Code structure** | Single file | Multi-file (surfaces/, components/, lib/) |
2098
+ | **Preview** | Built-in live preview | Local dev server + Cloudflare tunnel |
2099
+ | **AI assistance** | Sidekick chat + smart insertion | Your own AI editor (with SDK skills) |
2100
+ | **Version control** | Auto-saved to cloud | Git-based |
2101
+ | **Deployment** | Link to extension + deploy | CLI deploy command |
2102
+
2103
+ Both workflows produce the same output \u2014 a Stackable extension that runs in the
2104
+ host application via the same Remote DOM pipeline. Start in Studio to prototype,
2105
+ then scaffold to CLI when you need the full development workflow.
2106
+ `;
2107
+ };
2108
+ var generateProjectStructure = () => {
2109
+ const fm = frontmatter({
2110
+ root: false,
2111
+ targets: ["*"],
2112
+ description: "Extension project file structure, entry point, surfaces, store, and manifest",
2113
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
2114
+ });
2115
+ return `${fm}
2116
+
2117
+ # Project Structure
2118
+
2119
+ A Stackable extension project is a monorepo with two packages: the extension itself
2120
+ and a local preview host for development.
2121
+
2122
+ ## Directory Layout
2123
+
2124
+ \`\`\`
2125
+ my-extension/
2126
+ packages/
2127
+ extension/ # Your extension code
2128
+ public/
2129
+ manifest.json # Extension manifest
2130
+ src/
2131
+ index.tsx # Entry point
2132
+ store.ts # Shared state (createStore)
2133
+ surfaces/ # Surface components
2134
+ Header.tsx # slot.header
2135
+ Content.tsx # slot.content
2136
+ Footer.tsx # slot.footer
2137
+ components/ # Feature components
2138
+ lib/ # API wrappers, utilities
2139
+ preview/ # Local preview host \u2014 DO NOT MODIFY
2140
+ package.json # Workspace root
2141
+ tsconfig.json
2142
+ \`\`\`
2143
+
2144
+ ## Key Files
2145
+
2146
+ ### manifest.json
2147
+
2148
+ The extension manifest declares what the extension needs from the host:
2149
+
2150
+ \`\`\`json
2151
+ {
2152
+ "name": "My Extension",
2153
+ "version": "0.1.0",
2154
+ "targets": ["slot.header", "slot.content", "slot.footer"],
2155
+ "permissions": ["context:read", "data:fetch"],
2156
+ "allowedDomains": ["api.example.com"]
2157
+ }
2158
+ \`\`\`
2159
+
2160
+ | Field | Purpose |
2161
+ |-------|---------|
2162
+ | \`name\` | Display name shown to users |
2163
+ | \`version\` | Semver version string |
2164
+ | \`targets\` | Surface slots the extension renders into |
2165
+ | \`permissions\` | Capabilities the extension uses |
2166
+ | \`allowedDomains\` | Hostnames for data.fetch requests (exact match, no wildcards) |
2167
+
2168
+ ### index.tsx \u2014 Entry Point
2169
+
2170
+ The entry point bootstraps the extension runtime:
2171
+
2172
+ \`\`\`tsx
2173
+ ${EXAMPLE_SNIPPETS.bootstrap}
2174
+ \`\`\`
2175
+
2176
+ \`createExtension\` handles:
2177
+ - Sandboxed iframe communication with the host
2178
+ - Capability injection (makes \`useCapabilities()\` work)
2179
+ - Surface registration (maps \`<Surface id="...">\` to host layout slots)
2180
+
2181
+ **Do not modify \`index.html\`** \u2014 the extension always bootstraps through \`createExtension\`.
2182
+
2183
+ ### store.ts \u2014 Shared State
2184
+
2185
+ The store provides cross-surface state management:
2186
+
2187
+ \`\`\`tsx
2188
+ ${EXAMPLE_SNIPPETS.store}
2189
+ \`\`\`
2190
+
2191
+ All surfaces import from the same store instance. Use \`useStore(appStore, selector)\`
2192
+ to read state with minimal re-renders.
2193
+
2194
+ ### Surface Files
2195
+
2196
+ Each surface is a React component in \`src/surfaces/\`:
2197
+
2198
+ \`\`\`tsx
2199
+ ${SURFACE_SNIPPETS[SURFACE_TARGETS.CONTENT]}
2200
+ \`\`\`
2201
+
2202
+ The \`id\` prop must match a target in \`manifest.json\`.
2203
+
2204
+ ### Preview Host
2205
+
2206
+ The \`packages/preview/\` directory contains a local preview host that simulates
2207
+ the production environment. **Do not modify this directory** \u2014 it is pre-configured
2208
+ and managed by the SDK tooling.
2209
+
2210
+ ## Import Conventions
2211
+
2212
+ All SDK imports come from two packages:
2213
+
2214
+ \`\`\`tsx
2215
+ // Runtime: hooks, components, utilities
2216
+ import { ui, Surface, createExtension, useCapabilities, createStore, useStore, useContextData }
2217
+ from '@stackable-labs/sdk-extension-react'
2218
+
2219
+ // Types and constants (dev-time only)
2220
+ import type { ExtensionManifest } from '@stackable-labs/sdk-extension-contracts'
2221
+ \`\`\`
2222
+
2223
+ Components use the \`ui.*\` namespace \u2014 do not import components directly.
2224
+ `;
2225
+ };
2226
+
2227
+ // ../../sdk/extension/ai-docs/src/generators/styling-and-theming.ts
2228
+ var generateStylingAndTheming = () => {
2229
+ const fm = frontmatter({
2230
+ root: false,
2231
+ targets: ["*"],
2232
+ description: "Host theme passthrough, CSS constraints, and styling patterns for extensions",
2233
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
2234
+ });
2235
+ return `${fm}
2236
+
2237
+ # Styling & Theming
2238
+
2239
+ Extensions inherit the host application's theme automatically. The SDK component library
2240
+ (\`ui.*\` namespace) renders inside the host's styling context, so colors, fonts, and
2241
+ spacing match the host UI without any configuration.
2242
+
2243
+ ## Host Theme Inheritance
2244
+
2245
+ The \`ui.*\` components automatically use the host's design tokens:
2246
+ - **Colors** \u2014 text, backgrounds, borders adapt to the host's color scheme
2247
+ - **Typography** \u2014 font family, sizes, and weights match the host
2248
+ - **Spacing** \u2014 padding and margins follow the host's spacing scale
2249
+ - **Dark mode** \u2014 components respond to the host's light/dark mode setting
2250
+
2251
+ No CSS or theme configuration is needed in the extension.
2252
+
2253
+ ## The className Prop
2254
+
2255
+ Most \`ui.*\` components accept a \`className\` prop for layout adjustments:
2256
+
2257
+ \`\`\`tsx
2258
+ <ui.Card className="mt-4">
2259
+ <ui.CardContent className="p-6">
2260
+ <ui.Text className="text-center">Centered text</ui.Text>
2261
+ </ui.CardContent>
2262
+ </ui.Card>
2263
+ \`\`\`
2264
+
2265
+ **Use className for:**
2266
+ - Layout (margin, padding, flexbox, grid)
2267
+ - Positioning (relative, absolute)
2268
+ - Sizing (width, height, max-width)
2269
+ - Spacing between elements
2270
+
2271
+ **Avoid overriding with className:**
2272
+ - Colors and backgrounds (breaks theme consistency)
2273
+ - Font families and sizes (breaks typography scale)
2274
+ - Border styles (use component variants instead)
2275
+
2276
+ ## Layout Components
2277
+
2278
+ Use the built-in layout components instead of raw CSS:
2279
+
2280
+ | Component | Purpose |
2281
+ |-----------|---------|
2282
+ | \`<ui.Stack>\` | Vertical layout with consistent spacing |
2283
+ | \`<ui.Inline>\` | Horizontal layout with consistent spacing |
2284
+ | \`<ui.Separator>\` | Visual divider between sections |
2285
+ | \`<ui.ScrollArea>\` | Scrollable container for overflow content |
2286
+
2287
+ \`\`\`tsx
2288
+ <ui.Stack className="gap-4">
2289
+ <ui.Heading level={3}>Section Title</ui.Heading>
2290
+ <ui.Text>Section content</ui.Text>
2291
+ <ui.Separator />
2292
+ <ui.Inline className="gap-2">
2293
+ <ui.Button variant="primary">Save</ui.Button>
2294
+ <ui.Button variant="ghost">Cancel</ui.Button>
2295
+ </ui.Inline>
2296
+ </ui.Stack>
2297
+ \`\`\`
2298
+
2299
+ ## Component Variants
2300
+
2301
+ Use component props (not CSS) to control visual style:
2302
+
2303
+ \`\`\`tsx
2304
+ // Buttons \u2014 variant controls appearance
2305
+ <ui.Button variant="primary">Primary action</ui.Button>
2306
+ <ui.Button variant="ghost">Secondary action</ui.Button>
2307
+
2308
+ // Badges \u2014 variant + hue + tone for semantic colors
2309
+ <ui.Badge variant="default" hue="blue" tone="strong">Active</ui.Badge>
2310
+ <ui.Badge variant="default" hue="red" tone="subtle">Error</ui.Badge>
2311
+
2312
+ // Text \u2014 tone for semantic meaning
2313
+ <ui.Text tone="subdued">Helper text</ui.Text>
2314
+ <ui.Text tone="critical">Error message</ui.Text>
2315
+ \`\`\`
2316
+
2317
+ ## CSS Constraints
2318
+
2319
+ Extensions run in a sandboxed iframe. These constraints apply:
2320
+
2321
+ - **No global CSS** \u2014 styles don't leak between extensions or into the host
2322
+ - **No \`document\` access** \u2014 cannot inject stylesheets or modify the DOM directly
2323
+ - **No \`window.location\`** \u2014 cannot read or modify the URL
2324
+ - **Component-only styling** \u2014 use \`className\` on \`ui.*\` components, not raw HTML elements
2325
+ - **Bundle size limit** \u2014 keep CSS dependencies minimal (under 500KB total bundle)
2326
+
2327
+ ## Responsive Design
2328
+
2329
+ Extensions render in a constrained viewport (the host's sidebar or panel). Design for
2330
+ narrow widths:
2331
+
2332
+ \`\`\`tsx
2333
+ // Use ScrollArea for content that may overflow
2334
+ <ui.ScrollArea className="h-[400px]">
2335
+ <ui.Stack className="gap-3">
2336
+ {items.map((item) => (
2337
+ <ItemCard key={item.id} item={item} />
2338
+ ))}
2339
+ </ui.Stack>
2340
+ </ui.ScrollArea>
2341
+ \`\`\`
2342
+
2343
+ - Assume a **narrow viewport** (~300-400px wide)
2344
+ - Use \`<ui.ScrollArea>\` for long lists or content
2345
+ - Avoid horizontal scrolling \u2014 stack elements vertically
2346
+ - Test at the minimum panel width the host supports
2347
+ `;
2348
+ };
2349
+
2350
+ // ../../sdk/extension/ai-docs/src/generators/store-and-navigation.ts
2351
+ var generateStoreAndNavigation = () => {
2352
+ const fm = frontmatter({
2353
+ root: false,
2354
+ targets: ["*"],
2355
+ description: "Cross-surface state management with createStore and store-based navigation patterns",
2356
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
2357
+ });
2358
+ return `${fm}
2359
+
2360
+ # Store & Navigation
2361
+
2362
+ Extensions use \`createStore\` for cross-surface state management. The store is shared
2363
+ across all surfaces (header, content, footer), enabling coordinated UI updates from
2364
+ a single state source.
2365
+
2366
+ ## Creating a Store
2367
+
2368
+ Define your store in \`src/store.ts\`:
2369
+
2370
+ \`\`\`tsx
2371
+ import { createStore } from '@stackable-labs/sdk-extension-react'
2372
+
2373
+ type ViewState = { type: 'list' } | { type: 'detail'; id: string }
2374
+
2375
+ interface AppState {
2376
+ viewState: ViewState
2377
+ }
2378
+
2379
+ export const appStore = createStore<AppState>({
2380
+ viewState: { type: 'list' },
2381
+ })
2382
+ \`\`\`
2383
+
2384
+ ## Reading State in Surfaces
2385
+
2386
+ Use \`useStore\` with a selector to subscribe to specific slices of state:
2387
+
2388
+ \`\`\`tsx
2389
+ import { useStore } from '@stackable-labs/sdk-extension-react'
2390
+ import { appStore } from '../store'
2391
+
2392
+ export const Content = () => {
2393
+ const viewState = useStore(appStore, (s) => s.viewState)
2394
+
2395
+ if (viewState.type === 'list') {
2396
+ return <ListView onSelect={(id) => appStore.set({ viewState: { type: 'detail', id } })} />
2397
+ }
2398
+ return <DetailView id={viewState.id} onBack={() => appStore.set({ viewState: { type: 'list' } })} />
2399
+ }
2400
+ \`\`\`
2401
+
2402
+ ## Store-Based Navigation
2403
+
2404
+ Use discriminated unions for view state instead of string constants or URL routing.
2405
+ The store replaces traditional routing \u2014 there are no URLs inside the sandbox.
2406
+
2407
+ ### View State Pattern
2408
+
2409
+ \`\`\`tsx
2410
+ // Define all possible views as a discriminated union
2411
+ type ViewState =
2412
+ | { type: 'list' }
2413
+ | { type: 'detail'; id: string }
2414
+ | { type: 'edit'; id: string }
2415
+ | { type: 'create' }
2416
+
2417
+ // TypeScript narrows the type based on the discriminant
2418
+ switch (viewState.type) {
2419
+ case 'list': return <ListView />
2420
+ case 'detail': return <DetailView id={viewState.id} />
2421
+ case 'edit': return <EditView id={viewState.id} />
2422
+ case 'create': return <CreateView />
2423
+ }
2424
+ \`\`\`
2425
+
2426
+ ### Cross-Surface Coordination
2427
+
2428
+ The header can show context based on what the content surface displays:
2429
+
2430
+ \`\`\`tsx
2431
+ // Header surface \u2014 shows back button when on detail view
2432
+ export const Header = () => {
2433
+ const viewState = useStore(appStore, (s) => s.viewState)
2434
+
2435
+ return (
2436
+ <Surface id="slot.header">
2437
+ <ui.Inline>
2438
+ {viewState.type !== 'list' && (
2439
+ <ui.Button
2440
+ variant="ghost"
2441
+ onClick={() => appStore.set({ viewState: { type: 'list' } })}
2442
+ >
2443
+ Back
2444
+ </ui.Button>
2445
+ )}
2446
+ <ui.Heading level={3}>
2447
+ {viewState.type === 'list' ? 'All Items' : 'Item Detail'}
2448
+ </ui.Heading>
2449
+ </ui.Inline>
2450
+ </Surface>
2451
+ )
2452
+ }
2453
+ \`\`\`
2454
+
2455
+ ## Selector Performance
2456
+
2457
+ Use narrow selectors to minimize re-renders. Each \`useStore\` call only
2458
+ triggers a re-render when its selected value changes:
2459
+
2460
+ \`\`\`tsx
2461
+ // Good \u2014 component only re-renders when viewState changes
2462
+ const viewState = useStore(appStore, (s) => s.viewState)
2463
+
2464
+ // Bad \u2014 component re-renders on ANY state change
2465
+ const state = useStore(appStore, (s) => s)
2466
+ \`\`\`
2467
+
2468
+ ## Best Practices
2469
+
2470
+ - **One store per extension** \u2014 define in \`src/store.ts\`, import everywhere
2471
+ - **Discriminated unions for views** \u2014 TypeScript can narrow the type and catch missing cases
2472
+ - **Narrow selectors** \u2014 select only what the component needs
2473
+ - **Use \`appStore.set()\`** \u2014 update state directly via \`appStore.set({ viewState: ... })\` from any component
2474
+ - **No URL routing** \u2014 the extension runs in a sandboxed iframe with no URL bar
2475
+ `;
2476
+ };
2477
+
2478
+ // ../../sdk/extension/ai-docs/src/generators/cookbook-capabilities.ts
2479
+ var generateCookbookCapabilities = () => {
2480
+ const fm = frontmatter({
2481
+ root: false,
2482
+ targets: ["*"],
2483
+ description: "Cookbook: data.query, data.fetch, context.read, actions.toast, actions.invoke capability examples",
2484
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
2485
+ });
2486
+ return `${fm}
2487
+
2488
+ # Capability Examples
2489
+
2490
+ Focused examples showing each SDK capability in a working Surface component.
2491
+ Each example is self-contained \u2014 copy it into a surface file and add the
2492
+ corresponding permission to your \`manifest.json\`.
2493
+
2494
+ ## context.read \u2014 Reading Host Context
2495
+
2496
+ Read customer and session data provided by the host. The \`useContextData\`
2497
+ hook handles loading state automatically.
2498
+
2499
+ **Permission:** \`context:read\`
2500
+
2501
+ \`\`\`tsx
2502
+ ${EXAMPLE_SNIPPETS["context.read"]}
2503
+ \`\`\`
2504
+
2505
+ ## data.query \u2014 Host-Mediated Requests
2506
+
2507
+ Send structured requests to the host application. The host handles the API
2508
+ call and returns the result \u2014 no \`allowedDomains\` needed.
2509
+
2510
+ **Permission:** \`data:query\`
2511
+
2512
+ \`\`\`tsx
2513
+ ${EXAMPLE_SNIPPETS["data.query"]}
2514
+ \`\`\`
2515
+
2516
+ ## data.fetch \u2014 Direct HTTP Requests
2517
+
2518
+ Make HTTP requests directly from the extension sandbox. The domain must be
2519
+ listed in \`allowedDomains\` in your manifest.
2520
+
2521
+ **Permission:** \`data:fetch\`
2522
+
2523
+ \`\`\`tsx
2524
+ ${EXAMPLE_SNIPPETS["data.fetch"]}
2525
+ \`\`\`
2526
+
2527
+ ## actions.toast \u2014 Toast Notifications
2528
+
2529
+ Display toast notifications in the host UI to provide feedback to users.
2530
+
2531
+ **Permission:** \`actions:toast\`
2532
+
2533
+ \`\`\`tsx
2534
+ ${EXAMPLE_SNIPPETS["actions.toast"]}
2535
+ \`\`\`
2536
+
2537
+ ## actions.invoke \u2014 Host Actions
2538
+
2539
+ Trigger host-defined actions like starting conversations, setting tags, or
2540
+ updating custom fields.
2541
+
2542
+ **Permission:** \`actions:invoke\`
2543
+
2544
+ \`\`\`tsx
2545
+ ${EXAMPLE_SNIPPETS["actions.invoke"]}
2546
+ \`\`\`
2547
+ `;
2548
+ };
2549
+ var generateCookbookStructural = () => {
2550
+ const fm = frontmatter({
2551
+ root: false,
2552
+ targets: ["*"],
2553
+ description: "Cookbook: extension bootstrap, surface declaration, store navigation, and loading patterns",
2554
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
2555
+ });
2556
+ return `${fm}
2557
+
2558
+ # Structural Patterns
2559
+
2560
+ Focused examples for the foundational patterns every extension uses: bootstrapping,
2561
+ surfaces, store-based navigation, and loading states.
2562
+
2563
+ ## Bootstrap \u2014 Entry Point
2564
+
2565
+ Set up your extension entry point in \`src/index.tsx\`. The \`createExtension\` factory
2566
+ bootstraps the sandboxed runtime and registers all surfaces with the host.
2567
+
2568
+ \`\`\`tsx
2569
+ ${EXAMPLE_SNIPPETS.bootstrap}
2570
+ \`\`\`
2571
+
2572
+ ## Surfaces \u2014 Declaring UI Slots
2573
+
2574
+ Each surface renders into a specific layout slot in the host application.
2575
+ The \`id\` prop must match a target declared in your \`manifest.json\`.
2576
+
2577
+ ### Header
2578
+
2579
+ \`\`\`tsx
2580
+ ${SURFACE_SNIPPETS[SURFACE_TARGETS.HEADER]}
2581
+ \`\`\`
2582
+
2583
+ ### Content
2584
+
2585
+ \`\`\`tsx
2586
+ ${SURFACE_SNIPPETS[SURFACE_TARGETS.CONTENT]}
2587
+ \`\`\`
2588
+
2589
+ ### Footer
2590
+
2591
+ \`\`\`tsx
2592
+ ${SURFACE_SNIPPETS[SURFACE_TARGETS.FOOTER]}
2593
+ \`\`\`
2594
+
2595
+ ### Footer Links
2596
+
2597
+ \`\`\`tsx
2598
+ ${SURFACE_SNIPPETS[SURFACE_TARGETS.FOOTER_LINKS]}
2599
+ \`\`\`
2600
+
2601
+ ## Store \u2014 Cross-Surface State & Navigation
2602
+
2603
+ Extensions use \`createStore\` for shared state across surfaces. The store replaces
2604
+ traditional URL routing \u2014 there are no URLs inside the sandbox. Use discriminated
2605
+ unions for view state to get exhaustive type checking.
2606
+
2607
+ \`\`\`tsx
2608
+ ${EXAMPLE_SNIPPETS.store}
2609
+ \`\`\`
2610
+
2611
+ ## Loading States & Guards
2612
+
2613
+ Handle loading states and missing context gracefully. Always show a skeleton
2614
+ while context loads, and guard against missing data before rendering.
2615
+
2616
+ \`\`\`tsx
2617
+ ${EXAMPLE_SNIPPETS.loading}
2618
+ \`\`\`
2619
+
2620
+ ## Surface Context
2621
+
2622
+ Read host-pushed context for a specific surface using the lower-level
2623
+ \`useSurfaceContext\` hook.
2624
+
2625
+ \`\`\`tsx
2626
+ ${EXAMPLE_SNIPPETS.surfaceContext}
2627
+ \`\`\`
2628
+ `;
2629
+ };
2630
+ var identityEventTypes3 = Object.values(IDENTITY_EVENTS).map((e) => `\`${e}\``).join(", ");
2631
+ var activityEventTypes3 = Object.values(ACTIVITY_EVENTS).map((e) => `\`${e}\``).join(", ");
2632
+ var generateCookbookEvents = () => {
2633
+ const fm = frontmatter({
2634
+ root: false,
2635
+ targets: ["*"],
2636
+ description: "Cookbook: identity, messaging, and activity event subscriptions + extend identity",
2637
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
2638
+ });
2639
+ return `${fm}
2640
+
2641
+ # Events & Extensions
2642
+
2643
+ Subscribe to real-time events pushed from the host and extend identity claims.
2644
+ Each event type has a dedicated hook \u2014 never use \`capabilities.events.*\` directly.
2645
+
2646
+ ## Identity Events
2647
+
2648
+ Subscribe to login, logout, refresh, and expired events. Useful for tracking
2649
+ agent authentication state in your extension.
2650
+
2651
+ **Permission:** \`events:identity\`
2652
+ **Event types:** ${identityEventTypes3}
2653
+
2654
+ ### Hook usage
2655
+
2656
+ \`\`\`tsx
2657
+ ${HOOK_SNIPPETS["events:identity"]}
2658
+ \`\`\`
2659
+
2660
+ ### Full component example
2661
+
2662
+ \`\`\`tsx
2663
+ ${EXAMPLE_SNIPPETS["events:identity"]}
2664
+ \`\`\`
2665
+
2666
+ ## Messaging Events
2667
+
2668
+ Subscribe to postback button clicks from the Zendesk messaging widget.
2669
+ The \`actionName\` is the button's display text, not a programmatic identifier.
2670
+
2671
+ **Permission:** \`events:messaging\`
2672
+
2673
+ ### Hook usage
2674
+
2675
+ \`\`\`tsx
2676
+ ${HOOK_SNIPPETS["events:messaging"]}
2677
+ \`\`\`
2678
+
2679
+ ### Full component example
2680
+
2681
+ \`\`\`tsx
2682
+ ${EXAMPLE_SNIPPETS["events:messaging"]}
2683
+ \`\`\`
2684
+
2685
+ ## Activity Events
2686
+
2687
+ Subscribe to host activity events like page views, clicks, and purchases.
2688
+ Activity event names are domain-stripped \u2014 use \`useActivityEvent('product_view', ...)\`
2689
+ not \`'activity:product_view'\`.
2690
+
2691
+ **Permission:** \`events:activity\`
2692
+ **Well-known events:** ${activityEventTypes3}
2693
+
2694
+ ### Hook usage
2695
+
2696
+ \`\`\`tsx
2697
+ ${HOOK_SNIPPETS["events:activity"]}
2698
+ \`\`\`
2699
+
2700
+ ### Full component example
2701
+
2702
+ \`\`\`tsx
2703
+ ${EXAMPLE_SNIPPETS["events:activity"]}
2704
+ \`\`\`
2705
+
2706
+ ## Extend Identity
2707
+
2708
+ Enrich identity JWT claims before signing. The host sends base claims
2709
+ (\`external_id\`, \`email\`, \`name\`) and your handler returns additional
2710
+ claims to merge into the token.
2711
+
2712
+ **Permission:** \`extend:identity\`
2713
+
2714
+ ### Hook usage
2715
+
2716
+ \`\`\`tsx
2717
+ ${HOOK_SNIPPETS["extend.identity"]}
2718
+ \`\`\`
2719
+
2720
+ ### Full component example
2721
+
2722
+ \`\`\`tsx
2723
+ ${EXAMPLE_SNIPPETS["extend.identity"]}
2724
+ \`\`\`
2725
+ `;
2726
+ };
2727
+
2728
+ // ../../sdk/extension/ai-docs/src/commands/add-surface.ts
2729
+ var generateAddSurfaceCommand = () => {
2730
+ const fm = frontmatter({
2731
+ description: "Add a new surface (UI slot) to this Stackable extension",
2732
+ targets: ["*"]
2733
+ });
2734
+ return `${fm}
2735
+
2736
+ # Add a New Surface
2737
+
2738
+ Add a new surface to this extension. Follow these steps exactly:
2739
+
2740
+ ## 1. Determine the target slot
2741
+ Ask which target slot this surface is for. Valid targets:
2742
+ - \`slot.header\` \u2014 appears at the top of the extension area
2743
+ - \`slot.content\` \u2014 main content area
2744
+ - \`slot.footer\` \u2014 bottom of the extension area
2745
+ - \`slot.footer-links\` \u2014 footer link row
2746
+
2747
+ ## 2. Create the surface file
2748
+ Create a new file in \`packages/extension/src/surfaces/\` named after the slot
2749
+ (e.g., \`Header.tsx\`, \`Content.tsx\`, \`Footer.tsx\`).
2750
+
2751
+ Use this template:
2752
+ \`\`\`tsx
2753
+ import { Surface, useCapabilities, useContextData } from '@stackable-labs/sdk-extension-react'
2754
+ import { ui } from '@stackable-labs/sdk-extension-react'
2755
+
2756
+ export const [SurfaceName] = () => {
2757
+ return (
2758
+ <Surface id="[target]">
2759
+ {/* Surface content here */}
2760
+ </Surface>
2761
+ )
2762
+ }
2763
+ \`\`\`
2764
+
2765
+ If the surface needs state management, import and use the existing store from \`../store\`.
2766
+
2767
+ ## 3. Register the surface in index.tsx
2768
+ Import the new surface component and add it to the \`createExtension\` factory function
2769
+ alongside existing surfaces.
2770
+
2771
+ ## 4. Update manifest.json
2772
+ Add the target to the \`targets\` array in \`packages/extension/public/manifest.json\`.
2773
+ Also add any required permissions based on the target-permission mapping:
2774
+ - \`slot.header\` \u2192 \`context:read\`
2775
+ - \`slot.content\` \u2192 \`context:read\`, \`data:query\`, \`actions:toast\`, \`actions:invoke\`
2776
+ - \`slot.footer\` \u2192 (none)
2777
+ - \`slot.footer-links\` \u2192 (none)
2778
+
2779
+ Only add permissions that aren't already declared.
2780
+
2781
+ ## 5. Verify
2782
+ - Confirm the surface renders by checking the import chain: index.tsx \u2192 Surface component \u2192 Surface id matches manifest target
2783
+ - Confirm manifest.json is valid JSON with no duplicate targets or permissions
2784
+ `;
2785
+ };
2786
+
2787
+ // ../../sdk/extension/ai-docs/src/commands/add-capability.ts
2788
+ var generateAddCapabilityCommand = () => {
2789
+ const fm = frontmatter({
2790
+ description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, extend:identity, events:identity, events:messaging, events:activity) in this extension",
2791
+ targets: ["*"]
2792
+ });
2793
+ return `${fm}
2794
+
2795
+ # Add a Capability
2796
+
2797
+ Wire up a new capability in this extension. Follow these steps exactly:
2798
+
2799
+ ## 1. Determine the capability
2800
+ Ask which capability to add. Valid capabilities:
2801
+ - \`data.query\` \u2014 host-mediated data requests (action name + params \u2192 host returns data)
2802
+ - \`data.fetch\` \u2014 direct HTTP requests from the sandbox (requires allowedDomains)
2803
+ - \`context.read\` \u2014 read host-provided context (customerId, customerEmail, etc.)
2804
+ - \`actions.toast\` \u2014 show toast notifications (success, error, info, warning)
2805
+ - \`actions.invoke\` \u2014 invoke host actions (e.g., open new conversation)
2806
+ - \`extend:identity\` \u2014 enrich identity JWT claims before signing
2807
+ - \`events:identity\` \u2014 subscribe to identity events (login, logout, refresh, expired)
2808
+ - \`events:messaging\` \u2014 subscribe to messaging events (postback button clicks)
2809
+ - \`events:activity\` \u2014 subscribe to activity events (page views, clicks, purchases)
2810
+
2811
+ ## 2. Add permission to manifest.json
2812
+ Add the corresponding permission to \`packages/extension/public/manifest.json\`:
2813
+ - \`data.query\` \u2192 \`"data:query"\`
2814
+ - \`data.fetch\` \u2192 \`"data:fetch"\`
2815
+ - \`context.read\` \u2192 \`"context:read"\`
2816
+ - \`actions.toast\` \u2192 \`"actions:toast"\`
2817
+ - \`actions.invoke\` \u2192 \`"actions:invoke"\`
2818
+ - \`extend:identity\` \u2192 \`"extend:identity"\`
2819
+ - \`events:identity\` \u2192 \`"events:identity"\` (also add entries to \`events\` array, e.g. \`["identity:login", "identity:logout"]\`)
2820
+ - \`events:messaging\` \u2192 \`"events:messaging"\` (also add entries to \`events\` array, e.g. \`["messaging:postback:Buy Now"]\`)
2821
+ - \`events:activity\` \u2192 \`"events:activity"\` (also add entries to \`events\` array, e.g. \`["activity:product_view"]\`)
2822
+
2823
+ Only add if not already declared.
2824
+
2825
+ **Note:** Identity state is available via \`context.read()\` \u2192 \`identity\` field (requires \`context:read\`, no separate permission).
2826
+
2827
+ ## 3. If data.fetch \u2014 add allowedDomains
2828
+ Ask for the API domain(s) and add them to the \`allowedDomains\` array in manifest.json.
2829
+
2830
+ ## 4. If data.fetch \u2014 create API wrapper
2831
+ Create \`packages/extension/src/lib/api.ts\` with the API wrapper pattern:
2832
+ \`\`\`tsx
2833
+ type FetchFn = (url: string, init?: FetchRequestInit) => Promise<FetchResponse>
2834
+
2835
+ export function createApi(fetch: FetchFn) {
2836
+ return {
2837
+ async getData(): Promise<DataType> {
2838
+ const result = await fetch('https://api.example.com/data', { method: 'GET' })
2839
+ if (!result.ok) throw new Error(\`Request failed: \${result.status}\`)
2840
+ return result.data as DataType
2841
+ },
2842
+ }
2843
+ }
2844
+ \`\`\`
2845
+
2846
+ ## 5. Use the capability in a surface
2847
+
2848
+ ### For data.*, context.*, and actions.* capabilities \u2014 use \`useCapabilities()\`:
2849
+ \`\`\`tsx
2850
+ import { useCapabilities } from '@stackable-labs/sdk-extension-react'
2851
+
2852
+ const capabilities = useCapabilities()
2853
+ // data.query: capabilities.data.query({ action: 'getItems', ... })
2854
+ // data.fetch: const api = createApi(capabilities.data.fetch)
2855
+ // context.read: capabilities.context.read() (or use useContextData() hook)
2856
+ // actions.toast: capabilities.actions.toast({ message: 'Done!', type: 'success' })
2857
+ // actions.invoke: capabilities.actions.invoke('newConversation', { tags: ['order'], fields: [{ id: 'field_id', value: 'val' }] })
2858
+ \`\`\`
2859
+
2860
+ ### For events and extend \u2014 ALWAYS use dedicated hooks (INSTEAD of useCapabilities direct):
2861
+ **IMPORTANT:** Event and extend capabilities have their own React hooks. Never use \`capabilities.events.*\` or \`capabilities.extend.*\` \u2014 those do not exist.
2862
+
2863
+ \`\`\`tsx
2864
+ // events:identity \u2014 use useIdentityEvent hook
2865
+ ${HOOK_SNIPPETS["events:identity"]}
2866
+ \`\`\`
2867
+
2868
+ \`\`\`tsx
2869
+ // events:messaging \u2014 use useMessagingEvent hook
2870
+ ${HOOK_SNIPPETS["events:messaging"]}
2871
+ \`\`\`
2872
+
2873
+ \`\`\`tsx
2874
+ // events:activity \u2014 use useActivityEvent hook
2875
+ ${HOOK_SNIPPETS["events:activity"]}
2876
+ \`\`\`
2877
+
2878
+ \`\`\`tsx
2879
+ // extend:identity \u2014 use useExtendIdentity hook
2880
+ ${HOOK_SNIPPETS["extend.identity"]}
2881
+ \`\`\`
2882
+
2883
+ ## 6. Verify
2884
+ - Confirm the permission is in manifest.json
2885
+ - If data.fetch, confirm the domain is in allowedDomains
2886
+ - For data/context/actions: confirm accessed via \`useCapabilities()\` hook
2887
+ - For events: confirm using \`useIdentityEvent\`, \`useMessagingEvent\`, or \`useActivityEvent\` hooks
2888
+ - For extend: confirm using \`useExtendIdentity\` hook
2889
+ `;
2890
+ };
2891
+
2892
+ // ../../sdk/extension/ai-docs/src/commands/add-component.ts
2893
+ var generateAddComponentCommand = () => {
2894
+ const fm = frontmatter({
2895
+ description: "Add a UI component to this Stackable extension",
2896
+ targets: ["*"]
2897
+ });
2898
+ return `${fm}
2899
+
2900
+ # Add a UI Component
2901
+
2902
+ Add a UI component to this extension. Follow these steps:
2903
+
2904
+ ## 1. Identify the component
2905
+ All UI components use the \`ui.*\` namespace from \`@stackable-labs/sdk-extension-react\`.
2906
+
2907
+ **Available components by category:**
2908
+ - **Layout:** Card, CardContent, CardHeader, Stack, Inline, Separator, ScrollArea
2909
+ - **Text:** Text, Heading, Badge
2910
+ - **Input:** Button, Input, Textarea, Select, SelectOption, Checkbox, Switch, Label, RadioGroup, RadioGroupItem
2911
+ - **Feedback:** Skeleton, Tooltip, Progress, Alert
2912
+ - **Navigation:** Tabs, TabsList, TabsTrigger, TabsContent, Link, Menu, MenuItem
2913
+ - **Composite:** Collapsible, CollapsibleTrigger, CollapsibleContent, Avatar, Icon
2914
+
2915
+ ## 2. Place the component
2916
+ - Insert inside an existing \`<Surface>\` block \u2014 never outside a Surface
2917
+ - Place in a logical position within the existing UI structure
2918
+ - For compound components, include their required children:
2919
+ - **Card** \u2192 CardContent (and optionally CardHeader)
2920
+ - **Tabs** \u2192 TabsList + TabsTrigger(s) + TabsContent(s)
2921
+ - **Select** \u2192 SelectOption(s)
2922
+ - **Menu** \u2192 MenuItem(s)
2923
+ - **Collapsible** \u2192 CollapsibleTrigger + CollapsibleContent
2924
+ - **RadioGroup** \u2192 RadioGroupItem(s) + Label(s)
2925
+
2926
+ ## 3. Import
2927
+ Ensure \`ui\` is imported from \`@stackable-labs/sdk-extension-react\`. It usually already is.
2928
+ No additional imports needed \u2014 all components are accessed via \`ui.ComponentName\`.
2929
+
2930
+ ## 4. Constraints
2931
+ - Do not remove or change any existing code \u2014 only add the new component
2932
+ - Do not add components outside of a Surface
2933
+ - Do not use raw HTML elements \u2014 only \`ui.*\` components are allowed in the sandbox
2934
+ - Child-only tags (CardContent, CardHeader, SelectOption, TabsList, TabsTrigger, TabsContent,
2935
+ RadioGroupItem, CollapsibleTrigger, CollapsibleContent, MenuItem) must be nested inside
2936
+ their parent \u2014 never standalone
2937
+ `;
2938
+ };
2939
+ var generateValidateExtensionCommand = () => {
2940
+ const fm = frontmatter({
2941
+ description: "Validate this extension for common errors before deploying (manifest, permissions, surfaces, imports)",
2942
+ targets: ["*"]
2943
+ });
2944
+ return `${fm}
2945
+
2946
+ # Validate Extension
2947
+
2948
+ Check this extension for common errors before deploying. Run through each check
2949
+ and report all issues found.
2950
+
2951
+ ## 1. Manifest validation
2952
+ Read \`packages/extension/public/manifest.json\` and verify:
2953
+ - [ ] Valid JSON
2954
+ - [ ] \`name\` is a non-empty string
2955
+ - [ ] \`version\` follows semver (e.g., "1.0.0")
2956
+ - [ ] \`targets\` is a non-empty array of valid target strings (slot.header, slot.content, slot.footer, slot.footer-links)
2957
+ - [ ] \`permissions\` contains only valid permission strings (${PERMISSIONS.join(", ")})
2958
+ - [ ] \`allowedDomains\` is an array (can be empty if data:fetch is not used)
2959
+ - [ ] If \`data:fetch\` permission is declared, \`allowedDomains\` is not empty
2960
+
2961
+ ## 2. Permission-to-usage matching
2962
+ Scan all \`.tsx\` files in \`packages/extension/src/\` for capability usage:
2963
+ - \`capabilities.data.query\` or \`data.query\` \u2192 needs \`data:query\` permission
2964
+ - \`capabilities.data.fetch\` or \`data.fetch\` \u2192 needs \`data:fetch\` permission
2965
+ - \`capabilities.context.read\` or \`useContextData\` \u2192 needs \`context:read\` permission
2966
+ - \`capabilities.actions.toast\` \u2192 needs \`actions:toast\` permission
2967
+ - \`capabilities.actions.invoke\` \u2192 needs \`actions:invoke\` permission
2968
+ - \`useExtendIdentity\` \u2192 needs \`extend:identity\` permission
2969
+ - \`useIdentityEvent\` \u2192 needs \`events:identity\` permission (also check manifest \`events\` array has matching entries)
2970
+ - \`useMessagingEvent\` \u2192 needs \`events:messaging\` permission (also check manifest \`events\` array has matching entries)
2971
+ - \`useActivityEvent\` \u2192 needs \`events:activity\` permission (also check manifest \`events\` array has matching entries)
2972
+
2973
+ Report:
2974
+ - **Missing permissions:** capabilities used in code but not declared in manifest
2975
+ - **Unused permissions:** permissions declared in manifest but not used in code
2976
+
2977
+ ## 3. Surface-to-target matching
2978
+ - Each \`.tsx\` file with a \`<Surface id="...">\` should have a matching target in manifest.json
2979
+ - Each target in manifest.json should have a corresponding Surface component
2980
+
2981
+ ## 4. Import validation
2982
+ Verify that:
2983
+ - [ ] UI components are imported via \`ui.*\` namespace from \`@stackable-labs/sdk-extension-react\`
2984
+ - [ ] No direct imports of \`document\`, \`window.location\`, or other browser globals
2985
+ - [ ] No modifications to \`packages/preview/\` directory
2986
+ - [ ] Entry point is \`src/index.tsx\` using \`createExtension\`
2987
+
2988
+ ## 5. Summary
2989
+ Print a summary: total issues found, categorized by severity (error vs warning).
2990
+ Errors must be fixed before deploying. Warnings are recommendations.
2991
+ `;
2992
+ };
2993
+
2994
+ // ../../sdk/extension/ai-docs/src/skills.ts
2995
+ var SKILLS = [
2996
+ // ── Knowledge skills ──────────────────────────────────────────
2997
+ {
2998
+ id: "overview",
2999
+ description: "Stackable Labs Extension SDK overview, packages, import conventions, and project structure. Use when starting a new extension, asking about the SDK, or needing architectural context.",
3000
+ type: "knowledge",
3001
+ content: () => generateOverview(),
3002
+ references: {
3003
+ "core.md": () => generateCoreReference()
3004
+ }
3005
+ },
3006
+ {
3007
+ id: "components",
3008
+ description: "UI component catalog with allowed attributes per tag and available icon names. Use when building extension UI, looking up component attributes, or asking about available components.",
3009
+ type: "knowledge",
3010
+ content: () => generateComponents(),
3011
+ references: {
3012
+ "icons.md": () => generateIconReference()
3013
+ }
3014
+ },
3015
+ {
3016
+ id: "capabilities",
3017
+ description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke. Use when wiring up host-mediated APIs or direct HTTP requests.",
3018
+ type: "knowledge",
3019
+ content: () => generateCapabilities()
3020
+ },
3021
+ {
3022
+ id: "permissions",
3023
+ description: "Permission strings, capability-to-permission mapping, and target conventions. Use when configuring manifest.json or debugging permission errors.",
3024
+ type: "knowledge",
3025
+ content: () => generatePermissions()
3026
+ },
3027
+ {
3028
+ id: "hooks-api",
3029
+ description: "Extension hooks and API reference: createExtension, Surface, useCapabilities, createStore, useContextData. Use when writing extension runtime code.",
3030
+ type: "knowledge",
3031
+ content: () => generateHooksAndApi()
3032
+ },
3033
+ {
3034
+ id: "guardrails",
3035
+ description: "SDK constraints and rules that must always be followed: sandbox restrictions, component usage, manifest requirements, entry point rules. Use as a checklist when writing or reviewing extension code.",
3036
+ type: "knowledge",
3037
+ scopes: ["studio", "registry"],
3038
+ content: () => generateGuardrails()
3039
+ },
3040
+ {
3041
+ id: "patterns",
3042
+ description: "Code patterns extracted from the reference extension: entry point, store navigation, surface composition, API wrapper. Use when implementing common extension patterns.",
3043
+ type: "knowledge",
3044
+ content: () => generatePatterns()
3045
+ },
3046
+ {
3047
+ id: "recipes",
3048
+ description: "Reference code examples from the template extension source. Use when looking for working examples of store management, content surfaces, or API wrappers.",
3049
+ type: "knowledge",
3050
+ content: () => generateRecipes()
3051
+ },
3052
+ {
3053
+ id: "suggestions",
3054
+ description: "Contextual suggestion patterns for recommending next steps based on current extension code, capabilities, and surfaces. Use when deciding what to suggest after making changes.",
3055
+ type: "knowledge",
3056
+ scopes: ["studio"],
3057
+ content: () => generateSuggestions()
3058
+ },
3059
+ // ── Dual-use knowledge skills (studio + docs) ────────────────
3060
+ {
3061
+ id: "surfaces",
3062
+ description: "Surface types, lifecycle, layout slots, and multi-surface composition. Use when adding or configuring surfaces in an extension.",
3063
+ type: "knowledge",
3064
+ content: () => generateSurfaces()
3065
+ },
3066
+ {
3067
+ id: "external-apis",
3068
+ description: "Direct HTTP requests via data.fetch, allowedDomains configuration, and API wrapper patterns. Use when connecting an extension to external APIs.",
3069
+ type: "knowledge",
3070
+ content: () => generateExternalApis()
3071
+ },
3072
+ {
3073
+ id: "store-and-navigation",
3074
+ description: "Cross-surface state management with createStore, useStore selectors, and store-based navigation patterns. Use when managing shared state or view navigation.",
3075
+ type: "knowledge",
3076
+ content: () => generateStoreAndNavigation()
3077
+ },
3078
+ {
3079
+ id: "styling-and-theming",
3080
+ description: "Host theme inheritance, className usage, layout components, and CSS constraints. Use when styling extension UI or working with the host theme.",
3081
+ type: "knowledge",
3082
+ content: () => generateStylingAndTheming()
3083
+ },
3084
+ // ── Docs-only knowledge skills ──────────────────────────────
3085
+ {
3086
+ id: "quick-start",
3087
+ description: "Step-by-step guide to create, develop, and deploy your first Stackable extension.",
3088
+ type: "action",
3089
+ scopes: ["docs"],
3090
+ content: () => generateQuickStart()
3091
+ },
3092
+ {
3093
+ id: "project-structure",
3094
+ description: "Extension project file structure, entry point, surfaces, store, and manifest configuration.",
3095
+ type: "knowledge",
3096
+ scopes: ["docs"],
3097
+ content: () => generateProjectStructure()
3098
+ },
3099
+ {
3100
+ id: "cli-reference",
3101
+ description: "CLI commands for creating, developing, validating, and deploying Stackable extensions.",
3102
+ type: "knowledge",
3103
+ scopes: ["docs"],
3104
+ content: () => generateCliReference()
3105
+ },
3106
+ {
3107
+ id: "extension-studio",
3108
+ description: "Extension Studio: in-browser builder with AI assistant, component palette, and live preview. Use when learning about Studio or comparing Studio vs CLI workflows.",
3109
+ type: "knowledge",
3110
+ scopes: ["docs"],
3111
+ content: () => generateExtensionStudio()
3112
+ },
3113
+ // ── Cookbook skills (docs-only) ────────────────────────────────
3114
+ {
3115
+ id: "cookbook-structural",
3116
+ description: "Cookbook: extension bootstrap, surface declaration, store navigation, and loading patterns.",
3117
+ type: "knowledge",
3118
+ scopes: ["docs"],
3119
+ content: () => generateCookbookStructural()
3120
+ },
3121
+ {
3122
+ id: "cookbook-capabilities",
3123
+ description: "Cookbook: data.query, data.fetch, context.read, actions.toast, actions.invoke capability examples.",
3124
+ type: "knowledge",
3125
+ scopes: ["docs"],
3126
+ content: () => generateCookbookCapabilities()
3127
+ },
3128
+ {
3129
+ id: "cookbook-events",
3130
+ description: "Cookbook: identity, messaging, and activity event subscriptions + extend identity.",
3131
+ type: "knowledge",
3132
+ scopes: ["docs"],
3133
+ content: () => generateCookbookEvents()
3134
+ },
3135
+ // ── Action skills ─────────────────────────────────────────────
3136
+ {
3137
+ id: "add-surface",
3138
+ description: "Add a new surface (UI slot) to this Stackable extension. Use when the user wants to add a header, content, footer, or footer-links surface.",
3139
+ type: "action",
3140
+ content: () => generateAddSurfaceCommand()
3141
+ },
3142
+ {
3143
+ id: "add-capability",
3144
+ description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke) in this extension. Use when adding a new host-mediated API.",
3145
+ type: "action",
3146
+ content: () => generateAddCapabilityCommand()
3147
+ },
3148
+ {
3149
+ id: "add-component",
3150
+ description: "Add a UI component (ui.Card, ui.Button, ui.Tabs, etc.) to this extension. Use when the user wants to add a visual element.",
3151
+ type: "action",
3152
+ content: () => generateAddComponentCommand()
3153
+ },
3154
+ {
3155
+ id: "validate",
3156
+ description: "Validate this extension for common errors before deploying: manifest, permissions, surfaces, imports. Use before publishing or when debugging issues.",
3157
+ type: "action",
3158
+ content: () => generateValidateExtensionCommand()
3159
+ }
3160
+ ];
3161
+
3162
+ // ../../../node_modules/.pnpm/ultramatter@0.0.4/node_modules/ultramatter/dist/index.js
3163
+ function b(t) {
3164
+ let e = 0, r = "default", i = [];
3165
+ for (let n = 0; n < t.length; n++) {
3166
+ if (t[n] === "-") if (e === 0) if (r === "default" && n == 0 || r === "open" && t[n - 1] === `
3167
+ `) e = 1;
3168
+ else continue;
3169
+ else e++;
3170
+ else e = 0;
3171
+ e === 3 && (r = r === "default" ? "open" : "closed", i.push(n + 1));
3172
+ }
3173
+ switch (r) {
3174
+ case "default":
3175
+ return ["", t];
3176
+ case "open":
3177
+ return ["", t];
3178
+ case "closed":
3179
+ return [t.slice(i[0], i[1] - 3).trim(), t.slice(i[1]).trimStart()];
3180
+ }
3181
+ }
3182
+ var h = (t) => t.slice(0, t.length - t.trimStart().length);
3183
+ var a = (t) => {
3184
+ let e = h(t);
3185
+ return t.split(`
3186
+ `).map((r) => r.slice(e.length)).join(`
3187
+ `);
3188
+ };
3189
+ var E = (t) => {
3190
+ if (t[0] === '"' || t[0] === "'") {
3191
+ let e = t[0];
3192
+ if (t[t.length - 1] === e) return t.slice(1, -1);
3193
+ }
3194
+ return t;
3195
+ };
3196
+ var d = (t) => t === "true" || t === "false" ? t === "true" : Number.isNaN(Number(t)) ? E(t) : Number(t);
3197
+ var o = (t) => {
3198
+ let e = /(^[^\:\s]+):(?!\/)\n?([\s\S]*?(?=^\S)|[\s\S]*$)/gm, r = /[\:\-\[\]\|\#]/gm, i = /#.*$/gm, n = h(t);
3199
+ if (!r.test(t)) return n.length > 1 ? a(t) : d(t.trim());
3200
+ n.length <= 1 && (t = t.trimStart());
3201
+ let f, l = {};
3202
+ for (; f = e.exec(t); ) {
3203
+ let [m, c, g] = f;
3204
+ if (h(g).length > 1) {
3205
+ let u = a(g);
3206
+ l[c] = o(u);
3207
+ } else l[c] = o(g);
3208
+ }
3209
+ if (Object.keys(l).length > 0) return l;
3210
+ let s = t.trim().replace(i, "").trim();
3211
+ return s.startsWith("-") ? s.split(/^\-/gm).filter((c) => c).map((c) => o(c.trimEnd())) : s.startsWith("[") ? (s = s.slice(1, -1), s.split(",").map((m) => d(m.trim()))) : s.startsWith("|") ? a(s.replace("|", "").replace(`
3212
+ `, "")) : d(s.trim());
3213
+ };
3214
+ function R(t) {
3215
+ let [e, r] = b(t);
3216
+ return e ? { frontmatter: o(e), content: r } : { content: r };
3217
+ }
3218
+
3219
+ // ../../sdk/extension/ai-docs/src/skill-loader.ts
3220
+ var listSkills = (scope) => SKILLS.filter((s) => !s.scopes || s.scopes.includes(scope));
3221
+ var getSkillBody = (skill) => {
3222
+ const raw = skill.content();
3223
+ const { content } = R(raw);
3224
+ return content.trim();
3225
+ };
3226
+ var getSkillMetadata = (skill) => ({ id: skill.id, description: skill.description });
3227
+ var lookupSkill = (skills, id) => {
3228
+ const skill = skills.find((s) => s.id === id);
3229
+ if (!skill) {
3230
+ return null;
3231
+ }
3232
+ return getSkillBody(skill);
3233
+ };
3234
+ var findRelevantSkills = (skills, query) => {
3235
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
3236
+ if (terms.length === 0) {
3237
+ return [];
3238
+ }
3239
+ return skills.filter((skill) => {
3240
+ const text = `${skill.id} ${skill.description}`.toLowerCase();
3241
+ return terms.some((term) => text.includes(term));
3242
+ });
3243
+ };
3244
+
3245
+ // ../../sdk/extension/ai-docs/src/snippets/capabilities.ts
3246
+ var CAPABILITY_SNIPPETS = {
3247
+ "data.query": `
3248
+ const capabilities = useCapabilities()
3249
+ const result = await capabilities.data.query({ path: '/your-endpoint', method: 'GET' })
3250
+ `,
3251
+ "data.fetch": `
3252
+ const capabilities = useCapabilities()
3253
+ const response = await capabilities.data.fetch('https://api.example.com/endpoint')
3254
+ `,
3255
+ "context.read": `
3256
+ const capabilities = useCapabilities()
3257
+ const ctx = await capabilities.context.read()
3258
+ `,
3259
+ "actions.toast": `
3260
+ const capabilities = useCapabilities()
3261
+ capabilities.actions.toast({ type: 'success', message: 'Done!' })
3262
+ `,
3263
+ "actions.invoke": `
3264
+ const capabilities = useCapabilities()
3265
+
3266
+ // New conversation with tags and fields
3267
+ await capabilities.actions.invoke('newConversation', {
3268
+ tags: ['stackable', 'order-lookup'],
3269
+ fields: [{ id: 'stackable_action', value: 'order_status' }],
3270
+ metadata: { orderId: '12345' },
3271
+ })
3272
+
3273
+ // Standalone: set tags on current/next conversation
3274
+ await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])
3275
+ `,
3276
+ "events:identity": HOOK_SNIPPETS["events:identity"],
3277
+ "events:messaging": HOOK_SNIPPETS["events:messaging"],
3278
+ "events:activity": HOOK_SNIPPETS["events:activity"],
3279
+ "extend.identity": HOOK_SNIPPETS["extend.identity"]
3280
+ };
3281
+ var EVENT_SNIPPETS = {
3282
+ "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3283
+ import type { IdentityEvent } from '@stackable-labs/sdk-extension-contracts'
3284
+ import { useState } from 'react'
3285
+
3286
+ export function Header(): React.ReactElement {
3287
+ const [user, setUser] = useState<string | null>(null)
3288
+
3289
+ useIdentityEvent('login', (event: IdentityEvent) => {
3290
+ setUser(event.data.state.user?.email ?? null)
3291
+ })
3292
+
3293
+ useIdentityEvent('logout', () => {
3294
+ setUser(null)
3295
+ })
3296
+
3297
+ return (
3298
+ <Surface id="slot.header">
3299
+ <ui.Text className="text-xs">{user ?? 'Not logged in'}</ui.Text>
3300
+ </Surface>
3301
+ )
3302
+ }`,
3303
+ "events:messaging": `import { useMessagingEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3304
+ import type { MessagingEventHandler } from '@stackable-labs/sdk-extension-contracts'
3305
+ import { useState } from 'react'
3306
+
3307
+ export function Content(): React.ReactElement {
3308
+ const [lastPostback, setLastPostback] = useState<string | null>(null)
3309
+
3310
+ useMessagingEvent('postback', (event) => {
3311
+ setLastPostback(event.data.actionName)
3312
+ })
3313
+
3314
+ return (
3315
+ <Surface id="slot.content">
3316
+ <ui.Text className="text-xs">{lastPostback ?? 'No postbacks yet'}</ui.Text>
3317
+ </Surface>
3318
+ )
3319
+ }`,
3320
+ "events:activity": `import { useActivityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3321
+ import type { ActivityEventHandler } from '@stackable-labs/sdk-extension-contracts'
3322
+ import { useState } from 'react'
3323
+
3324
+ export function Content(): React.ReactElement {
3325
+ const [lastEvent, setLastEvent] = useState<string | null>(null)
3326
+
3327
+ useActivityEvent('page_view', (event) => {
3328
+ setLastEvent(event.data.url as string)
3329
+ })
3330
+
3331
+ return (
3332
+ <Surface id="slot.content">
3333
+ <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
3334
+ </Surface>
3335
+ )
3336
+ }`,
3337
+ "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
3338
+ import type { ExtendIdentityHandler } from '@stackable-labs/sdk-extension-contracts'
3339
+
3340
+ // Enrich identity JWT claims before signing.
3341
+ // The host sends base claims (external_id, email, name),
3342
+ // and your handler returns additional claims to merge.
3343
+ // Use ExtendIdentityHandler type with useCallback for memoized handlers.
3344
+ useExtendIdentity((claims) => ({
3345
+ external_id: \`shopify_\${claims.external_id}\`,
3346
+ loyalty_tier: 'gold',
3347
+ }))`
3348
+ };
3349
+
3350
+ // ../../sdk/extension/ai-docs/src/generated/example-snippets-jsx.ts
3351
+ var EXAMPLE_SNIPPETS2 = {
3352
+ "bootstrap": `import { createExtension } from '@stackable-labs/sdk-extension-react'
3353
+ import { Header } from './surfaces/Header'
3354
+ import { Content } from './surfaces/Content'
3355
+ import { Footer } from './surfaces/Footer'
3356
+
3357
+ const Extension = () => (
3358
+ <>
3359
+ <Header />
3360
+ <Content />
3361
+ <Footer />
3362
+ </>
3363
+ )
3364
+
3365
+ // NOTE: extensionId is optional \u2014 used when connected to a registered extension
3366
+ createExtension(() => <Extension />, { extensionId: 'my-extension' })`,
3367
+ "surfaces": `import { Surface, ui } from '@stackable-labs/sdk-extension-react'
3368
+
3369
+ export function Header() {
3370
+ return (
3371
+ <Surface id="slot.header">
3372
+ <ui.Stack direction="column" gap="2" className="p-3">
3373
+ <ui.Text className="text-sm font-medium">Hello from slot.header</ui.Text>
3374
+ </ui.Stack>
3375
+ </Surface>
3376
+ )
3377
+ }
3378
+
3379
+ export function Content() {
3380
+ return (
3381
+ <Surface id="slot.content">
3382
+ <ui.Stack direction="column" gap="3" className="p-4">
3383
+ <ui.Heading level="3">Content Surface</ui.Heading>
3384
+ <ui.Text>Rendered inside the host at slot.content.</ui.Text>
3385
+ </ui.Stack>
3386
+ </Surface>
3387
+ )
3388
+ }`,
3389
+ "store": `import { createStore, useStore, Surface, ui } from '@stackable-labs/sdk-extension-react'
3390
+
3391
+ // store.ts
3392
+
3393
+ export const appStore = createStore({
3394
+ viewState: { type: 'list' },
3395
+ })
3396
+
3397
+ // Content.tsx
3398
+ export function Content() {
3399
+ const viewState = useStore(appStore, (s) => s.viewState)
3400
+
3401
+ const goToDetail = (id) => appStore.set({ viewState: { type: 'detail', id } })
3402
+ const goBack = () => appStore.set({ viewState: { type: 'list' } })
3403
+
3404
+ return (
3405
+ <Surface id="slot.content">
3406
+ {viewState.type === 'list' && (
3407
+ <ui.Button onClick={() => goToDetail('abc123')}>View Detail</ui.Button>
3408
+ )}
3409
+ {viewState.type === 'detail' && (
3410
+ <ui.Stack direction="column" gap="2">
3411
+ <ui.Text>Viewing: {viewState.id}</ui.Text>
3412
+ <ui.Button onClick={goBack}>Back</ui.Button>
3413
+ </ui.Stack>
3414
+ )}
3415
+ </Surface>
3416
+ )
3417
+ }`,
3418
+ "loading": `import { useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
3419
+
3420
+ export function Content() {
3421
+ const { loading, customerId } = useContextData()
3422
+
3423
+ if (loading) {
3424
+ return (
3425
+ <Surface id="slot.content">
3426
+ <ui.Skeleton />
3427
+ </Surface>
3428
+ )
3429
+ }
3430
+
3431
+ if (!customerId) {
3432
+ return (
3433
+ <Surface id="slot.content">
3434
+ <ui.Stack direction="column" gap="1" className="p-4 items-center">
3435
+ <ui.Icon name="user" size="sm" />
3436
+ <ui.Text className="text-xs text-muted-foreground">No customer selected</ui.Text>
3437
+ </ui.Stack>
3438
+ </Surface>
3439
+ )
3440
+ }
3441
+
3442
+ return (
3443
+ <Surface id="slot.content">
3444
+ <ui.Text className="text-sm">Customer: {customerId}</ui.Text>
3445
+ </Surface>
3446
+ )
3447
+ }`,
3448
+ "surfaceContext": `import { useSurfaceContext, Surface, ui } from '@stackable-labs/sdk-extension-react'
3449
+
3450
+ export function Header() {
3451
+ // Lower-level hook \u2014 reads host-pushed context for this specific surface
3452
+ const context = useSurfaceContext()
3453
+
3454
+ return (
3455
+ <Surface id="slot.header">
3456
+ <ui.Text className="text-xs">{context.customerId ?? 'No customer'}</ui.Text>
3457
+ </Surface>
3458
+ )
3459
+ }`,
3460
+ "context.read": `import { useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
3461
+
3462
+ export function Header() {
3463
+ const { loading, customerId, customerEmail } = useContextData()
3464
+
3465
+ if (loading) {
3466
+ return (
3467
+ <Surface id="slot.header">
3468
+ <ui.Text className="text-xs text-muted-foreground">Loading...</ui.Text>
3469
+ </Surface>
3470
+ )
3471
+ }
3472
+
3473
+ return (
3474
+ <Surface id="slot.header">
3475
+ <ui.Stack direction="column" gap="1">
3476
+ <ui.Text className="text-xs font-semibold">{customerEmail}</ui.Text>
3477
+ <ui.Text className="text-xs text-muted-foreground">ID: {customerId}</ui.Text>
3478
+ </ui.Stack>
3479
+ </Surface>
3480
+ )
3481
+ }`,
3482
+ "data.query": `import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
3483
+ import { useState } from 'react'
3484
+
3485
+ export function Content() {
3486
+ const capabilities = useCapabilities()
3487
+ const [data, setData] = useState(null)
3488
+
3489
+ const handleQuery = async () => {
3490
+ const payload = {
3491
+ action: 'getCustomer',
3492
+ customerId: '123',
3493
+ }
3494
+ const result = await capabilities.data.query(payload)
3495
+ setData(result)
3496
+ }
3497
+
3498
+ return (
3499
+ <Surface id="slot.content">
3500
+ <ui.Button onClick={handleQuery}>Fetch Data</ui.Button>
3501
+ </Surface>
3502
+ )
3503
+ }`,
3504
+ "data.fetch": `import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
3505
+ import { useState } from 'react'
3506
+
3507
+ export function Content() {
3508
+ const capabilities = useCapabilities()
3509
+ const [data, setData] = useState(null)
3510
+
3511
+ const handleGet = async () => {
3512
+ const result = await capabilities.data.fetch('https://api.myservice.com/orders')
3513
+ if (result.ok) setData(result.data)
3514
+ }
3515
+
3516
+ const handlePost = async () => {
3517
+ const result = await capabilities.data.fetch(
3518
+ 'https://api.myservice.com/orders',
3519
+ { method: 'POST', body: { limit: 10 } }
3520
+ )
3521
+ if (result.ok) setData(result.data)
3522
+ }
3523
+
3524
+ return (
3525
+ <Surface id="slot.content">
3526
+ <ui.Stack gap="2">
3527
+ <ui.Button onClick={handleGet}>GET Orders</ui.Button>
3528
+ <ui.Button onClick={handlePost}>POST Orders</ui.Button>
3529
+ </ui.Stack>
3530
+ </Surface>
3531
+ )
3532
+ }`,
3533
+ "actions.toast": `import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
3534
+
3535
+ export function Content() {
3536
+ const capabilities = useCapabilities()
3537
+
3538
+ const showToast = async () => {
3539
+ await capabilities.actions.toast({ type: 'success', message: 'Done!' })
3540
+ }
3541
+
3542
+ return (
3543
+ <Surface id="slot.content">
3544
+ <ui.Button onClick={showToast}>Show Toast</ui.Button>
3545
+ </Surface>
3546
+ )
3547
+ }`,
3548
+ "actions.invoke": `import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
3549
+
3550
+ export function Content() {
3551
+ const capabilities = useCapabilities()
3552
+
3553
+ const newConversation = async () => {
3554
+ await capabilities.actions.invoke('newConversation', {
3555
+ tags: ['stackable', 'order-lookup'],
3556
+ fields: [{ id: 'stackable_action', value: 'order_status' }],
3557
+ metadata: { orderId: '12345' },
3558
+ })
3559
+ }
3560
+
3561
+ const setTags = async () => {
3562
+ await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])
3563
+ }
3564
+
3565
+ return (
3566
+ <Surface id="slot.content">
3567
+ <ui.Stack gap="2">
3568
+ <ui.Button onClick={newConversation}>New Conversation</ui.Button>
3569
+ <ui.Button onClick={setTags}>Set Tags</ui.Button>
3570
+ </ui.Stack>
3571
+ </Surface>
3572
+ )
3573
+ }`,
3574
+ "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
3575
+
3576
+ // Enrich identity JWT claims before signing.
3577
+ // The host sends base claims (external_id, email, name),
3578
+ // and your handler returns additional claims to merge.
3579
+ useExtendIdentity((claims) => ({
3580
+ external_id: \`shopify_\${claims.external_id}\`,
3581
+ loyalty_tier: 'gold',
3582
+ }))`,
3583
+ "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3584
+ import { useState } from 'react'
3585
+
3586
+ export function Header() {
3587
+ const [user, setUser] = useState(null)
3588
+
3589
+ useIdentityEvent('login', (event) => {
3590
+ setUser(event.data.state.user?.email ?? null)
3591
+ })
3592
+
3593
+ useIdentityEvent('logout', () => {
3594
+ setUser(null)
3595
+ })
3596
+
3597
+ return (
3598
+ <Surface id="slot.header">
3599
+ <ui.Text className="text-xs">{user ?? 'Not logged in'}</ui.Text>
3600
+ </Surface>
3601
+ )
3602
+ }`,
3603
+ "events:messaging": `import { useMessagingEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3604
+ import { useState } from 'react'
3605
+
3606
+ export function Content() {
3607
+ const [lastPostback, setLastPostback] = useState(null)
3608
+
3609
+ useMessagingEvent('postback', (event) => {
3610
+ setLastPostback(event.data.actionName)
3611
+ })
3612
+
3613
+ return (
3614
+ <Surface id="slot.content">
3615
+ <ui.Text className="text-xs">{lastPostback ?? 'No postbacks yet'}</ui.Text>
3616
+ </Surface>
3617
+ )
3618
+ }`,
3619
+ "events:activity": `import { useActivityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3620
+ import { useState } from 'react'
3621
+
3622
+ export function Content() {
3623
+ const [lastEvent, setLastEvent] = useState(null)
3624
+
3625
+ useActivityEvent('page_view', (event) => {
3626
+ setLastEvent(event.data.url)
3627
+ })
3628
+
3629
+ return (
3630
+ <Surface id="slot.content">
3631
+ <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
3632
+ </Surface>
3633
+ )
3634
+ }`
3635
+ };
3636
+
3637
+ // ../../sdk/extension/ai-docs/src/generated/capability-snippets-jsx.ts
3638
+ var CAPABILITY_SNIPPETS2 = {
3639
+ "data.query": `const capabilities = useCapabilities()
3640
+ const result = await capabilities.data.query({ path: '/your-endpoint', method: 'GET' })`,
3641
+ "data.fetch": `const capabilities = useCapabilities()
3642
+ const response = await capabilities.data.fetch('https://api.example.com/endpoint')`,
3643
+ "context.read": `const capabilities = useCapabilities()
3644
+ const ctx = await capabilities.context.read()`,
3645
+ "actions.toast": `const capabilities = useCapabilities()
3646
+ capabilities.actions.toast({ type: 'success', message: 'Done!' })`,
3647
+ "actions.invoke": `const capabilities = useCapabilities()
3648
+
3649
+ // New conversation with tags and fields
3650
+ await capabilities.actions.invoke('newConversation', {
3651
+ tags: ['stackable', 'order-lookup'],
3652
+ fields: [{ id: 'stackable_action', value: 'order_status' }],
3653
+ metadata: { orderId: '12345' },
3654
+ })
3655
+
3656
+ // Standalone: set tags on current/next conversation
3657
+ await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])`,
3658
+ "events:identity": `import { useIdentityEvent } from '@stackable-labs/sdk-extension-react'
3659
+
3660
+ useIdentityEvent('login', (event) => {
3661
+ console.log('User logged in:', event.data.state.user?.email)
3662
+ })
3663
+ useIdentityEvent('logout', () => {
3664
+ console.log('User logged out')
3665
+ })`,
3666
+ "events:messaging": `import { useMessagingEvent } from '@stackable-labs/sdk-extension-react'
3667
+
3668
+ useMessagingEvent('postback:Buy Now', (event) => {
3669
+ console.log('Postback:', event.data.actionName, event.data.conversationId)
3670
+ })`,
3671
+ "events:activity": `import { useActivityEvent } from '@stackable-labs/sdk-extension-react'
3672
+
3673
+ useActivityEvent('product_view', (event) => {
3674
+ console.log('Activity:', event.eventName, event.data)
3675
+ })`,
3676
+ "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
3677
+
3678
+ useExtendIdentity((claims) => ({
3679
+ external_id: \`custom_\${claims.external_id}\`,
3680
+ loyalty_tier: 'gold',
3681
+ }))`
3682
+ };
3683
+ var EVENT_SNIPPETS2 = {
3684
+ "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3685
+ import { useState } from 'react'
3686
+
3687
+ export function Header() {
3688
+ const [user, setUser] = useState(null)
3689
+
3690
+ useIdentityEvent('login', (event) => {
3691
+ setUser(event.data.state.user?.email ?? null)
3692
+ })
3693
+
3694
+ useIdentityEvent('logout', () => {
3695
+ setUser(null)
3696
+ })
3697
+
3698
+ return (
3699
+ <Surface id="slot.header">
3700
+ <ui.Text className="text-xs">{user ?? 'Not logged in'}</ui.Text>
3701
+ </Surface>
3702
+ )
3703
+ }`,
3704
+ "events:messaging": `import { useMessagingEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3705
+ import { useState } from 'react'
3706
+
3707
+ export function Content() {
3708
+ const [lastPostback, setLastPostback] = useState(null)
3709
+
3710
+ useMessagingEvent('postback', (event) => {
3711
+ setLastPostback(event.data.actionName)
3712
+ })
3713
+
3714
+ return (
3715
+ <Surface id="slot.content">
3716
+ <ui.Text className="text-xs">{lastPostback ?? 'No postbacks yet'}</ui.Text>
3717
+ </Surface>
3718
+ )
3719
+ }`,
3720
+ "events:activity": `import { useActivityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3721
+ import { useState } from 'react'
3722
+
3723
+ export function Content() {
3724
+ const [lastEvent, setLastEvent] = useState(null)
3725
+
3726
+ useActivityEvent('page_view', (event) => {
3727
+ setLastEvent(event.data.url)
3728
+ })
3729
+
3730
+ return (
3731
+ <Surface id="slot.content">
3732
+ <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
3733
+ </Surface>
3734
+ )
3735
+ }`,
3736
+ "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
3737
+
3738
+ // Enrich identity JWT claims before signing.
3739
+ // The host sends base claims (external_id, email, name),
3740
+ // and your handler returns additional claims to merge.
3741
+ // Use ExtendIdentityHandler type with useCallback for memoized handlers.
3742
+ useExtendIdentity((claims) => ({
3743
+ external_id: \`shopify_\${claims.external_id}\`,
3744
+ loyalty_tier: 'gold',
3745
+ }))`
3746
+ };
3747
+
3748
+ // ../../sdk/extension/ai-docs/src/index.ts
3749
+ var pair = (tsx, jsx) => Object.fromEntries(Object.entries(tsx).map(([k, v]) => [k, { tsx: v, jsx: jsx[k] ?? v }]));
3750
+ pair(EXAMPLE_SNIPPETS, EXAMPLE_SNIPPETS2);
3751
+ pair(CAPABILITY_SNIPPETS, CAPABILITY_SNIPPETS2);
3752
+ pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
3753
+
3754
+ // ../../lib/contracts/src/permissions.ts
3755
+ var SUPER_ROLE = {
3756
+ ADMIN: "org:super_admin"
3757
+ };
3758
+ var ORG_ROLE = {
3759
+ ADMIN: "org:admin",
3760
+ OWNER: "org:owner"};
3761
+ var EDITOR_ROLES = [
3762
+ SUPER_ROLE.ADMIN,
3763
+ ORG_ROLE.ADMIN,
3764
+ ORG_ROLE.OWNER
3765
+ ];
3766
+ [
3767
+ ...Object.values(SUPER_ROLE),
3768
+ ...Object.values(EDITOR_ROLES)
3769
+ ];
3770
+
3771
+ // ../../lib/utils-services/src/auth/index.ts
3772
+ var STANDALONE_CLIENT = {
3773
+ MCP: "@stackable-labs/mcp-app-extension"
3774
+ };
3775
+
3776
+ // package.json
3777
+ var package_default = {
3778
+ version: "0.0.0"};
3779
+
3780
+ // src/server.ts
3781
+ var MCP_CLIENT_NAME = STANDALONE_CLIENT.MCP;
3782
+ var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
3783
+ var textContent = (text) => ({ content: [{ type: "text", text }] });
3784
+ var errorContent = (text) => ({ content: [{ type: "text", text }], isError: true });
3785
+ var createMcpServer = (options = {}) => {
3786
+ const server = new McpServer({
3787
+ name: "stackable-extension-dev",
3788
+ version: package_default.version
3789
+ });
3790
+ const getAuthToken = async () => {
3791
+ if (options.authContext?.token) {
3792
+ return options.authContext.token;
3793
+ }
3794
+ const { readFile } = await import('fs/promises');
3795
+ const { join } = await import('path');
3796
+ const { homedir } = await import('os');
3797
+ const authFile = join(homedir(), ".stackable", "auth.json");
3798
+ let state;
3799
+ try {
3800
+ const content = await readFile(authFile, "utf8");
3801
+ state = JSON.parse(content);
3802
+ } catch {
3803
+ throw new Error("Not authenticated. Run 'stackable-app-extension auth login' first.");
3804
+ }
3805
+ const [, payload] = state.token.split(".");
3806
+ if (payload) {
3807
+ try {
3808
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
3809
+ if (decoded.exp && Date.now() >= decoded.exp * 1e3) {
3810
+ throw new Error("Session expired. Run 'stackable-app-extension auth login' to re-authenticate.");
3811
+ }
3812
+ } catch (err) {
3813
+ if (err instanceof Error && err.message.includes("expired")) {
3814
+ throw err;
3815
+ }
3816
+ }
3817
+ }
3818
+ return state.token;
3819
+ };
3820
+ const authHeaders = (token) => ({
3821
+ authorization: `Bearer ${token}`,
3822
+ "content-type": "application/json",
3823
+ "x-client-name": MCP_CLIENT_NAME
3824
+ });
3825
+ const callApi = async (path) => {
3826
+ const token = await getAuthToken();
3827
+ const baseUrl = process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
3828
+ const res = await fetch(`${baseUrl}${path}`, { headers: authHeaders(token) });
3829
+ if (!res.ok) {
3830
+ return errorContent(`API error: ${res.status} ${res.statusText}`);
3831
+ }
3832
+ const data = await res.json();
3833
+ return textContent(JSON.stringify(data, null, 2));
3834
+ };
3835
+ const withAuth = async (fn) => {
3836
+ try {
3837
+ return await fn();
3838
+ } catch (err) {
3839
+ return errorContent(err.message);
3840
+ }
3841
+ };
3842
+ const registrySkills = listSkills("registry");
3843
+ for (const skill of registrySkills) {
3844
+ server.registerResource(
3845
+ skill.id,
3846
+ `sdk://${skill.id}`,
3847
+ { description: skill.description, mimeType: "text/markdown" },
3848
+ async (uri) => ({
3849
+ contents: [{ uri: uri.href, text: getSkillBody(skill), mimeType: "text/markdown" }]
3850
+ })
3851
+ );
3852
+ }
3853
+ server.registerTool("list_skills", {
3854
+ description: "List all available SDK skills with descriptions"
3855
+ }, async () => textContent(JSON.stringify(registrySkills.map(getSkillMetadata), null, 2)));
3856
+ server.registerTool("lookup_skill", {
3857
+ description: "Look up a specific SDK skill by ID or search by keyword. Returns full skill content for ID lookup, or matching skill metadata for keyword search.",
3858
+ inputSchema: { id: z.string().optional().describe('Exact skill ID (e.g. "capabilities", "patterns")'), query: z.string().optional().describe("Search keywords to find relevant skills") }
3859
+ }, async ({ id, query }) => {
3860
+ if (id) {
3861
+ const body = lookupSkill(registrySkills, id);
3862
+ if (!body) {
3863
+ return errorContent(`No skill found with ID "${id}". Use list_skills to see available IDs.`);
3864
+ }
3865
+ return textContent(body);
3866
+ }
3867
+ if (query) {
3868
+ const matches = findRelevantSkills(registrySkills, query);
3869
+ if (matches.length === 0) {
3870
+ return errorContent(`No skills matched "${query}". Use list_skills to see available skills.`);
3871
+ }
3872
+ return textContent(JSON.stringify(matches.map(getSkillMetadata), null, 2));
3873
+ }
3874
+ return errorContent('Provide either "id" or "query" parameter.');
3875
+ });
3876
+ server.registerTool("validate_manifest", {
3877
+ description: "Validate an extension manifest.json. Checks required fields, valid permissions, targets, and allowedDomains consistency with permissions.",
3878
+ inputSchema: { manifestContent: z.string().describe("The full manifest.json content as a string") }
3879
+ }, async ({ manifestContent }) => {
3880
+ const errors = [];
3881
+ let manifest;
3882
+ try {
3883
+ manifest = JSON.parse(manifestContent);
3884
+ } catch {
3885
+ return errorContent("Invalid JSON: could not parse manifest content.");
3886
+ }
3887
+ if (!manifest.name || typeof manifest.name !== "string") {
3888
+ errors.push('Missing or invalid "name" field (must be a non-empty string).');
3889
+ }
3890
+ if (!manifest.version || typeof manifest.version !== "string") {
3891
+ errors.push('Missing or invalid "version" field (must be a non-empty string).');
3892
+ }
3893
+ if (!Array.isArray(manifest.targets) || manifest.targets.length === 0) {
3894
+ errors.push('Missing or empty "targets" array. At least one surface target is required.');
3895
+ }
3896
+ if (!Array.isArray(manifest.permissions)) {
3897
+ errors.push('Missing "permissions" array.');
3898
+ } else {
3899
+ const validPermissions = new Set(PERMISSIONS);
3900
+ for (const perm of manifest.permissions) {
3901
+ if (!validPermissions.has(perm)) {
3902
+ errors.push(`Invalid permission "${perm}". Valid permissions: ${PERMISSIONS.join(", ")}`);
3903
+ }
3904
+ }
3905
+ }
3906
+ if (!Array.isArray(manifest.allowedDomains)) {
3907
+ errors.push('Missing "allowedDomains" array. Use an empty array if no external API calls are needed.');
3908
+ }
3909
+ const permissions = manifest.permissions ?? [];
3910
+ if (permissions.includes("data:fetch") && Array.isArray(manifest.allowedDomains) && manifest.allowedDomains.length === 0) {
3911
+ errors.push('"data:fetch" permission declared but "allowedDomains" is empty. Add the domains your extension needs to fetch from.');
3912
+ }
3913
+ if (errors.length === 0) {
3914
+ return textContent("Manifest is valid.");
3915
+ }
3916
+ return errorContent(`Manifest validation failed:
3917
+ ${errors.map((e) => `- ${e}`).join("\n")}`);
3918
+ });
3919
+ server.registerTool("validate_permissions", {
3920
+ description: "Static analysis: match declared manifest permissions against actual capability and event hook usage in source files. Detects unused permissions and missing permissions.",
3921
+ inputSchema: {
3922
+ manifestContent: z.string().describe("The full manifest.json content as a string"),
3923
+ sourceFiles: z.record(z.string(), z.string()).describe("Map of filename \u2192 source content for all extension source files")
3924
+ }
3925
+ }, async ({ manifestContent, sourceFiles }) => {
3926
+ let manifest;
3927
+ try {
3928
+ manifest = JSON.parse(manifestContent);
3929
+ } catch {
3930
+ return errorContent("Invalid JSON: could not parse manifest content.");
3931
+ }
3932
+ const declaredPermissions = new Set(manifest.permissions ?? []);
3933
+ const usedPermissions = /* @__PURE__ */ new Set();
3934
+ const allSource = Object.values(sourceFiles).join("\n");
3935
+ for (const [capability, permission] of Object.entries(CAPABILITY_PERMISSION_MAP)) {
3936
+ if (allSource.includes(capability)) {
3937
+ usedPermissions.add(permission);
3938
+ }
3939
+ }
3940
+ const eventHookMap = {
3941
+ useIdentityEvent: "events:identity",
3942
+ useMessagingEvent: "events:messaging",
3943
+ useActivityEvent: "events:activity"
3944
+ };
3945
+ for (const [hook, permission] of Object.entries(eventHookMap)) {
3946
+ if (allSource.includes(hook)) {
3947
+ usedPermissions.add(permission);
3948
+ }
3949
+ }
3950
+ const issues = [];
3951
+ for (const perm of declaredPermissions) {
3952
+ if (!usedPermissions.has(perm)) {
3953
+ issues.push(`Unused permission "${perm}" \u2014 declared in manifest but no matching capability/hook usage found in source.`);
3954
+ }
3955
+ }
3956
+ for (const perm of usedPermissions) {
3957
+ if (!declaredPermissions.has(perm)) {
3958
+ issues.push(`Missing permission "${perm}" \u2014 capability/hook used in source but not declared in manifest.`);
3959
+ }
3960
+ }
3961
+ if (issues.length === 0) {
3962
+ return textContent("Permissions are consistent with source code usage.");
3963
+ }
3964
+ return textContent(`Permission analysis:
3965
+ ${issues.map((i) => `- ${i}`).join("\n")}`);
3966
+ });
3967
+ server.registerTool("list_apps", {
3968
+ description: "List available apps for development with IDs, names, and targets. Requires CLI auth."
3969
+ }, async () => withAuth(() => callApi("/app-extension")));
3970
+ server.registerTool("list_extensions", {
3971
+ description: "List extensions under an app with status, version, and bundle URL. Requires CLI auth.",
3972
+ inputSchema: { appId: z.string().describe("App ID") }
3973
+ }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/extensions`)));
3974
+ server.registerTool("get_extension", {
3975
+ description: "Get extension detail including manifest, permissions, and targets. Requires CLI auth.",
3976
+ inputSchema: { appId: z.string().describe("App ID"), extensionId: z.string().describe("Extension ID") }
3977
+ }, async ({ appId, extensionId }) => withAuth(() => callApi(`/app-extension/${appId}/extensions/${extensionId}`)));
3978
+ server.registerTool("list_instances", {
3979
+ description: "List instances under an app. Requires CLI auth.",
3980
+ inputSchema: { appId: z.string().describe("App ID") }
3981
+ }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/instances`)));
3982
+ return server;
3983
+ };
3984
+
3985
+ export { createMcpServer };