@stackable-labs/mcp-app-extension 0.1.1

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