@stackable-labs/mcp-app-extension 0.23.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { STANDALONE_CLIENT, MCP_AUTH_FILE, getToken, STANDALONE_CLIENT_AUTH_FILE } from './chunk-DCPV7HMV.js';
3
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
- import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, PERMISSIONS, CAPABILITY_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
4
+ import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, PERMISSIONS, CAPABILITY_PERMISSION_MAP, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
5
+ import fs4, { readFile, mkdir, writeFile, constants } from 'fs/promises';
6
+ import path, { join } from 'path';
7
+ import os, { homedir } from 'os';
6
8
  import { z } from 'zod';
9
+ import { createServer } from 'http';
10
+ import process6 from 'process';
11
+ import { Buffer as Buffer$1 } from 'buffer';
12
+ import { fileURLToPath } from 'url';
13
+ import { promisify } from 'util';
14
+ import childProcess, { execFile } from 'child_process';
15
+ import fs3 from 'fs';
7
16
 
8
17
  // ../../sdk/extension/ai-docs/src/generated/template-content.ts
9
18
  var PATTERN_SECTIONS = [
@@ -532,6 +541,8 @@ const result = await capabilities.data.fetch('https://api.example.com/orders', {
532
541
  - For optional secret fields, the entire header is omitted if the value is not configured
533
542
  - Declare secret fields in your \`manifest.json\` \`settingsSchema\` with \`"secret": true\`
534
543
 
544
+ > See [Instance Settings](./instance-settings) for the full schema-declaration + storage-mode story, including which field types accept \`secret: true\`.
545
+
535
546
  ## context.read \u2014 Read Host Context
536
547
  Read host-provided context (customer ID, email, extension settings, etc.).
537
548
  - **Permission required:** \`context:read\`
@@ -1150,6 +1161,202 @@ const context = await capabilities.context.read()
1150
1161
  \`\`\`
1151
1162
  `;
1152
1163
  };
1164
+
1165
+ // ../../sdk/extension/ai-docs/src/generators/instance-settings.ts
1166
+ var SECRET_ALLOWED = ["text", "textarea", "email"];
1167
+ var generateInstanceSettings = () => {
1168
+ const fm = frontmatter({
1169
+ root: false,
1170
+ targets: ["*"],
1171
+ description: "Instance Settings: declaring per-Instance configuration in your extension manifest, the install-time admin form, and reading both regular and secure values from extension code.",
1172
+ globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
1173
+ });
1174
+ return `${fm}
1175
+
1176
+ # Instance Settings
1177
+
1178
+ Instance Settings are the per-Instance configuration values your extension needs in
1179
+ order to do its job \u2014 API keys, account identifiers, environment toggles, feature
1180
+ flags. You declare them once in your extension's \`manifest.json\`; whoever installs
1181
+ your extension fills them in from the admin dashboard at install time; your code
1182
+ reads the values at runtime.
1183
+
1184
+ There are two flavors:
1185
+
1186
+ - **Regular** values live in plaintext and are readable from your extension code.
1187
+ Use them for non-sensitive configuration the UI needs to branch on.
1188
+ - **Secure** values are encrypted at rest and **never** leave the server. Use them
1189
+ for credentials. Your code references them as a placeholder; the proxy
1190
+ substitutes the real value server-side when it makes the outbound request.
1191
+
1192
+ ## 1. Declare your settings in \`manifest.json\`
1193
+
1194
+ Add a \`settingsSchema\` array to \`packages/extension/public/manifest.json\`. Each
1195
+ entry describes one input on the install-time admin form:
1196
+
1197
+ \`\`\`json
1198
+ {
1199
+ "name": "My Extension",
1200
+ "version": "1.0.0",
1201
+ "targets": ["slot.content"],
1202
+ "permissions": ["context:read", "data:fetch"],
1203
+ "allowedDomains": ["api.example.com"],
1204
+ "settingsSchema": [
1205
+ {
1206
+ "identifier": "apiKey",
1207
+ "label": "API Key",
1208
+ "type": "text",
1209
+ "secret": true,
1210
+ "required": true,
1211
+ "description": "Your account API key. Stored encrypted; injected server-side into outbound request headers."
1212
+ },
1213
+ {
1214
+ "identifier": "environmentType",
1215
+ "label": "Environment",
1216
+ "type": "select",
1217
+ "options": [
1218
+ { "label": "Production", "value": "prod" },
1219
+ { "label": "Sandbox", "value": "sandbox" }
1220
+ ]
1221
+ }
1222
+ ]
1223
+ }
1224
+ \`\`\`
1225
+
1226
+ That's the entire authoring step. The next time someone installs (or re-syncs)
1227
+ your extension, the admin dashboard renders a form from this schema on the
1228
+ Instance settings page. Regular fields show inline values; secret fields show a
1229
+ masked input that accepts a value once and displays \`\u2022\u2022\u2022\u2022\` after save (the
1230
+ cleartext value is never returned by the API).
1231
+
1232
+ ### Field types
1233
+
1234
+ | Type | Renders as | Accepts \`secret: true\` |
1235
+ |------|------------|--------------------------|
1236
+ | \`text\` | Single-line text input | Yes |
1237
+ | \`textarea\` | Multi-line text input | Yes |
1238
+ | \`email\` | Email input with format validation | Yes |
1239
+ | \`number\` | Numeric input (\`min\` / \`max\` / \`step\`) | No |
1240
+ | \`select\` | Dropdown (use \`allowMultiple\` for multi-select) | No |
1241
+ | \`radio\` | Radio button group | No |
1242
+ | \`toggle\` | On/off switch | No |
1243
+ | \`tags\` | Tag list / multi-string entry | No |
1244
+
1245
+ The \`secret: true\` flag is only valid on \`${SECRET_ALLOWED.join("`, `")}\` types.
1246
+
1247
+ All fields share these optional properties: \`required\`, \`placeholder\`,
1248
+ \`description\` (help text shown below the input), and \`dependsOn\` (conditional
1249
+ visibility \u2014 show this field only when another field has a matching value).
1250
+
1251
+ ## 2. The install-time admin form
1252
+
1253
+ When someone installs your extension into one of their Instances, the admin
1254
+ dashboard reads your \`settingsSchema\` and generates a form. Each field becomes
1255
+ an input; \`required\` fields block save; \`description\` text becomes inline help;
1256
+ \`dependsOn\` rules show or hide fields based on other field values.
1257
+
1258
+ Each Instance gets its own copy of these values, so the same extension can run
1259
+ side-by-side on multiple Instances with completely different credentials and
1260
+ configuration. The installer can edit values later from the same Instance
1261
+ settings page \u2014 your code always sees the current values.
1262
+
1263
+ ## 3. Read regular values: \`useSettings()\` (or \`useContextData()\`)
1264
+
1265
+ Regular (non-secret) values are exposed to your extension code via the
1266
+ \`useSettings()\` hook, keyed by each field's \`identifier\`. This requires the
1267
+ \`context:read\` permission.
1268
+
1269
+ \`\`\`tsx
1270
+ import { Surface, ui, useSettings } from '@stackable-labs/sdk-extension-react'
1271
+
1272
+ export const Content = () => {
1273
+ const settings = useSettings()
1274
+ const environmentType = (settings.environmentType as string) || 'prod'
1275
+
1276
+ return (
1277
+ <Surface id="slot.content">
1278
+ <ui.Card>
1279
+ <ui.CardContent>
1280
+ <ui.Text>Looking up orders\u2026</ui.Text>
1281
+ {environmentType === 'sandbox' && (
1282
+ <ui.Text className="text-xs opacity-50">Sandbox Mode</ui.Text>
1283
+ )}
1284
+ </ui.CardContent>
1285
+ </ui.Card>
1286
+ </Surface>
1287
+ )
1288
+ }
1289
+ \`\`\`
1290
+
1291
+ If you need settings alongside other context data (customer, locale, etc.), they
1292
+ also come back from \`useContextData()\`:
1293
+
1294
+ \`\`\`tsx
1295
+ import { useContextData } from '@stackable-labs/sdk-extension-react'
1296
+
1297
+ const { loading, settings, customerId } = useContextData()
1298
+ \`\`\`
1299
+
1300
+ > **Note:** Secret fields are **never** present in \`useSettings()\` or on
1301
+ > \`ctx.settings\`. Reading \`settings.apiKey\` for a \`secret: true\` field returns
1302
+ > \`undefined\`, even when a value is configured. If you find yourself reaching
1303
+ > for a secret value in extension code, you almost certainly want the
1304
+ > \`data.fetch\` placeholder pattern below instead.
1305
+
1306
+ ## 4. Use secure values: \`{{settings.<identifier>}}\` in \`data.fetch\`
1307
+
1308
+ Secure values are decrypted only by the proxy Lambda that handles \`data.fetch\` \u2014
1309
+ at substitution time, per-request, then discarded. Your code references them as
1310
+ template placeholders in **header values**:
1311
+
1312
+ \`\`\`tsx
1313
+ import { useCapabilities, useSettings } from '@stackable-labs/sdk-extension-react'
1314
+
1315
+ const capabilities = useCapabilities()
1316
+ const settings = useSettings()
1317
+ const environmentType = (settings.environmentType as string) || 'prod'
1318
+
1319
+ const result = await capabilities.data.fetch('https://api.example.com/orders', {
1320
+ method: 'GET',
1321
+ headers: {
1322
+ // environmentType is a regular value \u2014 interpolate it normally
1323
+ 'X-Environment': environmentType,
1324
+ // apiKey is secret \u2014 the proxy substitutes it server-side; the cleartext
1325
+ // value never enters this code path
1326
+ 'X-Api-Key': '{{settings.apiKey}}',
1327
+ },
1328
+ })
1329
+ if (!result.ok) throw new Error(\`Request failed: \${result.status}\`)
1330
+ \`\`\`
1331
+
1332
+ Placeholders are only resolved in **header values** \u2014 not URLs, not header
1333
+ names, not the request body. For \`required: true\` secret fields the proxy
1334
+ returns \`400\` when the value is not configured; for optional secret fields
1335
+ the entire header is omitted.
1336
+
1337
+ ## Regular vs secure at a glance
1338
+
1339
+ | | Regular | Secure (\`secret: true\`) |
1340
+ |--|---------|--------------------------|
1341
+ | **Stored** | Plaintext | Encrypted with a per-Instance derived key |
1342
+ | **Readable from extension code?** | Yes \u2014 \`useSettings().<identifier>\` | **Never** \u2014 secrets are not serialized to the browser or sandbox |
1343
+ | **How to use the value** | Read at render time and use freely | Reference as a \`{{settings.<identifier>}}\` placeholder in \`data.fetch\` headers \u2014 the proxy substitutes server-side |
1344
+ | **Returned by admin API after save?** | Yes (the value is shown back in the form) | No (the form shows \`\u2022\u2022\u2022\u2022\`; the cleartext value is unrecoverable) |
1345
+
1346
+ ## When in doubt, mark it \`secret: true\`
1347
+
1348
+ The cost is negligible \u2014 one encryption per write, one decryption per outbound
1349
+ request. The blast radius of an accidental log line, screenshot, or sandbox
1350
+ exfiltration drops to zero. Secrets on one Instance also can't be used to
1351
+ decrypt secrets on any other Instance, even when both Instances run the same
1352
+ extension \u2014 encryption keys are derived per-Instance.
1353
+
1354
+ If a value is sensitive at all (API keys, OAuth tokens, signing secrets, webhook
1355
+ shared secrets, personal access tokens), declare the field \`secret: true\` and
1356
+ reach for it via the \`data.fetch\` placeholder pattern. Save \`useSettings()\` for
1357
+ the values your UI legitimately needs to branch on.
1358
+ `;
1359
+ };
1153
1360
  var generatePermissions = () => {
1154
1361
  const fm = frontmatter({
1155
1362
  root: false,
@@ -1374,10 +1581,10 @@ var generatePatterns = () => {
1374
1581
  description: "Code patterns: store navigation, API wrapper, surface composition",
1375
1582
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
1376
1583
  });
1377
- const sections = PATTERN_SECTIONS.map(({ path, title, code }) => `## ${title}
1584
+ const sections = PATTERN_SECTIONS.map(({ path: path2, title, code }) => `## ${title}
1378
1585
 
1379
1586
  \`\`\`tsx
1380
- // src/${path}
1587
+ // src/${path2}
1381
1588
  ${code}
1382
1589
  \`\`\``).join("\n\n");
1383
1590
  const body = `# Code Patterns
@@ -1397,10 +1604,10 @@ var generateRecipes = () => {
1397
1604
  description: "Reference code and recipes for common extension development tasks",
1398
1605
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
1399
1606
  });
1400
- const references = RECIPE_SECTIONS.map(({ path, title, code }) => `## Reference: ${title}
1607
+ const references = RECIPE_SECTIONS.map(({ path: path2, title, code }) => `## Reference: ${title}
1401
1608
 
1402
1609
  \`\`\`tsx
1403
- // src/${path}
1610
+ // src/${path2}
1404
1611
  ${code}
1405
1612
  \`\`\``).join("\n\n");
1406
1613
  const body = `# Recipes
@@ -1540,28 +1747,42 @@ export const Header = () => {
1540
1747
  - **Use ScrollArea** \u2014 wrap content surfaces in \`<ui.ScrollArea>\` for overflow handling
1541
1748
  `;
1542
1749
  };
1543
-
1544
- // ../../sdk/extension/ai-docs/src/cli-commands.ts
1545
1750
  var DLX = "pnpm --config.dlx-cache-max-age=0 dlx";
1546
1751
  var CLI = {
1547
1752
  /** Scaffold a new extension project */
1548
- create: (name = "<project-name>") => `${DLX} @stackable-labs/create-extension ${name}`,
1753
+ create: (name = "<extension-name>") => `${DLX} @stackable-labs/create-extension ${name}`,
1549
1754
  /** Start dev servers with hot reload */
1550
1755
  dev: `${DLX} @stackable-labs/cli-app-extension@latest dev`,
1551
- /** Deploy the extension */
1756
+ /** Deploy the extension (future) */
1552
1757
  deploy: `${DLX} @stackable-labs/cli-app-extension@latest deploy`,
1553
- /** Validate the extension for common errors */
1758
+ /** Validate the extension for common errors (coming soon) */
1554
1759
  validate: `${DLX} @stackable-labs/cli-app-extension@latest validate`,
1555
1760
  /** Scaffold from an existing extension */
1556
1761
  scaffold: `${DLX} @stackable-labs/cli-app-extension@latest scaffold`,
1557
1762
  /** Update an existing extension */
1558
- update: `${DLX} @stackable-labs/cli-app-extension@latest update`
1763
+ update: `${DLX} @stackable-labs/cli-app-extension@latest update`,
1764
+ /** Manage CLI authentication (browser-based OAuth) */
1765
+ auth: {
1766
+ login: `${DLX} @stackable-labs/cli-app-extension@latest auth login`,
1767
+ logout: `${DLX} @stackable-labs/cli-app-extension@latest auth logout`,
1768
+ status: `${DLX} @stackable-labs/cli-app-extension@latest auth status`
1769
+ },
1770
+ /** AI editor configuration tools (Skills + MCP) */
1771
+ ai: {
1772
+ /** Download AI editor config files (Stackable Skills) into your project */
1773
+ scaffold: `${DLX} @stackable-labs/cli-app-extension@latest ai scaffold`,
1774
+ /** Add Stackable MCP server config to your project */
1775
+ mcp: `${DLX} @stackable-labs/cli-app-extension@latest ai mcp`
1776
+ }
1559
1777
  };
1560
- var TEMPLATE_FLAVORS = [
1561
- { name: "minimal", label: "Minimal", description: "Bare minimum \u2014 single surface, hello-world component" },
1562
- { name: "starter", label: "Starter", description: "Common patterns \u2014 store, api helpers, menu (default)" },
1563
- { name: "kitchen-sink", label: "Kitchen Sink", description: "Everything \u2014 every component, capability, surface, and hook" }
1564
- ];
1778
+ var TEMPLATE_FLAVOR_META = {
1779
+ minimal: { label: "Minimal", description: "Bare minimum \u2014 single surface, hello-world component" },
1780
+ starter: { label: "Starter", description: "Common patterns \u2014 store, api helpers, menu (default)" },
1781
+ "kitchen-sink": { label: "Kitchen Sink", description: "Everything \u2014 every component, capability, surface, and hook" }
1782
+ };
1783
+ var TEMPLATE_FLAVOR_DETAILS = TEMPLATE_FLAVORS.map(
1784
+ (name) => ({ name, ...TEMPLATE_FLAVOR_META[name] })
1785
+ );
1565
1786
 
1566
1787
  // ../../sdk/extension/ai-docs/src/generators/quick-start.ts
1567
1788
  var generateQuickStart = () => {
@@ -1577,6 +1798,21 @@ var generateQuickStart = () => {
1577
1798
 
1578
1799
  Create, develop, and deploy your first Stackable extension in minutes.
1579
1800
 
1801
+ ## Choosing your path
1802
+
1803
+ Stackable supports two authoring paths. Both produce the same kind of extension and ship to the same marketplace.
1804
+
1805
+ | | AI Extension Studio | CLI (this guide) |
1806
+ |---|---|---|
1807
+ | **Best for** | Prototyping, learning, quick iterations | Production extensions, team workflows |
1808
+ | **Code structure** | Single file | Multi-file (surfaces/, components/, lib/) |
1809
+ | **Preview** | Built-in live preview | Local dev server + Cloudflare tunnel |
1810
+ | **AI assistance** | Sidekick chat + smart insertion | Skills, MCP server, and Claude Code plugin (see [AI-Accelerated Development](/docs/reference/ai-accelerated-development)) |
1811
+ | **Version control** | Auto-saved to cloud | Git-based |
1812
+ | **Deployment** | Link to extension + deploy | CLI deploy command |
1813
+
1814
+ Both produce the same output \u2014 a Stackable extension that runs in the host application via the same Remote DOM pipeline. **This guide covers the [CLI](/docs/reference/cli-reference) path.** For Studio, see [AI Extension Studio](/docs/reference/extension-studio).
1815
+
1580
1816
  ## Prerequisites
1581
1817
 
1582
1818
  - **Node.js** 22 or later
@@ -1715,21 +1951,11 @@ export const Content = () => {
1715
1951
  }
1716
1952
  \`\`\`
1717
1953
 
1718
- ## 6. Validate & Deploy
1719
-
1720
- Before deploying, validate your extension:
1721
-
1722
- \`\`\`bash
1723
- ${CLI.validate}
1724
- \`\`\`
1725
-
1726
- This checks manifest structure, permission usage, surface targets, and import patterns.
1954
+ ## 6. Submit for review
1727
1955
 
1728
- When ready, deploy:
1956
+ When your extension is ready, open it in the [Stackable admin dashboard](https://admin.stackablelabs.com), fill in the marketplace listing (icon, screenshots, description, tagline, support links), and submit for review. Most reviews complete within a few business days.
1729
1957
 
1730
- \`\`\`bash
1731
- ${CLI.deploy}
1732
- \`\`\`
1958
+ > *CLI \`validate\` and \`deploy\` commands are on the roadmap (see [CLI Reference](/docs/reference/cli-reference#validate-coming-soon)). Today, manifests are validated server-side during submission and via the \`validate_manifest\` MCP tool.*
1733
1959
 
1734
1960
  ## Next Steps
1735
1961
 
@@ -1748,7 +1974,7 @@ var generateCliReference = () => {
1748
1974
  description: "CLI commands for creating, developing, and deploying Stackable extensions",
1749
1975
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
1750
1976
  });
1751
- const templateRows = TEMPLATE_FLAVORS.map((t) => `| \`${t.name}\` | ${t.description} |`).join("\n");
1977
+ const templateRows = TEMPLATE_FLAVOR_DETAILS.map((t) => `| \`${t.name}\` | ${t.description} |`).join("\n");
1752
1978
  return `${fm}
1753
1979
 
1754
1980
  # CLI Reference
@@ -1767,7 +1993,7 @@ ${CLI.create()}
1767
1993
 
1768
1994
  | Argument | Description |
1769
1995
  |----------|-------------|
1770
- | \`project-name\` | Directory name for the new project |
1996
+ | \`extension-name\` | Name for your extension (saved as the manifest's \`name\`; kebab-cased to derive the directory and extension ID) |
1771
1997
 
1772
1998
  **Options:**
1773
1999
 
@@ -1828,9 +2054,11 @@ Append this to your deployed host app URL to load your local extension instead
1828
2054
  of the production bundle. The override is browser-session only \u2014 no DB changes,
1829
2055
  no shared state. Each developer gets isolated overrides.
1830
2056
 
1831
- ## validate
2057
+ ## validate *(coming soon)*
1832
2058
 
1833
- Check your extension for common errors before deploying:
2059
+ > *NOTE: manifests are validated server-side during the marketplace submission flow, in the AI Studio, or can be validated using the \`validate_manifest\` MCP tool \u2014 see [AI-Accelerated Development](/docs/reference/ai-accelerated-development#live-mcp-server).*
2060
+
2061
+ When shipped, the \`validate\` command will check your extension for common errors locally before submission:
1834
2062
 
1835
2063
  \`\`\`bash
1836
2064
  ${CLI.validate}
@@ -1851,23 +2079,21 @@ ${CLI.validate}
1851
2079
  - \`0\` \u2014 all checks pass
1852
2080
  - \`1\` \u2014 errors found (must fix before deploying)
1853
2081
 
1854
- ## deploy
2082
+ ## deploy *(future)*
2083
+
2084
+ > *This command is on the roadmap. Currently build, deployment and hosting are the responsibility of the extension developer with your hosted Bundle URL being updated via the admin dashboard or via CLI.*
1855
2085
 
1856
- Package and deploy the extension:
2086
+ When shipped, the \`deploy\` command will package and ship your extension straight from the terminal:
1857
2087
 
1858
2088
  \`\`\`bash
1859
2089
  ${CLI.deploy}
1860
2090
  \`\`\`
1861
2091
 
1862
- **What it does:**
1863
- 1. Runs validation checks (same as validate)
1864
- 2. Builds the extension for production
1865
- 3. Packages the build output
1866
- 4. Uploads to the Stackable extension registry
1867
-
1868
- **Prerequisites:**
1869
- - All validation checks must pass
1870
- - You must be authenticated (follow the prompts if not)
2092
+ **Planned behavior:**
2093
+ 1. Run validation checks (same as \`validate\`)
2094
+ 2. Build the extension for production
2095
+ 3. Package the build output
2096
+ 4. Upload to the Stackable extension registry
1871
2097
 
1872
2098
  ## scaffold
1873
2099
 
@@ -1904,6 +2130,62 @@ ${CLI.update} [extensionId]
1904
2130
  | \`--bundle-url <url>\` | New bundle URL |
1905
2131
  | \`--enabled <bool>\` | Enable/disable extension |
1906
2132
  | \`--dir <path>\` | Project root (default: cwd) |
2133
+
2134
+ ## auth
2135
+
2136
+ Manage CLI authentication. Subcommands open a browser-based OAuth flow against the Stackable platform and store the resulting credentials locally.
2137
+
2138
+ **Subcommands:**
2139
+
2140
+ | Subcommand | Description |
2141
+ |------------|-------------|
2142
+ | \`login\` | Authenticate with Stackable via browser-based OAuth |
2143
+ | \`logout\` | Clear stored CLI credentials |
2144
+ | \`status\` | Show current authentication status (signed-in user, org, token expiry) |
2145
+
2146
+ **Examples:**
2147
+
2148
+ \`\`\`bash
2149
+ # First-time setup
2150
+ ${CLI.auth.login}
2151
+
2152
+ # Check who's signed in
2153
+ ${CLI.auth.status}
2154
+
2155
+ # Sign out
2156
+ ${CLI.auth.logout}
2157
+ \`\`\`
2158
+
2159
+ ## ai
2160
+
2161
+ Manage AI editor configuration in your Extension project. Subcommands write Stackable's [AI tooling](/docs/reference/ai-accelerated-development) (Skills + MCP server config) into your project.
2162
+
2163
+ **Subcommands:**
2164
+
2165
+ | Subcommand | Description |
2166
+ |------------|-------------|
2167
+ | \`scaffold\` | Download Stackable Skills into your project's \`.claude/\`, \`.cursor/\`, \`.windsurf/\`, etc. |
2168
+ | \`mcp\` | Add Stackable MCP server config to your project so any MCP-compatible AI client can connect |
2169
+
2170
+ **Options:**
2171
+
2172
+ | Flag | Description | Applies to |
2173
+ |------|-------------|------------|
2174
+ | \`--version <version>\` | AI docs version (semver or \`latest\`, default: \`latest\`) | both |
2175
+ | \`--dir <path>\` | Project root directory (default: cwd) | \`mcp\` only |
2176
+
2177
+ **Examples:**
2178
+
2179
+ \`\`\`bash
2180
+ # Add the latest Stackable Skills to your project
2181
+ ${CLI.ai.scaffold}
2182
+
2183
+ # Pin a specific version
2184
+ ${CLI.ai.scaffold} --version 0.2.0
2185
+
2186
+ # Configure your project to talk to the Stackable MCP server
2187
+ ${CLI.ai.mcp}
2188
+ \`\`\`
1907
2189
  `;
1908
2190
  };
1909
2191
 
@@ -2062,29 +2344,29 @@ var generateExtensionStudio = () => {
2062
2344
  const fm = frontmatter({
2063
2345
  root: false,
2064
2346
  targets: ["*"],
2065
- description: "Extension Studio: in-browser builder with AI assistant, component palette, and live preview",
2347
+ description: "AI Extension Studio: in-browser builder with AI assistant, component palette, and live preview",
2066
2348
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
2067
2349
  });
2068
2350
  return `${fm}
2069
2351
 
2070
- # Extension Studio
2352
+ # AI Extension Studio
2071
2353
 
2072
- Extension Studio is an in-browser development environment for building Stackable
2354
+ Stackable's AI Extension Studio is an in-browser development environment for building Stackable
2073
2355
  extensions without local tooling. It runs inside the admin dashboard and provides
2074
2356
  a code editor, live preview, component palette, and an AI assistant \u2014 everything
2075
2357
  needed to create, iterate on, and deploy extensions from the browser.
2076
2358
 
2077
2359
  ## Layout
2078
2360
 
2079
- Studio is organized as a 3-pane workspace:
2361
+ "Studio" is organized as a 3-pane workspace:
2080
2362
 
2081
2363
  | Pane | Position | Purpose |
2082
2364
  |------|----------|---------|
2083
2365
  | **Shelf** | Left (collapsible) | Component palette, surface picker, capability list |
2084
- | **Stage** | Center | Code editor (CodeMirror) or live preview \u2014 toggle between modes |
2366
+ | **Stage** | Center | Code editor or live preview \u2014 toggle between modes |
2085
2367
  | **Sidekick** | Right (collapsible, resizable) | AI chat assistant for code generation and SDK guidance |
2086
2368
 
2087
- ## The Shelf
2369
+ ## The "Shelf"
2088
2370
 
2089
2371
  The Shelf is a collapsible sidebar with three sections:
2090
2372
 
@@ -2109,7 +2391,7 @@ The SDK capabilities your extension can use: \`data.query\`, \`data.fetch\`,
2109
2391
  \`events:identity\`, \`events:messaging\`, and \`events:activity\`. Clicking a
2110
2392
  capability adds the permission to your manifest and AI-inserts the hook usage.
2111
2393
 
2112
- ## The Stage
2394
+ ## The "Stage"
2113
2395
 
2114
2396
  The center pane switches between two modes:
2115
2397
 
@@ -2124,7 +2406,7 @@ Changes in Code mode recompile automatically via in-browser esbuild and update
2124
2406
  the preview. The toolbar shows the current status: Ready, Saving, Compiling,
2125
2407
  Thinking (AI), or Error.
2126
2408
 
2127
- ## The Sidekick
2409
+ ## The "Sidekick"
2128
2410
 
2129
2411
  The Sidekick is an AI chat assistant that understands the Stackable Extension SDK.
2130
2412
  It can:
@@ -2164,18 +2446,114 @@ Studio toolbar.
2164
2446
 
2165
2447
  ## Studio vs CLI
2166
2448
 
2167
- | | Studio | CLI |
2449
+ Comparing approaches? See [Choosing your path](/docs/guides/quick-start#choosing-your-path) in the Quick Start guide for the full Studio vs CLI comparison.
2450
+
2451
+ Both workflows produce the same output \u2014 a Stackable extension that runs in the host application via the same Remote DOM pipeline. Start in Studio to prototype, then scaffold to CLI when you need the full development workflow.
2452
+ `;
2453
+ };
2454
+
2455
+ // ../../sdk/extension/ai-docs/src/generators/ai-accelerated-development.ts
2456
+ var generateAIAcceleratedDevelopment = () => {
2457
+ const fm = frontmatter({
2458
+ root: false,
2459
+ targets: ["*"],
2460
+ description: "AI-Accelerated Development: AI Extension Studio, Agent Skills, the live MCP server, and the Claude Code plugin \u2014 and how to choose between them.",
2461
+ globs: ["*"]
2462
+ });
2463
+ return `${fm}
2464
+
2465
+ # AI-Accelerated Development
2466
+
2467
+ Stackable's AI-native messaging experience platform makes building extensions really simple - and incredibly fast. Four complementary surfaces (Studio, Agent Skills, MCP Server, and Claude Code Plugin) take you from possibility to production in minutes.
2468
+
2469
+ | Surface | Best for | Install |
2168
2470
  |---|---|---|
2169
- | **Best for** | Prototyping, learning, quick iterations | Production extensions, team workflows |
2170
- | **Code structure** | Single file | Multi-file (surfaces/, components/, lib/) |
2171
- | **Preview** | Built-in live preview | Local dev server + Cloudflare tunnel |
2172
- | **AI assistance** | Sidekick chat + smart insertion | Your own AI editor (with SDK skills) |
2173
- | **Version control** | Auto-saved to cloud | Git-based |
2174
- | **Deployment** | Link to extension + deploy | CLI deploy command |
2471
+ | **AI Extension Studio** | First extensions, prototyping, in-browser builds | Open the admin dashboard |
2472
+ | **Agent Skills** | Any AI coding assistant | \`pnpm dlx skills add stackable-labs/skills\` (auto-bundled in scaffolds) |
2473
+ | **Live MCP Server** | AI clients that speak MCP | Add to your client's MCP config |
2474
+ | **Claude Code Plugin** | Claude Code users (bundles Skills + MCP) | \`/plugin marketplace add stackable-labs/claude-plugins\` |
2475
+
2476
+ ---
2477
+
2478
+ ## AI Extension Studio
2479
+
2480
+ AI Extension Studio is more than just another AI App builder ([Lovable](https://lovable.dev), [v0](https://v0.dev), [Bolt](https://bolt.new), [Replit](https://replit.com), etc.). It's an in-browser companion trained specifically to conceptualize and build experiences for the Stackable framework that are secure, look great, and are **production ready in minutes!**
2481
+
2482
+ The 3-pane workspace (component palette, live preview, agentic chat) takes you from idea to shipped code in moments. The agentic chat ("Sidekick") writes and modifies code, updates your manifest, looks up SDK reference on demand, and suggests next steps as you work. No installs, no Node version to manage, no tunnel \u2014 open the admin dashboard and you're building.
2483
+
2484
+ **Best for:** first extensions, prototyping, non-developers, and quick experiments.
2485
+
2486
+ **When to move to the CLI:** Studio produces a simple single-file project. When you need multi-file structure, custom dependencies, your own build pipeline, or git-based version control, you can transition seamlessly. Either download your Studio project as a ZIP from the export menu, or pull it directly via the CLI. Either path produces a fully-structured local project ready for production build and deployment.
2487
+
2488
+ For full Studio details, see [AI Extension Studio](/docs/reference/extension-studio).
2489
+
2490
+ ## AI Agentic Skills
2175
2491
 
2176
- Both workflows produce the same output \u2014 a Stackable extension that runs in the
2177
- host application via the same Remote DOM pipeline. Start in Studio to prototype,
2178
- then scaffold to CLI when you need the full development workflow.
2492
+ [Agent Skills](https://agentskills.io) is an open standard for publishing machine-readable knowledge that AI coding assistants can pull on demand instead of crawling your docs. Stackable publishes the core SDK concepts (surfaces, capabilities, components, hooks, patterns, recipes, and more) as Agent Skills, so any compatible assistant has just-in-time access to exactly the surface area it needs.
2493
+
2494
+ ### How to install
2495
+
2496
+ - **Bundled automatically when you scaffold** \u2014 \`@stackable-labs/cli-app-extension create\` writes the Stackable Skills into your project's \`.claude/\`, \`.cursor/\`, \`.windsurf/\`, and equivalent directories. No extra setup.
2497
+ - **Standalone install** \u2014 for an existing project, or to update to the latest skills, run:
2498
+ \`\`\`bash
2499
+ pnpm dlx skills add stackable-labs/skills
2500
+ \`\`\`
2501
+ This is powered by [Vercel's open \`skills\` CLI](https://github.com/vercel-labs/skills).
2502
+
2503
+ ### Compatible assistants
2504
+
2505
+ Agent Skills work with any tool that supports the format \u2014 Claude Code, Cursor, Windsurf, Codex, VS Code (with MCP-aware extensions), Continue, Zed, and 40+ others listed on agentskills.io. Stackable's Claude Code plugin (below) bundles + auto-updates these skills for Claude Code users.
2506
+
2507
+ ## Live MCP Server
2508
+
2509
+ The Stackable platform exposes a hosted MCP (Model Context Protocol) server that any compatible AI client can connect to for live access to your account's apps, extensions, and platform metadata.
2510
+
2511
+ **Endpoint:** \`https://api-use1.stackablelabs.io/mcp/app-extension\`
2512
+
2513
+ ### Available tools (snapshot)
2514
+
2515
+ | Tool | What it does |
2516
+ |---|---|
2517
+ | \`list_apps\` | List apps in your account |
2518
+ | \`list_extensions\` | List extensions for a given app |
2519
+ | \`list_instances\` | List deployed instances of an extension |
2520
+ | \`list_skills\` | List available SDK skills |
2521
+ | \`lookup_skill\` | Fetch a specific SDK skill's content on demand |
2522
+ | \`get_extension\` | Fetch full details for a single extension |
2523
+ | \`validate_manifest\` | Validate an extension manifest against the platform |
2524
+ | \`validate_permissions\` | Validate manifest permissions |
2525
+
2526
+ ### Compatible clients
2527
+
2528
+ [Claude Desktop](https://claude.ai/download), [VS Code](https://code.visualstudio.com), [Cursor](https://cursor.com), [Codex](https://developers.openai.com/codex), [Antigravity](https://antigravity.google), [Windsurf](https://windsurf.com), and any other MCP-compatible AI tool. Each client has its own way to register MCP servers \u2014 see your client's docs for the exact config syntax.
2529
+
2530
+ Authentication uses standard OAuth2 \u2014 your client will prompt you to sign in on first use.
2531
+
2532
+ ## Claude Code Plugin
2533
+
2534
+ For Claude Code users, the Stackable Claude Code plugin bundles the Skills and pre-configures the MCP server in one install.
2535
+
2536
+ **Install:**
2537
+ \`\`\`bash
2538
+ # Add marketplace
2539
+ /plugin marketplace add stackable-labs/claude-plugins
2540
+
2541
+ # Install plugin
2542
+ /plugin install stackable-extension-dev@stackable-claude-plugins
2543
+ \`\`\`
2544
+
2545
+ **What it includes:**
2546
+ - Latest Stackable Agent Skills (auto-updated)
2547
+ - The Stackable MCP server pre-configured
2548
+ - Slash commands and workflows tailored for extension development
2549
+
2550
+ After install, Claude Code can scaffold, validate, and inspect your Stackable extensions directly from your editor without any extra config.
2551
+
2552
+ ## Choosing your tools
2553
+
2554
+ - **Just starting?** Open [AI Extension Studio](/docs/reference/extension-studio) in the admin dashboard. No installs, no decisions \u2014 get started on your first extension right in the browser.
2555
+ - **Want more control, or preparing for release?** Use the [CLI](/docs/guides/quick-start) for a full local TypeScript project: multi-file structure, your own dependencies, git-based version control, and your own build pipeline.
2556
+ - **Building from scratch with your own AI workflow?** Wire up your editor with [Agent Skills](#ai-agentic-skills), the [Live MCP Server](#live-mcp-server), or both \u2014 or use the [Claude Code Plugin](#claude-code-plugin) for Claude Code users, which auto-bundles Skills and pre-configures the MCP server in one install.
2179
2557
  `;
2180
2558
  };
2181
2559
  var generateProjectStructure = () => {
@@ -3014,12 +3392,23 @@ var generateValidateExtensionCommand = () => {
3014
3392
  description: "Validate this extension for common errors before deploying (manifest, permissions, surfaces, imports)",
3015
3393
  targets: ["*"]
3016
3394
  });
3395
+ const eventHookBullets = Object.keys(EVENT_HOOK_PERMISSION_MAP).map((hook) => `- \`${hook}\` \u2192 needs \`${EVENT_HOOK_PERMISSION_MAP[hook]}\` permission (also check manifest \`events\` array has matching entries)`).join("\n");
3017
3396
  return `${fm}
3018
3397
 
3019
3398
  # Validate Extension
3020
3399
 
3021
3400
  Check this extension for common errors before deploying. Run through each check
3022
- and report all issues found.
3401
+ and ensure no issues found before you submit.
3402
+
3403
+ > **Tip \u2014 let the platform validate for you.** The hosted Stackable MCP server
3404
+ > exposes a \`validate_manifest\` tool that runs the same server-side checks the
3405
+ > marketplace submission flow uses. If your AI client is connected to the
3406
+ > Stackable MCP server (see [AI-Accelerated Development](/docs/reference/ai-accelerated-development#live-mcp-server)),
3407
+ > just ask it to validate \`packages/extension/public/manifest.json\` for you \u2014
3408
+ > it returns structured errors and warnings without you having to walk the
3409
+ > checklist below by hand. The manual steps remain useful for code-level checks
3410
+ > the manifest validator can't see (permission usage, surface wiring, sandbox
3411
+ > compliance).
3023
3412
 
3024
3413
  ## 1. Manifest validation
3025
3414
  Read \`packages/extension/public/manifest.json\` and verify:
@@ -3039,9 +3428,7 @@ Scan all \`.tsx\` files in \`packages/extension/src/\` for capability usage:
3039
3428
  - \`capabilities.actions.toast\` \u2192 needs \`actions:toast\` permission
3040
3429
  - \`capabilities.actions.invoke\` \u2192 needs \`actions:invoke\` permission
3041
3430
  - \`useExtendIdentity\` \u2192 needs \`extend:identity\` permission
3042
- - \`useIdentityEvent\` \u2192 needs \`events:identity\` permission (also check manifest \`events\` array has matching entries)
3043
- - \`useMessagingEvent\` \u2192 needs \`events:messaging\` permission (also check manifest \`events\` array has matching entries)
3044
- - \`useActivityEvent\` \u2192 needs \`events:activity\` permission (also check manifest \`events\` array has matching entries)
3431
+ ${eventHookBullets}
3045
3432
 
3046
3433
  Report:
3047
3434
  - **Missing permissions:** capabilities used in code but not declared in manifest
@@ -3061,6 +3448,202 @@ Verify that:
3061
3448
  ## 5. Summary
3062
3449
  Print a summary: total issues found, categorized by severity (error vs warning).
3063
3450
  Errors must be fixed before deploying. Warnings are recommendations.
3451
+
3452
+ ## Next: deploy
3453
+
3454
+ Once validation passes, build the production bundle, host it, and register the
3455
+ Bundle URL with Stackable. See the [Deploy guide](/docs/guides/deploy).
3456
+ `;
3457
+ };
3458
+
3459
+ // ../../sdk/extension/ai-docs/src/commands/deploy-extension.ts
3460
+ var generateDeployExtensionCommand = () => {
3461
+ const fm = frontmatter({
3462
+ description: "Build, host, and register the production Bundle URL for this Stackable extension",
3463
+ targets: ["*"]
3464
+ });
3465
+ return `${fm}
3466
+
3467
+ # Deploy
3468
+
3469
+ Stackable hosts the marketplace, the proxy, and the runtime \u2014 but **you host
3470
+ your extension's bundle/runtime**. Deployment is three steps: build for production,
3471
+ upload to a host of your choice, then point your extension's **Bundle URL** at
3472
+ the result.
3473
+
3474
+ ## 1. Build the production bundle
3475
+
3476
+ From the project root:
3477
+
3478
+ \`\`\`bash
3479
+ pnpm build
3480
+ \`\`\`
3481
+
3482
+ This runs Vite in production mode and writes the static bundle to
3483
+ \`packages/extension/dist/\` \u2014 a small set of JS/CSS files plus your
3484
+ \`manifest.json\`. That's the artifact you ship.
3485
+
3486
+ > **Tip:** Validate the build before you upload it. Run through the
3487
+ > [Validate guide](/docs/guides/validate) (or ask your AI client to call the
3488
+ > \`validate_manifest\` MCP tool) so manifest typos and missing permissions are
3489
+ > caught before they reach the marketplace.
3490
+
3491
+ ## 2. Upload to a hosting provider
3492
+
3493
+ Stackable doesn't care where the bundle lives \u2014 only that it's served over
3494
+ HTTPS with permissive CORS so the host runtime can fetch it. Anything that
3495
+ serves static files works. The most common options:
3496
+
3497
+ | Provider | Notes |
3498
+ |----------|-------|
3499
+ | **Netlify** | Drop \`packages/extension/dist/\` into a site, or wire up Git auto-deploys. CORS is already permissive. |
3500
+ | **Vercel** | Same idea \u2014 link the repo and let Vercel build/deploy on push. Set the build output to \`packages/extension/dist\`. |
3501
+ | **Cloudflare Pages** | Connect the repo, set the output directory, deploys land on the edge globally. |
3502
+ | **AWS S3 + CloudFront** | Sync \`dist/\` to an S3 bucket, front it with a CloudFront distribution. Make sure the CloudFront response headers include \`Access-Control-Allow-Origin: *\` (or your host origin). |
3503
+ | **Your own CDN / object storage** | Anything that returns \`200 OK\` with the right \`Content-Type\` and CORS headers will work. |
3504
+
3505
+ Whichever you pick, the only requirements are:
3506
+
3507
+ - Bundle is reachable over **HTTPS**
3508
+ - CORS allows the host application's origin (most providers do this by default; S3+CloudFront is the one that usually needs explicit configuration)
3509
+ - The URL is **stable** \u2014 every Instance running your extension fetches from this URL on load
3510
+
3511
+ > **Need help with setup or deployment?** Reach out at [developers@stackablelabs.com](mailto:developers@stackablelabs.com) or open an issue / discussion on [GitHub](https://github.com/stackable-labs).
3512
+
3513
+ ## 3. Register your Bundle URL with Stackable
3514
+
3515
+ Once your extension bundle is deployed/live, tell Stackable where to find it. You have two options:
3516
+
3517
+ - **Admin dashboard** \u2014 open your extension at [admin.stackablelabs.com](https://admin.stackablelabs.com), edit the **Bundle URL** field, and save. Existing Instances pick up the new bundle the next time they load.
3518
+ - **CLI** \u2014 from the project directory:
3519
+
3520
+ \`\`\`bash
3521
+ ${CLI.update} --bundle-url https://your-host.example.com/manifest.json
3522
+ \`\`\`
3523
+
3524
+ Or, with an explicit extension ID (handy for CI/CD):
3525
+
3526
+ \`\`\`bash
3527
+ ${CLI.update} ext_xxx --bundle-url https://your-host.example.com/manifest.json
3528
+ \`\`\`
3529
+
3530
+ Point the URL at your bundle's \`manifest.json\` \u2014 that's the entrypoint the
3531
+ runtime fetches first; everything else is loaded relative to it.
3532
+
3533
+ ## 4. Roll out and iterate
3534
+
3535
+ That's the entire deploy loop. Subsequent updates follow the same path: bump
3536
+ \`version\` in \`manifest.json\`, run \`pnpm build\`, re-upload, and (only if the
3537
+ URL changed) re-register. Most teams wire steps 1\u20133 into a single CI/CD job
3538
+ that runs on every merge to \`main\`.
3539
+
3540
+ ## Next: list it on the Marketplace
3541
+
3542
+ Deploying makes your extension **runnable** at a stable URL. To make it
3543
+ **installable** by other organizations, you also need to fill in the
3544
+ marketplace listing (icon, screenshots, description, visibility) and submit
3545
+ for review. See [Marketplace Listing](/docs/reference/marketplace-listing).
3546
+ `;
3547
+ };
3548
+
3549
+ // ../../sdk/extension/ai-docs/src/commands/marketplace-listing.ts
3550
+ var generateMarketplaceListing = () => {
3551
+ const fm = frontmatter({
3552
+ description: "Fill in your marketplace listing, choose visibility, and submit your Stackable extension for review and approval",
3553
+ targets: ["*"]
3554
+ });
3555
+ return `${fm}
3556
+
3557
+ # Listing & Submission
3558
+
3559
+ Once your extension is built, hosted, and registered with a Bundle URL (see
3560
+ [Deploy](/docs/guides/deploy)), the last step is to make it discoverable and
3561
+ installable. That happens entirely in the [admin dashboard](https://admin.stackablelabs.com)
3562
+ \u2014 you fill in your marketplace listing, pick a visibility mode, and submit for
3563
+ review.
3564
+
3565
+ ## 1. Fill in the listing fields
3566
+
3567
+ Open your extension in the admin dashboard and complete the **Listing** tab.
3568
+ Every field below is part of the marketplace metadata that prospective
3569
+ installers see when browsing or evaluating your extension.
3570
+
3571
+ | Field | Required | What it's for |
3572
+ |-------|----------|---------------|
3573
+ | **Icon** | Yes | Square image rendered on browse cards and the detail page. Square aspect ratio, transparent background recommended. |
3574
+ | **Short description** | Yes | One-line summary used on browse cards and search results. Keep it under ~120 characters. |
3575
+ | **Long description** | Yes | Markdown body for the extension detail page. Cover what the extension does, the problem it solves, and any setup requirements. Screenshots and links allowed. |
3576
+ | **Screenshots** | Recommended | Image gallery shown on the detail page. Pick views that demonstrate the extension actually working in a host application. |
3577
+ | **Categories** | Yes | One or more curated categories from the platform taxonomy (e.g., commerce, support, analytics). Drives marketplace browse filters. |
3578
+ | **Tags** | Optional | Freeform searchable tags. Used for keyword search across the marketplace. |
3579
+ | **Publisher** | Auto | Pulled from your organization profile \u2014 the org name shown as the listing's publisher. Update your org profile in the dashboard if this needs to change. |
3580
+
3581
+ > **Tip:** The icon and screenshots are what make a listing feel real. A
3582
+ > placeholder icon and no screenshots is the single biggest reason listings
3583
+ > get a "needs more info" review response.
3584
+
3585
+ ## 2. Choose a visibility mode
3586
+
3587
+ Every listing has a **visibility** setting that controls who can install it:
3588
+
3589
+ | Visibility | Who can find and install it |
3590
+ |------------|------------------------------|
3591
+ | **Public** | Anyone browsing the marketplace. Goes through full marketplace review. Best for extensions you want available to everyone. |
3592
+ | **Protected** | Only organizations you explicitly add to the **allowed orgs** list. The listing is hidden from public browse; people you've allowed see it in their marketplace as if it were public. Best for private/internal extensions, paid customers, or staged rollouts. |
3593
+
3594
+ Visibility can be changed later from the same Listing tab. Switching from
3595
+ **Protected** \u2192 **Public** triggers a fresh marketplace review.
3596
+
3597
+ ## 3. Submit for review
3598
+
3599
+ Click **Submit for review**. The platform runs a two-stage review:
3600
+
3601
+ ### Stage 1 \u2014 Automated Bundle scan
3602
+
3603
+ Runs immediately on submit. The platform fetches your bundle from the
3604
+ registered Bundle URL and runs deterministic checks:
3605
+
3606
+ - Bundle reachable, valid \`manifest.json\`, sane bundle size
3607
+ - Declared permissions match observed capability usage
3608
+ - Declared targets match the surfaces your code actually registers
3609
+ - Declared \`allowedDomains\` match the endpoints \`data.fetch\` actually hits
3610
+ - No use of forbidden browser globals (\`document\`, \`window.location\`, etc.)
3611
+
3612
+ Findings come back categorized as **error**, **warning**, or **info**. Errors
3613
+ must be fixed before the listing can move forward. Warnings don't block
3614
+ submission but factor into the review.
3615
+
3616
+ ### Stage 2 \u2014 Advanced review + Stackable approval
3617
+
3618
+ If Stage 1 passes, the platform runs an advanced review of your bundle \u2014
3619
+ framework analysis, permission surface, network behavior, and any
3620
+ security-relevant findings get scored and assessed against your listing
3621
+ content. From there, Stackable will either:
3622
+
3623
+ - **Approve** \u2014 your listing moves to **Published** and becomes installable.
3624
+ - **Request changes** \u2014 you'll see structured feedback on the listing page. Address it, re-deploy if needed, and resubmit.
3625
+
3626
+ Most reviews complete within a few business days. You'll get an email when
3627
+ the status changes; the listing's review status is also visible in the admin
3628
+ dashboard at all times.
3629
+
3630
+ ## Review status lifecycle
3631
+
3632
+ | Status | Meaning |
3633
+ |--------|---------|
3634
+ | **Draft** | Listing is being edited; not submitted. |
3635
+ | **Pending review** | Submitted; waiting on Stage 1 + Stage 2 + Stackable approval. |
3636
+ | **Rejected** | Stackable requested changes. Address the feedback and resubmit. |
3637
+ | **Published** | Live in the marketplace. Installable per your visibility setting. |
3638
+ | **Suspended** | Pulled from the marketplace by Stackable (security issue, ToS violation, etc.). Contact support. |
3639
+
3640
+ ## After publishing
3641
+
3642
+ Re-deploys to the same Bundle URL don't require re-submission \u2014 your existing
3643
+ Instances pick up the new bundle automatically. **Listing edits** (description,
3644
+ screenshots, categories, visibility) and **major version bumps** do go through
3645
+ review again. Patch updates that don't change permissions, targets, or
3646
+ \`allowedDomains\` typically clear review quickly.
3064
3647
  `;
3065
3648
  };
3066
3649
 
@@ -3154,6 +3737,12 @@ var SKILLS = [
3154
3737
  type: "knowledge",
3155
3738
  content: () => generateStylingAndTheming()
3156
3739
  },
3740
+ {
3741
+ id: "instance-settings",
3742
+ description: "Instance Settings: declaring settingsSchema in your extension manifest, the install-time admin form, and reading regular values via useSettings() vs secure values via {{settings.x}} placeholders in data.fetch. Use when adding per-Instance configuration to an extension.",
3743
+ type: "knowledge",
3744
+ content: () => generateInstanceSettings()
3745
+ },
3157
3746
  // ── Docs-only knowledge skills ──────────────────────────────
3158
3747
  {
3159
3748
  id: "quick-start",
@@ -3183,6 +3772,13 @@ var SKILLS = [
3183
3772
  scopes: ["docs"],
3184
3773
  content: () => generateExtensionStudio()
3185
3774
  },
3775
+ {
3776
+ id: "ai-accelerated-development",
3777
+ description: "AI-Accelerated Development: AI Extension Studio, Agent Skills, the live MCP server, and the Claude Code plugin \u2014 and how to choose between them.",
3778
+ type: "knowledge",
3779
+ scopes: ["docs"],
3780
+ content: () => generateAIAcceleratedDevelopment()
3781
+ },
3186
3782
  // ── Cookbook skills (docs-only) ────────────────────────────────
3187
3783
  {
3188
3784
  id: "cookbook-structural",
@@ -3205,6 +3801,13 @@ var SKILLS = [
3205
3801
  scopes: ["docs"],
3206
3802
  content: () => generateCookbookEvents()
3207
3803
  },
3804
+ {
3805
+ id: "marketplace-listing",
3806
+ description: "Marketplace listing reference: the listing fields (icon, screenshots, description, categories), Public vs Protected visibility, and the two-stage review process (bundle scan + security review + approval). Use when publishing a deployed extension to the marketplace.",
3807
+ type: "knowledge",
3808
+ scopes: ["docs"],
3809
+ content: () => generateMarketplaceListing()
3810
+ },
3208
3811
  // ── Action skills ─────────────────────────────────────────────
3209
3812
  {
3210
3813
  id: "add-surface",
@@ -3229,6 +3832,15 @@ var SKILLS = [
3229
3832
  description: "Validate this extension for common errors before deploying: manifest, permissions, surfaces, imports. Use before publishing or when debugging issues.",
3230
3833
  type: "action",
3231
3834
  content: () => generateValidateExtensionCommand()
3835
+ },
3836
+ {
3837
+ id: "deploy",
3838
+ description: "Build the production bundle, host it on Netlify / Vercel / Cloudflare Pages / S3+CloudFront, and register your Bundle URL with Stackable via the admin dashboard or CLI. Use when shipping an extension after validation passes.",
3839
+ type: "action",
3840
+ // TODO: T3 #24 — remove `scopes: ['docs']` once `cli deploy` is implemented so Studio's
3841
+ // AI assistant in the future should be able invoke the deploy flow (not just describe it).
3842
+ scopes: ["docs"],
3843
+ content: () => generateDeployExtensionCommand()
3232
3844
  }
3233
3845
  ];
3234
3846
 
@@ -3830,6 +4442,108 @@ pair(EXAMPLE_SNIPPETS, EXAMPLE_SNIPPETS2);
3830
4442
  pair(CAPABILITY_SNIPPETS, CAPABILITY_SNIPPETS2);
3831
4443
  pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
3832
4444
 
4445
+ // ../../lib/contracts/src/custom.ts
4446
+ var CUSTOM_ROLE = {
4447
+ SUPER_ADMIN: "super_admin"
4448
+ };
4449
+ new Set(Object.values(CUSTOM_ROLE));
4450
+
4451
+ // ../../lib/utils-js/src/crypto.ts
4452
+ var getCrypto = () => {
4453
+ if (typeof globalThis !== "undefined" && globalThis.crypto) {
4454
+ return globalThis.crypto;
4455
+ }
4456
+ throw new Error("Web Crypto API not available \u2014 requires Node.js 22+ or a modern browser");
4457
+ };
4458
+ var getSubtle = () => {
4459
+ const crypto = getCrypto();
4460
+ if (crypto.subtle) {
4461
+ return crypto.subtle;
4462
+ }
4463
+ throw new Error("SubtleCrypto not available \u2014 requires a secure context (HTTPS) or Node.js 22+");
4464
+ };
4465
+ var encodeBytes = (bytes, encoding) => {
4466
+ if (encoding === "hex") {
4467
+ return [...bytes].map((b2) => b2.toString(16).padStart(2, "0")).join("");
4468
+ }
4469
+ const base64 = btoa(String.fromCharCode(...bytes));
4470
+ if (encoding === "base64url") {
4471
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4472
+ }
4473
+ return base64;
4474
+ };
4475
+ var getRandomBytes = (length, encoding = "base64") => {
4476
+ const bytes = new Uint8Array(length);
4477
+ getCrypto().getRandomValues(bytes);
4478
+ return encodeBytes(bytes, encoding);
4479
+ };
4480
+ var getDigest = async (input, encoding = "hex") => {
4481
+ const digest = await getSubtle().digest(
4482
+ "SHA-256",
4483
+ new TextEncoder().encode(input)
4484
+ );
4485
+ return encodeBytes(new Uint8Array(digest), encoding);
4486
+ };
4487
+ var getNonce = () => getRandomBytes(16, "base64url");
4488
+ var getVerifier = () => getRandomBytes(32, "base64url");
4489
+
4490
+ // ../../lib/utils-auth/src/constants.ts
4491
+ var STANDALONE_CLIENT_DATA = {
4492
+ CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
4493
+ MCP: { name: "@stackable-labs/mcp-app-extension", authFile: "mcp-auth.json" }
4494
+ };
4495
+ var STANDALONE_CLIENT = Object.fromEntries(
4496
+ Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.name])
4497
+ );
4498
+ var STANDALONE_CLIENT_AUTH_FILE = Object.fromEntries(
4499
+ Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.authFile])
4500
+ );
4501
+ Object.values(STANDALONE_CLIENT);
4502
+
4503
+ // ../../lib/utils-auth/src/index.ts
4504
+ var deriveClientId = async (clientName) => (await getDigest(clientName)).slice(0, 32);
4505
+ var AUTH_DIR = join(homedir(), ".stackable");
4506
+ join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.CLI);
4507
+ var MCP_AUTH_FILE = join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.MCP);
4508
+ var resolveAuthFile = (filename) => join(AUTH_DIR, filename ?? STANDALONE_CLIENT_AUTH_FILE.CLI);
4509
+ var readAuthState = async (filename) => {
4510
+ try {
4511
+ const content = await readFile(resolveAuthFile(filename), "utf8");
4512
+ return JSON.parse(content);
4513
+ } catch {
4514
+ return null;
4515
+ }
4516
+ };
4517
+ var writeAuthState = async (state, filename) => {
4518
+ await mkdir(AUTH_DIR, { recursive: true, mode: 448 });
4519
+ await writeFile(resolveAuthFile(filename), JSON.stringify(state, null, 2), { mode: 384 });
4520
+ };
4521
+ var decodeJwtPayload = (token) => {
4522
+ try {
4523
+ const [, payload] = token.split(".");
4524
+ if (!payload) {
4525
+ return null;
4526
+ }
4527
+ const json = Buffer.from(payload, "base64url").toString("utf8");
4528
+ return JSON.parse(json);
4529
+ } catch {
4530
+ return null;
4531
+ }
4532
+ };
4533
+ var getToken = async (filename) => {
4534
+ const state = await readAuthState(filename);
4535
+ if (!state) {
4536
+ throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
4537
+ }
4538
+ const payload = decodeJwtPayload(state.token);
4539
+ if (payload?.exp && typeof payload.exp === "number") {
4540
+ if (Date.now() >= payload.exp * 1e3) {
4541
+ throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
4542
+ }
4543
+ }
4544
+ return state.token;
4545
+ };
4546
+
3833
4547
  // package.json
3834
4548
  var package_default = {
3835
4549
  version: "0.0.0"};
@@ -3853,19 +4567,23 @@ var createMcpServer = (options = {}) => {
3853
4567
  return await getToken(STANDALONE_CLIENT_AUTH_FILE.MCP);
3854
4568
  } catch {
3855
4569
  }
3856
- const { performOAuthFlow } = await import('./auth-WHLYYSCQ.js');
4570
+ if (!options.performOAuthFlow) {
4571
+ throw new Error(
4572
+ "No auth token available. Provide authContext (server) or performOAuthFlow (CLI)."
4573
+ );
4574
+ }
3857
4575
  const MCP_API_BASE_URL = process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_API_BASE_URL;
3858
- return performOAuthFlow(`${MCP_API_BASE_URL}/.well-known/oauth-authorization-server`);
4576
+ return options.performOAuthFlow(`${MCP_API_BASE_URL}/.well-known/oauth-authorization-server`);
3859
4577
  };
3860
4578
  const authHeaders = (token) => ({
3861
4579
  authorization: `Bearer ${token}`,
3862
4580
  "content-type": "application/json",
3863
4581
  "x-client-name": MCP_CLIENT_NAME
3864
4582
  });
3865
- const callApi = async (path) => {
4583
+ const callApi = async (path2) => {
3866
4584
  const token = await getAuthToken();
3867
4585
  const ADMIN_API_BASE_URL = process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
3868
- const url = `${ADMIN_API_BASE_URL}${path}`;
4586
+ const url = `${ADMIN_API_BASE_URL}${path2}`;
3869
4587
  let res;
3870
4588
  try {
3871
4589
  res = await fetch(url, { headers: authHeaders(token) });
@@ -3992,12 +4710,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
3992
4710
  usedPermissions.add(permission);
3993
4711
  }
3994
4712
  }
3995
- const eventHookMap = {
3996
- useIdentityEvent: "events:identity",
3997
- useMessagingEvent: "events:messaging",
3998
- useActivityEvent: "events:activity"
3999
- };
4000
- for (const [hook, permission] of Object.entries(eventHookMap)) {
4713
+ for (const [hook, permission] of Object.entries(EVENT_HOOK_PERMISSION_MAP)) {
4001
4714
  if (allSource.includes(hook)) {
4002
4715
  usedPermissions.add(permission);
4003
4716
  }
@@ -4036,8 +4749,623 @@ ${issues.map((i) => `- ${i}`).join("\n")}`);
4036
4749
  }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/instances`)));
4037
4750
  return server2;
4038
4751
  };
4752
+ var isDockerCached;
4753
+ function hasDockerEnv() {
4754
+ try {
4755
+ fs3.statSync("/.dockerenv");
4756
+ return true;
4757
+ } catch {
4758
+ return false;
4759
+ }
4760
+ }
4761
+ function hasDockerCGroup() {
4762
+ try {
4763
+ return fs3.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
4764
+ } catch {
4765
+ return false;
4766
+ }
4767
+ }
4768
+ function isDocker() {
4769
+ if (isDockerCached === void 0) {
4770
+ isDockerCached = hasDockerEnv() || hasDockerCGroup();
4771
+ }
4772
+ return isDockerCached;
4773
+ }
4774
+
4775
+ // ../../../node_modules/.pnpm/is-inside-container@1.0.0/node_modules/is-inside-container/index.js
4776
+ var cachedResult;
4777
+ var hasContainerEnv = () => {
4778
+ try {
4779
+ fs3.statSync("/run/.containerenv");
4780
+ return true;
4781
+ } catch {
4782
+ return false;
4783
+ }
4784
+ };
4785
+ function isInsideContainer() {
4786
+ if (cachedResult === void 0) {
4787
+ cachedResult = hasContainerEnv() || isDocker();
4788
+ }
4789
+ return cachedResult;
4790
+ }
4791
+
4792
+ // ../../../node_modules/.pnpm/is-wsl@3.1.1/node_modules/is-wsl/index.js
4793
+ var isWsl = () => {
4794
+ if (process6.platform !== "linux") {
4795
+ return false;
4796
+ }
4797
+ if (os.release().toLowerCase().includes("microsoft")) {
4798
+ if (isInsideContainer()) {
4799
+ return false;
4800
+ }
4801
+ return true;
4802
+ }
4803
+ try {
4804
+ if (fs3.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
4805
+ return !isInsideContainer();
4806
+ }
4807
+ } catch {
4808
+ }
4809
+ if (fs3.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs3.existsSync("/run/WSL")) {
4810
+ return !isInsideContainer();
4811
+ }
4812
+ return false;
4813
+ };
4814
+ var is_wsl_default = process6.env.__IS_WSL_TEST__ ? isWsl : isWsl();
4815
+
4816
+ // ../../../node_modules/.pnpm/wsl-utils@0.1.0/node_modules/wsl-utils/index.js
4817
+ var wslDrivesMountPoint = /* @__PURE__ */ (() => {
4818
+ const defaultMountPoint = "/mnt/";
4819
+ let mountPoint;
4820
+ return async function() {
4821
+ if (mountPoint) {
4822
+ return mountPoint;
4823
+ }
4824
+ const configFilePath = "/etc/wsl.conf";
4825
+ let isConfigFileExists = false;
4826
+ try {
4827
+ await fs4.access(configFilePath, constants.F_OK);
4828
+ isConfigFileExists = true;
4829
+ } catch {
4830
+ }
4831
+ if (!isConfigFileExists) {
4832
+ return defaultMountPoint;
4833
+ }
4834
+ const configContent = await fs4.readFile(configFilePath, { encoding: "utf8" });
4835
+ const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
4836
+ if (!configMountPoint) {
4837
+ return defaultMountPoint;
4838
+ }
4839
+ mountPoint = configMountPoint.groups.mountPoint.trim();
4840
+ mountPoint = mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`;
4841
+ return mountPoint;
4842
+ };
4843
+ })();
4844
+ var powerShellPathFromWsl = async () => {
4845
+ const mountPoint = await wslDrivesMountPoint();
4846
+ return `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`;
4847
+ };
4848
+ var powerShellPath = async () => {
4849
+ if (is_wsl_default) {
4850
+ return powerShellPathFromWsl();
4851
+ }
4852
+ return `${process6.env.SYSTEMROOT || process6.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
4853
+ };
4854
+
4855
+ // ../../../node_modules/.pnpm/define-lazy-prop@3.0.0/node_modules/define-lazy-prop/index.js
4856
+ function defineLazyProperty(object, propertyName, valueGetter) {
4857
+ const define = (value) => Object.defineProperty(object, propertyName, { value, enumerable: true, writable: true });
4858
+ Object.defineProperty(object, propertyName, {
4859
+ configurable: true,
4860
+ enumerable: true,
4861
+ get() {
4862
+ const result = valueGetter();
4863
+ define(result);
4864
+ return result;
4865
+ },
4866
+ set(value) {
4867
+ define(value);
4868
+ }
4869
+ });
4870
+ return object;
4871
+ }
4872
+ var execFileAsync = promisify(execFile);
4873
+ async function defaultBrowserId() {
4874
+ if (process6.platform !== "darwin") {
4875
+ throw new Error("macOS only");
4876
+ }
4877
+ const { stdout } = await execFileAsync("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
4878
+ const match = /LSHandlerRoleAll = "(?!-)(?<id>[^"]+?)";\s+?LSHandlerURLScheme = (?:http|https);/.exec(stdout);
4879
+ const browserId = match?.groups.id ?? "com.apple.Safari";
4880
+ if (browserId === "com.apple.safari") {
4881
+ return "com.apple.Safari";
4882
+ }
4883
+ return browserId;
4884
+ }
4885
+ var execFileAsync2 = promisify(execFile);
4886
+ async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
4887
+ if (process6.platform !== "darwin") {
4888
+ throw new Error("macOS only");
4889
+ }
4890
+ const outputArguments = humanReadableOutput ? [] : ["-ss"];
4891
+ const execOptions = {};
4892
+ if (signal) {
4893
+ execOptions.signal = signal;
4894
+ }
4895
+ const { stdout } = await execFileAsync2("osascript", ["-e", script, outputArguments], execOptions);
4896
+ return stdout.trim();
4897
+ }
4898
+
4899
+ // ../../../node_modules/.pnpm/bundle-name@4.1.0/node_modules/bundle-name/index.js
4900
+ async function bundleName(bundleId) {
4901
+ return runAppleScript(`tell application "Finder" to set app_path to application file id "${bundleId}" as string
4902
+ tell application "System Events" to get value of property list item "CFBundleName" of property list file (app_path & ":Contents:Info.plist")`);
4903
+ }
4904
+ var execFileAsync3 = promisify(execFile);
4905
+ var windowsBrowserProgIds = {
4906
+ MSEdgeHTM: { name: "Edge", id: "com.microsoft.edge" },
4907
+ // The missing `L` is correct.
4908
+ MSEdgeBHTML: { name: "Edge Beta", id: "com.microsoft.edge.beta" },
4909
+ MSEdgeDHTML: { name: "Edge Dev", id: "com.microsoft.edge.dev" },
4910
+ AppXq0fevzme2pys62n3e0fbqa7peapykr8v: { name: "Edge", id: "com.microsoft.edge.old" },
4911
+ ChromeHTML: { name: "Chrome", id: "com.google.chrome" },
4912
+ ChromeBHTML: { name: "Chrome Beta", id: "com.google.chrome.beta" },
4913
+ ChromeDHTML: { name: "Chrome Dev", id: "com.google.chrome.dev" },
4914
+ ChromiumHTM: { name: "Chromium", id: "org.chromium.Chromium" },
4915
+ BraveHTML: { name: "Brave", id: "com.brave.Browser" },
4916
+ BraveBHTML: { name: "Brave Beta", id: "com.brave.Browser.beta" },
4917
+ BraveDHTML: { name: "Brave Dev", id: "com.brave.Browser.dev" },
4918
+ BraveSSHTM: { name: "Brave Nightly", id: "com.brave.Browser.nightly" },
4919
+ FirefoxURL: { name: "Firefox", id: "org.mozilla.firefox" },
4920
+ OperaStable: { name: "Opera", id: "com.operasoftware.Opera" },
4921
+ VivaldiHTM: { name: "Vivaldi", id: "com.vivaldi.Vivaldi" },
4922
+ "IE.HTTP": { name: "Internet Explorer", id: "com.microsoft.ie" }
4923
+ };
4924
+ new Map(Object.entries(windowsBrowserProgIds));
4925
+ var UnknownBrowserError = class extends Error {
4926
+ };
4927
+ async function defaultBrowser(_execFileAsync = execFileAsync3) {
4928
+ const { stdout } = await _execFileAsync("reg", [
4929
+ "QUERY",
4930
+ " HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
4931
+ "/v",
4932
+ "ProgId"
4933
+ ]);
4934
+ const match = /ProgId\s*REG_SZ\s*(?<id>\S+)/.exec(stdout);
4935
+ if (!match) {
4936
+ throw new UnknownBrowserError(`Cannot find Windows browser in stdout: ${JSON.stringify(stdout)}`);
4937
+ }
4938
+ const { id } = match.groups;
4939
+ const dotIndex = id.lastIndexOf(".");
4940
+ const hyphenIndex = id.lastIndexOf("-");
4941
+ const baseIdByDot = dotIndex === -1 ? void 0 : id.slice(0, dotIndex);
4942
+ const baseIdByHyphen = hyphenIndex === -1 ? void 0 : id.slice(0, hyphenIndex);
4943
+ return windowsBrowserProgIds[id] ?? windowsBrowserProgIds[baseIdByDot] ?? windowsBrowserProgIds[baseIdByHyphen] ?? { name: id, id };
4944
+ }
4945
+
4946
+ // ../../../node_modules/.pnpm/default-browser@5.5.0/node_modules/default-browser/index.js
4947
+ var execFileAsync4 = promisify(execFile);
4948
+ var titleize = (string) => string.toLowerCase().replaceAll(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
4949
+ async function defaultBrowser2() {
4950
+ if (process6.platform === "darwin") {
4951
+ const id = await defaultBrowserId();
4952
+ const name = await bundleName(id);
4953
+ return { name, id };
4954
+ }
4955
+ if (process6.platform === "linux") {
4956
+ const { stdout } = await execFileAsync4("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
4957
+ const id = stdout.trim();
4958
+ const name = titleize(id.replace(/.desktop$/, "").replace("-", " "));
4959
+ return { name, id };
4960
+ }
4961
+ if (process6.platform === "win32") {
4962
+ return defaultBrowser();
4963
+ }
4964
+ throw new Error("Only macOS, Linux, and Windows are supported");
4965
+ }
4966
+
4967
+ // ../../../node_modules/.pnpm/open@10.2.0/node_modules/open/index.js
4968
+ var execFile5 = promisify(childProcess.execFile);
4969
+ var __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
4970
+ var localXdgOpenPath = path.join(__dirname$1, "xdg-open");
4971
+ var { platform, arch } = process6;
4972
+ async function getWindowsDefaultBrowserFromWsl() {
4973
+ const powershellPath = await powerShellPath();
4974
+ const rawCommand = String.raw`(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId`;
4975
+ const encodedCommand = Buffer$1.from(rawCommand, "utf16le").toString("base64");
4976
+ const { stdout } = await execFile5(
4977
+ powershellPath,
4978
+ [
4979
+ "-NoProfile",
4980
+ "-NonInteractive",
4981
+ "-ExecutionPolicy",
4982
+ "Bypass",
4983
+ "-EncodedCommand",
4984
+ encodedCommand
4985
+ ],
4986
+ { encoding: "utf8" }
4987
+ );
4988
+ const progId = stdout.trim();
4989
+ const browserMap = {
4990
+ ChromeHTML: "com.google.chrome",
4991
+ BraveHTML: "com.brave.Browser",
4992
+ MSEdgeHTM: "com.microsoft.edge",
4993
+ FirefoxURL: "org.mozilla.firefox"
4994
+ };
4995
+ return browserMap[progId] ? { id: browserMap[progId] } : {};
4996
+ }
4997
+ var pTryEach = async (array, mapper) => {
4998
+ let latestError;
4999
+ for (const item of array) {
5000
+ try {
5001
+ return await mapper(item);
5002
+ } catch (error) {
5003
+ latestError = error;
5004
+ }
5005
+ }
5006
+ throw latestError;
5007
+ };
5008
+ var baseOpen = async (options) => {
5009
+ options = {
5010
+ wait: false,
5011
+ background: false,
5012
+ newInstance: false,
5013
+ allowNonzeroExitCode: false,
5014
+ ...options
5015
+ };
5016
+ if (Array.isArray(options.app)) {
5017
+ return pTryEach(options.app, (singleApp) => baseOpen({
5018
+ ...options,
5019
+ app: singleApp
5020
+ }));
5021
+ }
5022
+ let { name: app, arguments: appArguments = [] } = options.app ?? {};
5023
+ appArguments = [...appArguments];
5024
+ if (Array.isArray(app)) {
5025
+ return pTryEach(app, (appName) => baseOpen({
5026
+ ...options,
5027
+ app: {
5028
+ name: appName,
5029
+ arguments: appArguments
5030
+ }
5031
+ }));
5032
+ }
5033
+ if (app === "browser" || app === "browserPrivate") {
5034
+ const ids = {
5035
+ "com.google.chrome": "chrome",
5036
+ "google-chrome.desktop": "chrome",
5037
+ "com.brave.Browser": "brave",
5038
+ "org.mozilla.firefox": "firefox",
5039
+ "firefox.desktop": "firefox",
5040
+ "com.microsoft.msedge": "edge",
5041
+ "com.microsoft.edge": "edge",
5042
+ "com.microsoft.edgemac": "edge",
5043
+ "microsoft-edge.desktop": "edge"
5044
+ };
5045
+ const flags = {
5046
+ chrome: "--incognito",
5047
+ brave: "--incognito",
5048
+ firefox: "--private-window",
5049
+ edge: "--inPrivate"
5050
+ };
5051
+ const browser = is_wsl_default ? await getWindowsDefaultBrowserFromWsl() : await defaultBrowser2();
5052
+ if (browser.id in ids) {
5053
+ const browserName = ids[browser.id];
5054
+ if (app === "browserPrivate") {
5055
+ appArguments.push(flags[browserName]);
5056
+ }
5057
+ return baseOpen({
5058
+ ...options,
5059
+ app: {
5060
+ name: apps[browserName],
5061
+ arguments: appArguments
5062
+ }
5063
+ });
5064
+ }
5065
+ throw new Error(`${browser.name} is not supported as a default browser`);
5066
+ }
5067
+ let command;
5068
+ const cliArguments = [];
5069
+ const childProcessOptions = {};
5070
+ if (platform === "darwin") {
5071
+ command = "open";
5072
+ if (options.wait) {
5073
+ cliArguments.push("--wait-apps");
5074
+ }
5075
+ if (options.background) {
5076
+ cliArguments.push("--background");
5077
+ }
5078
+ if (options.newInstance) {
5079
+ cliArguments.push("--new");
5080
+ }
5081
+ if (app) {
5082
+ cliArguments.push("-a", app);
5083
+ }
5084
+ } else if (platform === "win32" || is_wsl_default && !isInsideContainer() && !app) {
5085
+ command = await powerShellPath();
5086
+ cliArguments.push(
5087
+ "-NoProfile",
5088
+ "-NonInteractive",
5089
+ "-ExecutionPolicy",
5090
+ "Bypass",
5091
+ "-EncodedCommand"
5092
+ );
5093
+ if (!is_wsl_default) {
5094
+ childProcessOptions.windowsVerbatimArguments = true;
5095
+ }
5096
+ const encodedArguments = ["Start"];
5097
+ if (options.wait) {
5098
+ encodedArguments.push("-Wait");
5099
+ }
5100
+ if (app) {
5101
+ encodedArguments.push(`"\`"${app}\`""`);
5102
+ if (options.target) {
5103
+ appArguments.push(options.target);
5104
+ }
5105
+ } else if (options.target) {
5106
+ encodedArguments.push(`"${options.target}"`);
5107
+ }
5108
+ if (appArguments.length > 0) {
5109
+ appArguments = appArguments.map((argument) => `"\`"${argument}\`""`);
5110
+ encodedArguments.push("-ArgumentList", appArguments.join(","));
5111
+ }
5112
+ options.target = Buffer$1.from(encodedArguments.join(" "), "utf16le").toString("base64");
5113
+ } else {
5114
+ if (app) {
5115
+ command = app;
5116
+ } else {
5117
+ const isBundled = !__dirname$1 || __dirname$1 === "/";
5118
+ let exeLocalXdgOpen = false;
5119
+ try {
5120
+ await fs4.access(localXdgOpenPath, constants.X_OK);
5121
+ exeLocalXdgOpen = true;
5122
+ } catch {
5123
+ }
5124
+ const useSystemXdgOpen = process6.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen);
5125
+ command = useSystemXdgOpen ? "xdg-open" : localXdgOpenPath;
5126
+ }
5127
+ if (appArguments.length > 0) {
5128
+ cliArguments.push(...appArguments);
5129
+ }
5130
+ if (!options.wait) {
5131
+ childProcessOptions.stdio = "ignore";
5132
+ childProcessOptions.detached = true;
5133
+ }
5134
+ }
5135
+ if (platform === "darwin" && appArguments.length > 0) {
5136
+ cliArguments.push("--args", ...appArguments);
5137
+ }
5138
+ if (options.target) {
5139
+ cliArguments.push(options.target);
5140
+ }
5141
+ const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
5142
+ if (options.wait) {
5143
+ return new Promise((resolve, reject) => {
5144
+ subprocess.once("error", reject);
5145
+ subprocess.once("close", (exitCode) => {
5146
+ if (!options.allowNonzeroExitCode && exitCode > 0) {
5147
+ reject(new Error(`Exited with code ${exitCode}`));
5148
+ return;
5149
+ }
5150
+ resolve(subprocess);
5151
+ });
5152
+ });
5153
+ }
5154
+ subprocess.unref();
5155
+ return subprocess;
5156
+ };
5157
+ var open = (target, options) => {
5158
+ if (typeof target !== "string") {
5159
+ throw new TypeError("Expected a `target`");
5160
+ }
5161
+ return baseOpen({
5162
+ ...options,
5163
+ target
5164
+ });
5165
+ };
5166
+ function detectArchBinary(binary) {
5167
+ if (typeof binary === "string" || Array.isArray(binary)) {
5168
+ return binary;
5169
+ }
5170
+ const { [arch]: archBinary } = binary;
5171
+ if (!archBinary) {
5172
+ throw new Error(`${arch} is not supported`);
5173
+ }
5174
+ return archBinary;
5175
+ }
5176
+ function detectPlatformBinary({ [platform]: platformBinary }, { wsl }) {
5177
+ if (wsl && is_wsl_default) {
5178
+ return detectArchBinary(wsl);
5179
+ }
5180
+ if (!platformBinary) {
5181
+ throw new Error(`${platform} is not supported`);
5182
+ }
5183
+ return detectArchBinary(platformBinary);
5184
+ }
5185
+ var apps = {};
5186
+ defineLazyProperty(apps, "chrome", () => detectPlatformBinary({
5187
+ darwin: "google chrome",
5188
+ win32: "chrome",
5189
+ linux: ["google-chrome", "google-chrome-stable", "chromium"]
5190
+ }, {
5191
+ wsl: {
5192
+ ia32: "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
5193
+ x64: ["/mnt/c/Program Files/Google/Chrome/Application/chrome.exe", "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"]
5194
+ }
5195
+ }));
5196
+ defineLazyProperty(apps, "brave", () => detectPlatformBinary({
5197
+ darwin: "brave browser",
5198
+ win32: "brave",
5199
+ linux: ["brave-browser", "brave"]
5200
+ }, {
5201
+ wsl: {
5202
+ ia32: "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe",
5203
+ x64: ["/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe"]
5204
+ }
5205
+ }));
5206
+ defineLazyProperty(apps, "firefox", () => detectPlatformBinary({
5207
+ darwin: "firefox",
5208
+ win32: String.raw`C:\Program Files\Mozilla Firefox\firefox.exe`,
5209
+ linux: "firefox"
5210
+ }, {
5211
+ wsl: "/mnt/c/Program Files/Mozilla Firefox/firefox.exe"
5212
+ }));
5213
+ defineLazyProperty(apps, "edge", () => detectPlatformBinary({
5214
+ darwin: "microsoft edge",
5215
+ win32: "msedge",
5216
+ linux: ["microsoft-edge", "microsoft-edge-dev"]
5217
+ }, {
5218
+ wsl: "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
5219
+ }));
5220
+ defineLazyProperty(apps, "browser", () => "browser");
5221
+ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
5222
+ var open_default = open;
5223
+
5224
+ // src/auth.ts
5225
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
5226
+ var generatePKCE = async () => {
5227
+ const codeVerifier = getVerifier();
5228
+ const codeChallenge = await getDigest(codeVerifier, "base64url");
5229
+ return { codeVerifier, codeChallenge };
5230
+ };
5231
+ var fetchDiscovery = async (discoveryUrl) => {
5232
+ const res = await fetch(discoveryUrl);
5233
+ if (!res.ok) {
5234
+ throw new Error(`Failed to fetch OAuth discovery: ${res.status} ${res.statusText}`);
5235
+ }
5236
+ return res.json();
5237
+ };
5238
+ var registerClient = async (registrationEndpoint) => {
5239
+ const res = await fetch(registrationEndpoint, {
5240
+ method: "POST",
5241
+ headers: { "content-type": "application/json" },
5242
+ body: JSON.stringify({
5243
+ client_name: STANDALONE_CLIENT.MCP,
5244
+ redirect_uris: ["http://127.0.0.1/callback"]
5245
+ })
5246
+ });
5247
+ if (!res.ok) {
5248
+ throw new Error(`Client registration failed: ${res.status}`);
5249
+ }
5250
+ const data = await res.json();
5251
+ return data.client_id;
5252
+ };
5253
+ var exchangeCodeForToken = async (tokenEndpoint, code, codeVerifier, clientId, redirectUri) => {
5254
+ const res = await fetch(tokenEndpoint, {
5255
+ method: "POST",
5256
+ headers: { "content-type": "application/x-www-form-urlencoded" },
5257
+ body: new URLSearchParams({
5258
+ grant_type: "authorization_code",
5259
+ code,
5260
+ code_verifier: codeVerifier,
5261
+ client_id: clientId,
5262
+ redirect_uri: redirectUri
5263
+ }).toString()
5264
+ });
5265
+ if (!res.ok) {
5266
+ const error = await res.json().catch(() => ({}));
5267
+ throw new Error(error.error_description ?? error.error ?? `Token exchange failed: ${res.status}`);
5268
+ }
5269
+ return res.json();
5270
+ };
5271
+ var decodeJwtPayload2 = (token) => {
5272
+ const [, payload] = token.split(".");
5273
+ if (!payload) {
5274
+ throw new Error("Invalid JWT format");
5275
+ }
5276
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
5277
+ };
5278
+ var performOAuthFlow = async (discoveryUrl) => {
5279
+ const discovery = await fetchDiscovery(discoveryUrl);
5280
+ let clientId = await deriveClientId(STANDALONE_CLIENT.MCP);
5281
+ if (discovery.registration_endpoint) {
5282
+ clientId = await registerClient(discovery.registration_endpoint);
5283
+ }
5284
+ const { codeVerifier, codeChallenge } = await generatePKCE();
5285
+ const state = getNonce();
5286
+ let resolveCode;
5287
+ let rejectCode;
5288
+ const codePromise = new Promise((resolve, reject) => {
5289
+ resolveCode = resolve;
5290
+ rejectCode = reject;
5291
+ });
5292
+ let server2;
5293
+ server2 = createServer((req, res) => {
5294
+ const url = new URL(req.url, "http://localhost");
5295
+ if (url.pathname === "/callback") {
5296
+ const error = url.searchParams.get("error");
5297
+ if (error) {
5298
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
5299
+ res.end("<html><body><h2>Authentication failed</h2><p>You can close this tab.</p></body></html>");
5300
+ rejectCode(new Error(url.searchParams.get("error_description") ?? error));
5301
+ return;
5302
+ }
5303
+ const code2 = url.searchParams.get("code");
5304
+ const returnedState = url.searchParams.get("state");
5305
+ if (!code2) {
5306
+ res.writeHead(400, { "content-type": "text/plain" });
5307
+ res.end("Missing authorization code");
5308
+ return;
5309
+ }
5310
+ if (returnedState !== state) {
5311
+ res.writeHead(400, { "content-type": "text/plain" });
5312
+ res.end("State mismatch");
5313
+ rejectCode(new Error("OAuth state mismatch \u2014 possible CSRF attack"));
5314
+ return;
5315
+ }
5316
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
5317
+ res.end("<html><body><h2>Authenticated</h2><p>You can return to your terminal.</p></body></html>");
5318
+ resolveCode(code2);
5319
+ } else {
5320
+ res.writeHead(404);
5321
+ res.end();
5322
+ }
5323
+ });
5324
+ await new Promise((r) => server2.listen(0, "127.0.0.1", r));
5325
+ const port = server2.address().port;
5326
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
5327
+ const authUrl = new URL(discovery.authorization_endpoint);
5328
+ authUrl.searchParams.set("client_id", clientId);
5329
+ authUrl.searchParams.set("redirect_uri", redirectUri);
5330
+ authUrl.searchParams.set("code_challenge", codeChallenge);
5331
+ authUrl.searchParams.set("code_challenge_method", "S256");
5332
+ authUrl.searchParams.set("state", state);
5333
+ authUrl.searchParams.set("response_type", "code");
5334
+ await open_default(authUrl.toString());
5335
+ const timeout = setTimeout(() => {
5336
+ server2.close();
5337
+ rejectCode(new Error("Authentication timed out. Please try again."));
5338
+ }, LOGIN_TIMEOUT_MS);
5339
+ let code;
5340
+ try {
5341
+ code = await codePromise;
5342
+ } catch (err) {
5343
+ clearTimeout(timeout);
5344
+ server2.close();
5345
+ throw err;
5346
+ }
5347
+ clearTimeout(timeout);
5348
+ server2.close();
5349
+ const tokenResponse = await exchangeCodeForToken(
5350
+ discovery.token_endpoint,
5351
+ code,
5352
+ codeVerifier,
5353
+ clientId,
5354
+ redirectUri
5355
+ );
5356
+ const payload = decodeJwtPayload2(tokenResponse.access_token);
5357
+ await writeAuthState(
5358
+ {
5359
+ token: tokenResponse.access_token,
5360
+ orgId: payload.orgId,
5361
+ userId: payload.sub
5362
+ },
5363
+ STANDALONE_CLIENT_AUTH_FILE.MCP
5364
+ );
5365
+ return tokenResponse.access_token;
5366
+ };
4039
5367
 
4040
5368
  // src/index.ts
4041
- var server = createMcpServer();
5369
+ var server = createMcpServer({ performOAuthFlow });
4042
5370
  var transport = new StdioServerTransport();
4043
5371
  await server.connect(transport);