@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 +1406 -78
- package/dist/server.js +735 -71
- package/package.json +1 -1
- package/dist/auth-DWIWIJNN.js +0 -629
- package/dist/auth-WHLYYSCQ.js +0 -630
- package/dist/chunk-DCPV7HMV.js +0 -108
- package/dist/chunk-HOOVB46Z.js +0 -107
package/dist/server.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { STANDALONE_CLIENT, MCP_AUTH_FILE, getToken, STANDALONE_CLIENT_AUTH_FILE } from './chunk-HOOVB46Z.js';
|
|
2
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
-
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';
|
|
2
|
+
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';
|
|
3
|
+
import { readFile } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
4
6
|
import { z } from 'zod';
|
|
5
7
|
|
|
8
|
+
// src/server.ts
|
|
9
|
+
|
|
6
10
|
// ../../sdk/extension/ai-docs/src/generated/template-content.ts
|
|
7
11
|
var PATTERN_SECTIONS = [
|
|
8
12
|
{
|
|
@@ -530,6 +534,8 @@ const result = await capabilities.data.fetch('https://api.example.com/orders', {
|
|
|
530
534
|
- For optional secret fields, the entire header is omitted if the value is not configured
|
|
531
535
|
- Declare secret fields in your \`manifest.json\` \`settingsSchema\` with \`"secret": true\`
|
|
532
536
|
|
|
537
|
+
> See [Instance Settings](./instance-settings) for the full schema-declaration + storage-mode story, including which field types accept \`secret: true\`.
|
|
538
|
+
|
|
533
539
|
## context.read \u2014 Read Host Context
|
|
534
540
|
Read host-provided context (customer ID, email, extension settings, etc.).
|
|
535
541
|
- **Permission required:** \`context:read\`
|
|
@@ -1148,6 +1154,202 @@ const context = await capabilities.context.read()
|
|
|
1148
1154
|
\`\`\`
|
|
1149
1155
|
`;
|
|
1150
1156
|
};
|
|
1157
|
+
|
|
1158
|
+
// ../../sdk/extension/ai-docs/src/generators/instance-settings.ts
|
|
1159
|
+
var SECRET_ALLOWED = ["text", "textarea", "email"];
|
|
1160
|
+
var generateInstanceSettings = () => {
|
|
1161
|
+
const fm = frontmatter({
|
|
1162
|
+
root: false,
|
|
1163
|
+
targets: ["*"],
|
|
1164
|
+
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.",
|
|
1165
|
+
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
|
|
1166
|
+
});
|
|
1167
|
+
return `${fm}
|
|
1168
|
+
|
|
1169
|
+
# Instance Settings
|
|
1170
|
+
|
|
1171
|
+
Instance Settings are the per-Instance configuration values your extension needs in
|
|
1172
|
+
order to do its job \u2014 API keys, account identifiers, environment toggles, feature
|
|
1173
|
+
flags. You declare them once in your extension's \`manifest.json\`; whoever installs
|
|
1174
|
+
your extension fills them in from the admin dashboard at install time; your code
|
|
1175
|
+
reads the values at runtime.
|
|
1176
|
+
|
|
1177
|
+
There are two flavors:
|
|
1178
|
+
|
|
1179
|
+
- **Regular** values live in plaintext and are readable from your extension code.
|
|
1180
|
+
Use them for non-sensitive configuration the UI needs to branch on.
|
|
1181
|
+
- **Secure** values are encrypted at rest and **never** leave the server. Use them
|
|
1182
|
+
for credentials. Your code references them as a placeholder; the proxy
|
|
1183
|
+
substitutes the real value server-side when it makes the outbound request.
|
|
1184
|
+
|
|
1185
|
+
## 1. Declare your settings in \`manifest.json\`
|
|
1186
|
+
|
|
1187
|
+
Add a \`settingsSchema\` array to \`packages/extension/public/manifest.json\`. Each
|
|
1188
|
+
entry describes one input on the install-time admin form:
|
|
1189
|
+
|
|
1190
|
+
\`\`\`json
|
|
1191
|
+
{
|
|
1192
|
+
"name": "My Extension",
|
|
1193
|
+
"version": "1.0.0",
|
|
1194
|
+
"targets": ["slot.content"],
|
|
1195
|
+
"permissions": ["context:read", "data:fetch"],
|
|
1196
|
+
"allowedDomains": ["api.example.com"],
|
|
1197
|
+
"settingsSchema": [
|
|
1198
|
+
{
|
|
1199
|
+
"identifier": "apiKey",
|
|
1200
|
+
"label": "API Key",
|
|
1201
|
+
"type": "text",
|
|
1202
|
+
"secret": true,
|
|
1203
|
+
"required": true,
|
|
1204
|
+
"description": "Your account API key. Stored encrypted; injected server-side into outbound request headers."
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
"identifier": "environmentType",
|
|
1208
|
+
"label": "Environment",
|
|
1209
|
+
"type": "select",
|
|
1210
|
+
"options": [
|
|
1211
|
+
{ "label": "Production", "value": "prod" },
|
|
1212
|
+
{ "label": "Sandbox", "value": "sandbox" }
|
|
1213
|
+
]
|
|
1214
|
+
}
|
|
1215
|
+
]
|
|
1216
|
+
}
|
|
1217
|
+
\`\`\`
|
|
1218
|
+
|
|
1219
|
+
That's the entire authoring step. The next time someone installs (or re-syncs)
|
|
1220
|
+
your extension, the admin dashboard renders a form from this schema on the
|
|
1221
|
+
Instance settings page. Regular fields show inline values; secret fields show a
|
|
1222
|
+
masked input that accepts a value once and displays \`\u2022\u2022\u2022\u2022\` after save (the
|
|
1223
|
+
cleartext value is never returned by the API).
|
|
1224
|
+
|
|
1225
|
+
### Field types
|
|
1226
|
+
|
|
1227
|
+
| Type | Renders as | Accepts \`secret: true\` |
|
|
1228
|
+
|------|------------|--------------------------|
|
|
1229
|
+
| \`text\` | Single-line text input | Yes |
|
|
1230
|
+
| \`textarea\` | Multi-line text input | Yes |
|
|
1231
|
+
| \`email\` | Email input with format validation | Yes |
|
|
1232
|
+
| \`number\` | Numeric input (\`min\` / \`max\` / \`step\`) | No |
|
|
1233
|
+
| \`select\` | Dropdown (use \`allowMultiple\` for multi-select) | No |
|
|
1234
|
+
| \`radio\` | Radio button group | No |
|
|
1235
|
+
| \`toggle\` | On/off switch | No |
|
|
1236
|
+
| \`tags\` | Tag list / multi-string entry | No |
|
|
1237
|
+
|
|
1238
|
+
The \`secret: true\` flag is only valid on \`${SECRET_ALLOWED.join("`, `")}\` types.
|
|
1239
|
+
|
|
1240
|
+
All fields share these optional properties: \`required\`, \`placeholder\`,
|
|
1241
|
+
\`description\` (help text shown below the input), and \`dependsOn\` (conditional
|
|
1242
|
+
visibility \u2014 show this field only when another field has a matching value).
|
|
1243
|
+
|
|
1244
|
+
## 2. The install-time admin form
|
|
1245
|
+
|
|
1246
|
+
When someone installs your extension into one of their Instances, the admin
|
|
1247
|
+
dashboard reads your \`settingsSchema\` and generates a form. Each field becomes
|
|
1248
|
+
an input; \`required\` fields block save; \`description\` text becomes inline help;
|
|
1249
|
+
\`dependsOn\` rules show or hide fields based on other field values.
|
|
1250
|
+
|
|
1251
|
+
Each Instance gets its own copy of these values, so the same extension can run
|
|
1252
|
+
side-by-side on multiple Instances with completely different credentials and
|
|
1253
|
+
configuration. The installer can edit values later from the same Instance
|
|
1254
|
+
settings page \u2014 your code always sees the current values.
|
|
1255
|
+
|
|
1256
|
+
## 3. Read regular values: \`useSettings()\` (or \`useContextData()\`)
|
|
1257
|
+
|
|
1258
|
+
Regular (non-secret) values are exposed to your extension code via the
|
|
1259
|
+
\`useSettings()\` hook, keyed by each field's \`identifier\`. This requires the
|
|
1260
|
+
\`context:read\` permission.
|
|
1261
|
+
|
|
1262
|
+
\`\`\`tsx
|
|
1263
|
+
import { Surface, ui, useSettings } from '@stackable-labs/sdk-extension-react'
|
|
1264
|
+
|
|
1265
|
+
export const Content = () => {
|
|
1266
|
+
const settings = useSettings()
|
|
1267
|
+
const environmentType = (settings.environmentType as string) || 'prod'
|
|
1268
|
+
|
|
1269
|
+
return (
|
|
1270
|
+
<Surface id="slot.content">
|
|
1271
|
+
<ui.Card>
|
|
1272
|
+
<ui.CardContent>
|
|
1273
|
+
<ui.Text>Looking up orders\u2026</ui.Text>
|
|
1274
|
+
{environmentType === 'sandbox' && (
|
|
1275
|
+
<ui.Text className="text-xs opacity-50">Sandbox Mode</ui.Text>
|
|
1276
|
+
)}
|
|
1277
|
+
</ui.CardContent>
|
|
1278
|
+
</ui.Card>
|
|
1279
|
+
</Surface>
|
|
1280
|
+
)
|
|
1281
|
+
}
|
|
1282
|
+
\`\`\`
|
|
1283
|
+
|
|
1284
|
+
If you need settings alongside other context data (customer, locale, etc.), they
|
|
1285
|
+
also come back from \`useContextData()\`:
|
|
1286
|
+
|
|
1287
|
+
\`\`\`tsx
|
|
1288
|
+
import { useContextData } from '@stackable-labs/sdk-extension-react'
|
|
1289
|
+
|
|
1290
|
+
const { loading, settings, customerId } = useContextData()
|
|
1291
|
+
\`\`\`
|
|
1292
|
+
|
|
1293
|
+
> **Note:** Secret fields are **never** present in \`useSettings()\` or on
|
|
1294
|
+
> \`ctx.settings\`. Reading \`settings.apiKey\` for a \`secret: true\` field returns
|
|
1295
|
+
> \`undefined\`, even when a value is configured. If you find yourself reaching
|
|
1296
|
+
> for a secret value in extension code, you almost certainly want the
|
|
1297
|
+
> \`data.fetch\` placeholder pattern below instead.
|
|
1298
|
+
|
|
1299
|
+
## 4. Use secure values: \`{{settings.<identifier>}}\` in \`data.fetch\`
|
|
1300
|
+
|
|
1301
|
+
Secure values are decrypted only by the proxy Lambda that handles \`data.fetch\` \u2014
|
|
1302
|
+
at substitution time, per-request, then discarded. Your code references them as
|
|
1303
|
+
template placeholders in **header values**:
|
|
1304
|
+
|
|
1305
|
+
\`\`\`tsx
|
|
1306
|
+
import { useCapabilities, useSettings } from '@stackable-labs/sdk-extension-react'
|
|
1307
|
+
|
|
1308
|
+
const capabilities = useCapabilities()
|
|
1309
|
+
const settings = useSettings()
|
|
1310
|
+
const environmentType = (settings.environmentType as string) || 'prod'
|
|
1311
|
+
|
|
1312
|
+
const result = await capabilities.data.fetch('https://api.example.com/orders', {
|
|
1313
|
+
method: 'GET',
|
|
1314
|
+
headers: {
|
|
1315
|
+
// environmentType is a regular value \u2014 interpolate it normally
|
|
1316
|
+
'X-Environment': environmentType,
|
|
1317
|
+
// apiKey is secret \u2014 the proxy substitutes it server-side; the cleartext
|
|
1318
|
+
// value never enters this code path
|
|
1319
|
+
'X-Api-Key': '{{settings.apiKey}}',
|
|
1320
|
+
},
|
|
1321
|
+
})
|
|
1322
|
+
if (!result.ok) throw new Error(\`Request failed: \${result.status}\`)
|
|
1323
|
+
\`\`\`
|
|
1324
|
+
|
|
1325
|
+
Placeholders are only resolved in **header values** \u2014 not URLs, not header
|
|
1326
|
+
names, not the request body. For \`required: true\` secret fields the proxy
|
|
1327
|
+
returns \`400\` when the value is not configured; for optional secret fields
|
|
1328
|
+
the entire header is omitted.
|
|
1329
|
+
|
|
1330
|
+
## Regular vs secure at a glance
|
|
1331
|
+
|
|
1332
|
+
| | Regular | Secure (\`secret: true\`) |
|
|
1333
|
+
|--|---------|--------------------------|
|
|
1334
|
+
| **Stored** | Plaintext | Encrypted with a per-Instance derived key |
|
|
1335
|
+
| **Readable from extension code?** | Yes \u2014 \`useSettings().<identifier>\` | **Never** \u2014 secrets are not serialized to the browser or sandbox |
|
|
1336
|
+
| **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 |
|
|
1337
|
+
| **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) |
|
|
1338
|
+
|
|
1339
|
+
## When in doubt, mark it \`secret: true\`
|
|
1340
|
+
|
|
1341
|
+
The cost is negligible \u2014 one encryption per write, one decryption per outbound
|
|
1342
|
+
request. The blast radius of an accidental log line, screenshot, or sandbox
|
|
1343
|
+
exfiltration drops to zero. Secrets on one Instance also can't be used to
|
|
1344
|
+
decrypt secrets on any other Instance, even when both Instances run the same
|
|
1345
|
+
extension \u2014 encryption keys are derived per-Instance.
|
|
1346
|
+
|
|
1347
|
+
If a value is sensitive at all (API keys, OAuth tokens, signing secrets, webhook
|
|
1348
|
+
shared secrets, personal access tokens), declare the field \`secret: true\` and
|
|
1349
|
+
reach for it via the \`data.fetch\` placeholder pattern. Save \`useSettings()\` for
|
|
1350
|
+
the values your UI legitimately needs to branch on.
|
|
1351
|
+
`;
|
|
1352
|
+
};
|
|
1151
1353
|
var generatePermissions = () => {
|
|
1152
1354
|
const fm = frontmatter({
|
|
1153
1355
|
root: false,
|
|
@@ -1538,28 +1740,42 @@ export const Header = () => {
|
|
|
1538
1740
|
- **Use ScrollArea** \u2014 wrap content surfaces in \`<ui.ScrollArea>\` for overflow handling
|
|
1539
1741
|
`;
|
|
1540
1742
|
};
|
|
1541
|
-
|
|
1542
|
-
// ../../sdk/extension/ai-docs/src/cli-commands.ts
|
|
1543
1743
|
var DLX = "pnpm --config.dlx-cache-max-age=0 dlx";
|
|
1544
1744
|
var CLI = {
|
|
1545
1745
|
/** Scaffold a new extension project */
|
|
1546
|
-
create: (name = "<
|
|
1746
|
+
create: (name = "<extension-name>") => `${DLX} @stackable-labs/create-extension ${name}`,
|
|
1547
1747
|
/** Start dev servers with hot reload */
|
|
1548
1748
|
dev: `${DLX} @stackable-labs/cli-app-extension@latest dev`,
|
|
1549
|
-
/** Deploy the extension */
|
|
1749
|
+
/** Deploy the extension (future) */
|
|
1550
1750
|
deploy: `${DLX} @stackable-labs/cli-app-extension@latest deploy`,
|
|
1551
|
-
/** Validate the extension for common errors */
|
|
1751
|
+
/** Validate the extension for common errors (coming soon) */
|
|
1552
1752
|
validate: `${DLX} @stackable-labs/cli-app-extension@latest validate`,
|
|
1553
1753
|
/** Scaffold from an existing extension */
|
|
1554
1754
|
scaffold: `${DLX} @stackable-labs/cli-app-extension@latest scaffold`,
|
|
1555
1755
|
/** Update an existing extension */
|
|
1556
|
-
update: `${DLX} @stackable-labs/cli-app-extension@latest update
|
|
1756
|
+
update: `${DLX} @stackable-labs/cli-app-extension@latest update`,
|
|
1757
|
+
/** Manage CLI authentication (browser-based OAuth) */
|
|
1758
|
+
auth: {
|
|
1759
|
+
login: `${DLX} @stackable-labs/cli-app-extension@latest auth login`,
|
|
1760
|
+
logout: `${DLX} @stackable-labs/cli-app-extension@latest auth logout`,
|
|
1761
|
+
status: `${DLX} @stackable-labs/cli-app-extension@latest auth status`
|
|
1762
|
+
},
|
|
1763
|
+
/** AI editor configuration tools (Skills + MCP) */
|
|
1764
|
+
ai: {
|
|
1765
|
+
/** Download AI editor config files (Stackable Skills) into your project */
|
|
1766
|
+
scaffold: `${DLX} @stackable-labs/cli-app-extension@latest ai scaffold`,
|
|
1767
|
+
/** Add Stackable MCP server config to your project */
|
|
1768
|
+
mcp: `${DLX} @stackable-labs/cli-app-extension@latest ai mcp`
|
|
1769
|
+
}
|
|
1557
1770
|
};
|
|
1558
|
-
var
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1771
|
+
var TEMPLATE_FLAVOR_META = {
|
|
1772
|
+
minimal: { label: "Minimal", description: "Bare minimum \u2014 single surface, hello-world component" },
|
|
1773
|
+
starter: { label: "Starter", description: "Common patterns \u2014 store, api helpers, menu (default)" },
|
|
1774
|
+
"kitchen-sink": { label: "Kitchen Sink", description: "Everything \u2014 every component, capability, surface, and hook" }
|
|
1775
|
+
};
|
|
1776
|
+
var TEMPLATE_FLAVOR_DETAILS = TEMPLATE_FLAVORS.map(
|
|
1777
|
+
(name) => ({ name, ...TEMPLATE_FLAVOR_META[name] })
|
|
1778
|
+
);
|
|
1563
1779
|
|
|
1564
1780
|
// ../../sdk/extension/ai-docs/src/generators/quick-start.ts
|
|
1565
1781
|
var generateQuickStart = () => {
|
|
@@ -1575,6 +1791,21 @@ var generateQuickStart = () => {
|
|
|
1575
1791
|
|
|
1576
1792
|
Create, develop, and deploy your first Stackable extension in minutes.
|
|
1577
1793
|
|
|
1794
|
+
## Choosing your path
|
|
1795
|
+
|
|
1796
|
+
Stackable supports two authoring paths. Both produce the same kind of extension and ship to the same marketplace.
|
|
1797
|
+
|
|
1798
|
+
| | AI Extension Studio | CLI (this guide) |
|
|
1799
|
+
|---|---|---|
|
|
1800
|
+
| **Best for** | Prototyping, learning, quick iterations | Production extensions, team workflows |
|
|
1801
|
+
| **Code structure** | Single file | Multi-file (surfaces/, components/, lib/) |
|
|
1802
|
+
| **Preview** | Built-in live preview | Local dev server + Cloudflare tunnel |
|
|
1803
|
+
| **AI assistance** | Sidekick chat + smart insertion | Skills, MCP server, and Claude Code plugin (see [AI-Accelerated Development](/docs/reference/ai-accelerated-development)) |
|
|
1804
|
+
| **Version control** | Auto-saved to cloud | Git-based |
|
|
1805
|
+
| **Deployment** | Link to extension + deploy | CLI deploy command |
|
|
1806
|
+
|
|
1807
|
+
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).
|
|
1808
|
+
|
|
1578
1809
|
## Prerequisites
|
|
1579
1810
|
|
|
1580
1811
|
- **Node.js** 22 or later
|
|
@@ -1713,21 +1944,11 @@ export const Content = () => {
|
|
|
1713
1944
|
}
|
|
1714
1945
|
\`\`\`
|
|
1715
1946
|
|
|
1716
|
-
## 6.
|
|
1947
|
+
## 6. Submit for review
|
|
1717
1948
|
|
|
1718
|
-
|
|
1949
|
+
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.
|
|
1719
1950
|
|
|
1720
|
-
|
|
1721
|
-
${CLI.validate}
|
|
1722
|
-
\`\`\`
|
|
1723
|
-
|
|
1724
|
-
This checks manifest structure, permission usage, surface targets, and import patterns.
|
|
1725
|
-
|
|
1726
|
-
When ready, deploy:
|
|
1727
|
-
|
|
1728
|
-
\`\`\`bash
|
|
1729
|
-
${CLI.deploy}
|
|
1730
|
-
\`\`\`
|
|
1951
|
+
> *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.*
|
|
1731
1952
|
|
|
1732
1953
|
## Next Steps
|
|
1733
1954
|
|
|
@@ -1746,7 +1967,7 @@ var generateCliReference = () => {
|
|
|
1746
1967
|
description: "CLI commands for creating, developing, and deploying Stackable extensions",
|
|
1747
1968
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
1748
1969
|
});
|
|
1749
|
-
const templateRows =
|
|
1970
|
+
const templateRows = TEMPLATE_FLAVOR_DETAILS.map((t) => `| \`${t.name}\` | ${t.description} |`).join("\n");
|
|
1750
1971
|
return `${fm}
|
|
1751
1972
|
|
|
1752
1973
|
# CLI Reference
|
|
@@ -1765,7 +1986,7 @@ ${CLI.create()}
|
|
|
1765
1986
|
|
|
1766
1987
|
| Argument | Description |
|
|
1767
1988
|
|----------|-------------|
|
|
1768
|
-
| \`
|
|
1989
|
+
| \`extension-name\` | Name for your extension (saved as the manifest's \`name\`; kebab-cased to derive the directory and extension ID) |
|
|
1769
1990
|
|
|
1770
1991
|
**Options:**
|
|
1771
1992
|
|
|
@@ -1826,9 +2047,11 @@ Append this to your deployed host app URL to load your local extension instead
|
|
|
1826
2047
|
of the production bundle. The override is browser-session only \u2014 no DB changes,
|
|
1827
2048
|
no shared state. Each developer gets isolated overrides.
|
|
1828
2049
|
|
|
1829
|
-
## validate
|
|
2050
|
+
## validate *(coming soon)*
|
|
1830
2051
|
|
|
1831
|
-
|
|
2052
|
+
> *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).*
|
|
2053
|
+
|
|
2054
|
+
When shipped, the \`validate\` command will check your extension for common errors locally before submission:
|
|
1832
2055
|
|
|
1833
2056
|
\`\`\`bash
|
|
1834
2057
|
${CLI.validate}
|
|
@@ -1849,23 +2072,21 @@ ${CLI.validate}
|
|
|
1849
2072
|
- \`0\` \u2014 all checks pass
|
|
1850
2073
|
- \`1\` \u2014 errors found (must fix before deploying)
|
|
1851
2074
|
|
|
1852
|
-
## deploy
|
|
2075
|
+
## deploy *(future)*
|
|
2076
|
+
|
|
2077
|
+
> *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.*
|
|
1853
2078
|
|
|
1854
|
-
|
|
2079
|
+
When shipped, the \`deploy\` command will package and ship your extension straight from the terminal:
|
|
1855
2080
|
|
|
1856
2081
|
\`\`\`bash
|
|
1857
2082
|
${CLI.deploy}
|
|
1858
2083
|
\`\`\`
|
|
1859
2084
|
|
|
1860
|
-
**
|
|
1861
|
-
1.
|
|
1862
|
-
2.
|
|
1863
|
-
3.
|
|
1864
|
-
4.
|
|
1865
|
-
|
|
1866
|
-
**Prerequisites:**
|
|
1867
|
-
- All validation checks must pass
|
|
1868
|
-
- You must be authenticated (follow the prompts if not)
|
|
2085
|
+
**Planned behavior:**
|
|
2086
|
+
1. Run validation checks (same as \`validate\`)
|
|
2087
|
+
2. Build the extension for production
|
|
2088
|
+
3. Package the build output
|
|
2089
|
+
4. Upload to the Stackable extension registry
|
|
1869
2090
|
|
|
1870
2091
|
## scaffold
|
|
1871
2092
|
|
|
@@ -1902,6 +2123,62 @@ ${CLI.update} [extensionId]
|
|
|
1902
2123
|
| \`--bundle-url <url>\` | New bundle URL |
|
|
1903
2124
|
| \`--enabled <bool>\` | Enable/disable extension |
|
|
1904
2125
|
| \`--dir <path>\` | Project root (default: cwd) |
|
|
2126
|
+
|
|
2127
|
+
## auth
|
|
2128
|
+
|
|
2129
|
+
Manage CLI authentication. Subcommands open a browser-based OAuth flow against the Stackable platform and store the resulting credentials locally.
|
|
2130
|
+
|
|
2131
|
+
**Subcommands:**
|
|
2132
|
+
|
|
2133
|
+
| Subcommand | Description |
|
|
2134
|
+
|------------|-------------|
|
|
2135
|
+
| \`login\` | Authenticate with Stackable via browser-based OAuth |
|
|
2136
|
+
| \`logout\` | Clear stored CLI credentials |
|
|
2137
|
+
| \`status\` | Show current authentication status (signed-in user, org, token expiry) |
|
|
2138
|
+
|
|
2139
|
+
**Examples:**
|
|
2140
|
+
|
|
2141
|
+
\`\`\`bash
|
|
2142
|
+
# First-time setup
|
|
2143
|
+
${CLI.auth.login}
|
|
2144
|
+
|
|
2145
|
+
# Check who's signed in
|
|
2146
|
+
${CLI.auth.status}
|
|
2147
|
+
|
|
2148
|
+
# Sign out
|
|
2149
|
+
${CLI.auth.logout}
|
|
2150
|
+
\`\`\`
|
|
2151
|
+
|
|
2152
|
+
## ai
|
|
2153
|
+
|
|
2154
|
+
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.
|
|
2155
|
+
|
|
2156
|
+
**Subcommands:**
|
|
2157
|
+
|
|
2158
|
+
| Subcommand | Description |
|
|
2159
|
+
|------------|-------------|
|
|
2160
|
+
| \`scaffold\` | Download Stackable Skills into your project's \`.claude/\`, \`.cursor/\`, \`.windsurf/\`, etc. |
|
|
2161
|
+
| \`mcp\` | Add Stackable MCP server config to your project so any MCP-compatible AI client can connect |
|
|
2162
|
+
|
|
2163
|
+
**Options:**
|
|
2164
|
+
|
|
2165
|
+
| Flag | Description | Applies to |
|
|
2166
|
+
|------|-------------|------------|
|
|
2167
|
+
| \`--version <version>\` | AI docs version (semver or \`latest\`, default: \`latest\`) | both |
|
|
2168
|
+
| \`--dir <path>\` | Project root directory (default: cwd) | \`mcp\` only |
|
|
2169
|
+
|
|
2170
|
+
**Examples:**
|
|
2171
|
+
|
|
2172
|
+
\`\`\`bash
|
|
2173
|
+
# Add the latest Stackable Skills to your project
|
|
2174
|
+
${CLI.ai.scaffold}
|
|
2175
|
+
|
|
2176
|
+
# Pin a specific version
|
|
2177
|
+
${CLI.ai.scaffold} --version 0.2.0
|
|
2178
|
+
|
|
2179
|
+
# Configure your project to talk to the Stackable MCP server
|
|
2180
|
+
${CLI.ai.mcp}
|
|
2181
|
+
\`\`\`
|
|
1905
2182
|
`;
|
|
1906
2183
|
};
|
|
1907
2184
|
|
|
@@ -2060,29 +2337,29 @@ var generateExtensionStudio = () => {
|
|
|
2060
2337
|
const fm = frontmatter({
|
|
2061
2338
|
root: false,
|
|
2062
2339
|
targets: ["*"],
|
|
2063
|
-
description: "Extension Studio: in-browser builder with AI assistant, component palette, and live preview",
|
|
2340
|
+
description: "AI Extension Studio: in-browser builder with AI assistant, component palette, and live preview",
|
|
2064
2341
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
2065
2342
|
});
|
|
2066
2343
|
return `${fm}
|
|
2067
2344
|
|
|
2068
|
-
# Extension Studio
|
|
2345
|
+
# AI Extension Studio
|
|
2069
2346
|
|
|
2070
|
-
Extension Studio is an in-browser development environment for building Stackable
|
|
2347
|
+
Stackable's AI Extension Studio is an in-browser development environment for building Stackable
|
|
2071
2348
|
extensions without local tooling. It runs inside the admin dashboard and provides
|
|
2072
2349
|
a code editor, live preview, component palette, and an AI assistant \u2014 everything
|
|
2073
2350
|
needed to create, iterate on, and deploy extensions from the browser.
|
|
2074
2351
|
|
|
2075
2352
|
## Layout
|
|
2076
2353
|
|
|
2077
|
-
Studio is organized as a 3-pane workspace:
|
|
2354
|
+
"Studio" is organized as a 3-pane workspace:
|
|
2078
2355
|
|
|
2079
2356
|
| Pane | Position | Purpose |
|
|
2080
2357
|
|------|----------|---------|
|
|
2081
2358
|
| **Shelf** | Left (collapsible) | Component palette, surface picker, capability list |
|
|
2082
|
-
| **Stage** | Center | Code editor
|
|
2359
|
+
| **Stage** | Center | Code editor or live preview \u2014 toggle between modes |
|
|
2083
2360
|
| **Sidekick** | Right (collapsible, resizable) | AI chat assistant for code generation and SDK guidance |
|
|
2084
2361
|
|
|
2085
|
-
## The Shelf
|
|
2362
|
+
## The "Shelf"
|
|
2086
2363
|
|
|
2087
2364
|
The Shelf is a collapsible sidebar with three sections:
|
|
2088
2365
|
|
|
@@ -2107,7 +2384,7 @@ The SDK capabilities your extension can use: \`data.query\`, \`data.fetch\`,
|
|
|
2107
2384
|
\`events:identity\`, \`events:messaging\`, and \`events:activity\`. Clicking a
|
|
2108
2385
|
capability adds the permission to your manifest and AI-inserts the hook usage.
|
|
2109
2386
|
|
|
2110
|
-
## The Stage
|
|
2387
|
+
## The "Stage"
|
|
2111
2388
|
|
|
2112
2389
|
The center pane switches between two modes:
|
|
2113
2390
|
|
|
@@ -2122,7 +2399,7 @@ Changes in Code mode recompile automatically via in-browser esbuild and update
|
|
|
2122
2399
|
the preview. The toolbar shows the current status: Ready, Saving, Compiling,
|
|
2123
2400
|
Thinking (AI), or Error.
|
|
2124
2401
|
|
|
2125
|
-
## The Sidekick
|
|
2402
|
+
## The "Sidekick"
|
|
2126
2403
|
|
|
2127
2404
|
The Sidekick is an AI chat assistant that understands the Stackable Extension SDK.
|
|
2128
2405
|
It can:
|
|
@@ -2162,18 +2439,114 @@ Studio toolbar.
|
|
|
2162
2439
|
|
|
2163
2440
|
## Studio vs CLI
|
|
2164
2441
|
|
|
2165
|
-
|
|
2442
|
+
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.
|
|
2443
|
+
|
|
2444
|
+
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.
|
|
2445
|
+
`;
|
|
2446
|
+
};
|
|
2447
|
+
|
|
2448
|
+
// ../../sdk/extension/ai-docs/src/generators/ai-accelerated-development.ts
|
|
2449
|
+
var generateAIAcceleratedDevelopment = () => {
|
|
2450
|
+
const fm = frontmatter({
|
|
2451
|
+
root: false,
|
|
2452
|
+
targets: ["*"],
|
|
2453
|
+
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.",
|
|
2454
|
+
globs: ["*"]
|
|
2455
|
+
});
|
|
2456
|
+
return `${fm}
|
|
2457
|
+
|
|
2458
|
+
# AI-Accelerated Development
|
|
2459
|
+
|
|
2460
|
+
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.
|
|
2461
|
+
|
|
2462
|
+
| Surface | Best for | Install |
|
|
2166
2463
|
|---|---|---|
|
|
2167
|
-
| **
|
|
2168
|
-
| **
|
|
2169
|
-
| **
|
|
2170
|
-
| **
|
|
2171
|
-
|
|
2172
|
-
|
|
2464
|
+
| **AI Extension Studio** | First extensions, prototyping, in-browser builds | Open the admin dashboard |
|
|
2465
|
+
| **Agent Skills** | Any AI coding assistant | \`pnpm dlx skills add stackable-labs/skills\` (auto-bundled in scaffolds) |
|
|
2466
|
+
| **Live MCP Server** | AI clients that speak MCP | Add to your client's MCP config |
|
|
2467
|
+
| **Claude Code Plugin** | Claude Code users (bundles Skills + MCP) | \`/plugin marketplace add stackable-labs/claude-plugins\` |
|
|
2468
|
+
|
|
2469
|
+
---
|
|
2470
|
+
|
|
2471
|
+
## AI Extension Studio
|
|
2472
|
+
|
|
2473
|
+
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!**
|
|
2474
|
+
|
|
2475
|
+
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.
|
|
2476
|
+
|
|
2477
|
+
**Best for:** first extensions, prototyping, non-developers, and quick experiments.
|
|
2478
|
+
|
|
2479
|
+
**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.
|
|
2480
|
+
|
|
2481
|
+
For full Studio details, see [AI Extension Studio](/docs/reference/extension-studio).
|
|
2482
|
+
|
|
2483
|
+
## AI Agentic Skills
|
|
2484
|
+
|
|
2485
|
+
[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.
|
|
2486
|
+
|
|
2487
|
+
### How to install
|
|
2488
|
+
|
|
2489
|
+
- **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.
|
|
2490
|
+
- **Standalone install** \u2014 for an existing project, or to update to the latest skills, run:
|
|
2491
|
+
\`\`\`bash
|
|
2492
|
+
pnpm dlx skills add stackable-labs/skills
|
|
2493
|
+
\`\`\`
|
|
2494
|
+
This is powered by [Vercel's open \`skills\` CLI](https://github.com/vercel-labs/skills).
|
|
2495
|
+
|
|
2496
|
+
### Compatible assistants
|
|
2497
|
+
|
|
2498
|
+
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.
|
|
2499
|
+
|
|
2500
|
+
## Live MCP Server
|
|
2501
|
+
|
|
2502
|
+
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.
|
|
2503
|
+
|
|
2504
|
+
**Endpoint:** \`https://api-use1.stackablelabs.io/mcp/app-extension\`
|
|
2505
|
+
|
|
2506
|
+
### Available tools (snapshot)
|
|
2507
|
+
|
|
2508
|
+
| Tool | What it does |
|
|
2509
|
+
|---|---|
|
|
2510
|
+
| \`list_apps\` | List apps in your account |
|
|
2511
|
+
| \`list_extensions\` | List extensions for a given app |
|
|
2512
|
+
| \`list_instances\` | List deployed instances of an extension |
|
|
2513
|
+
| \`list_skills\` | List available SDK skills |
|
|
2514
|
+
| \`lookup_skill\` | Fetch a specific SDK skill's content on demand |
|
|
2515
|
+
| \`get_extension\` | Fetch full details for a single extension |
|
|
2516
|
+
| \`validate_manifest\` | Validate an extension manifest against the platform |
|
|
2517
|
+
| \`validate_permissions\` | Validate manifest permissions |
|
|
2518
|
+
|
|
2519
|
+
### Compatible clients
|
|
2520
|
+
|
|
2521
|
+
[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.
|
|
2522
|
+
|
|
2523
|
+
Authentication uses standard OAuth2 \u2014 your client will prompt you to sign in on first use.
|
|
2524
|
+
|
|
2525
|
+
## Claude Code Plugin
|
|
2526
|
+
|
|
2527
|
+
For Claude Code users, the Stackable Claude Code plugin bundles the Skills and pre-configures the MCP server in one install.
|
|
2528
|
+
|
|
2529
|
+
**Install:**
|
|
2530
|
+
\`\`\`bash
|
|
2531
|
+
# Add marketplace
|
|
2532
|
+
/plugin marketplace add stackable-labs/claude-plugins
|
|
2533
|
+
|
|
2534
|
+
# Install plugin
|
|
2535
|
+
/plugin install stackable-extension-dev@stackable-claude-plugins
|
|
2536
|
+
\`\`\`
|
|
2537
|
+
|
|
2538
|
+
**What it includes:**
|
|
2539
|
+
- Latest Stackable Agent Skills (auto-updated)
|
|
2540
|
+
- The Stackable MCP server pre-configured
|
|
2541
|
+
- Slash commands and workflows tailored for extension development
|
|
2542
|
+
|
|
2543
|
+
After install, Claude Code can scaffold, validate, and inspect your Stackable extensions directly from your editor without any extra config.
|
|
2173
2544
|
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2545
|
+
## Choosing your tools
|
|
2546
|
+
|
|
2547
|
+
- **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.
|
|
2548
|
+
- **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.
|
|
2549
|
+
- **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.
|
|
2177
2550
|
`;
|
|
2178
2551
|
};
|
|
2179
2552
|
var generateProjectStructure = () => {
|
|
@@ -3012,12 +3385,23 @@ var generateValidateExtensionCommand = () => {
|
|
|
3012
3385
|
description: "Validate this extension for common errors before deploying (manifest, permissions, surfaces, imports)",
|
|
3013
3386
|
targets: ["*"]
|
|
3014
3387
|
});
|
|
3388
|
+
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");
|
|
3015
3389
|
return `${fm}
|
|
3016
3390
|
|
|
3017
3391
|
# Validate Extension
|
|
3018
3392
|
|
|
3019
3393
|
Check this extension for common errors before deploying. Run through each check
|
|
3020
|
-
and
|
|
3394
|
+
and ensure no issues found before you submit.
|
|
3395
|
+
|
|
3396
|
+
> **Tip \u2014 let the platform validate for you.** The hosted Stackable MCP server
|
|
3397
|
+
> exposes a \`validate_manifest\` tool that runs the same server-side checks the
|
|
3398
|
+
> marketplace submission flow uses. If your AI client is connected to the
|
|
3399
|
+
> Stackable MCP server (see [AI-Accelerated Development](/docs/reference/ai-accelerated-development#live-mcp-server)),
|
|
3400
|
+
> just ask it to validate \`packages/extension/public/manifest.json\` for you \u2014
|
|
3401
|
+
> it returns structured errors and warnings without you having to walk the
|
|
3402
|
+
> checklist below by hand. The manual steps remain useful for code-level checks
|
|
3403
|
+
> the manifest validator can't see (permission usage, surface wiring, sandbox
|
|
3404
|
+
> compliance).
|
|
3021
3405
|
|
|
3022
3406
|
## 1. Manifest validation
|
|
3023
3407
|
Read \`packages/extension/public/manifest.json\` and verify:
|
|
@@ -3037,9 +3421,7 @@ Scan all \`.tsx\` files in \`packages/extension/src/\` for capability usage:
|
|
|
3037
3421
|
- \`capabilities.actions.toast\` \u2192 needs \`actions:toast\` permission
|
|
3038
3422
|
- \`capabilities.actions.invoke\` \u2192 needs \`actions:invoke\` permission
|
|
3039
3423
|
- \`useExtendIdentity\` \u2192 needs \`extend:identity\` permission
|
|
3040
|
-
|
|
3041
|
-
- \`useMessagingEvent\` \u2192 needs \`events:messaging\` permission (also check manifest \`events\` array has matching entries)
|
|
3042
|
-
- \`useActivityEvent\` \u2192 needs \`events:activity\` permission (also check manifest \`events\` array has matching entries)
|
|
3424
|
+
${eventHookBullets}
|
|
3043
3425
|
|
|
3044
3426
|
Report:
|
|
3045
3427
|
- **Missing permissions:** capabilities used in code but not declared in manifest
|
|
@@ -3059,6 +3441,202 @@ Verify that:
|
|
|
3059
3441
|
## 5. Summary
|
|
3060
3442
|
Print a summary: total issues found, categorized by severity (error vs warning).
|
|
3061
3443
|
Errors must be fixed before deploying. Warnings are recommendations.
|
|
3444
|
+
|
|
3445
|
+
## Next: deploy
|
|
3446
|
+
|
|
3447
|
+
Once validation passes, build the production bundle, host it, and register the
|
|
3448
|
+
Bundle URL with Stackable. See the [Deploy guide](/docs/guides/deploy).
|
|
3449
|
+
`;
|
|
3450
|
+
};
|
|
3451
|
+
|
|
3452
|
+
// ../../sdk/extension/ai-docs/src/commands/deploy-extension.ts
|
|
3453
|
+
var generateDeployExtensionCommand = () => {
|
|
3454
|
+
const fm = frontmatter({
|
|
3455
|
+
description: "Build, host, and register the production Bundle URL for this Stackable extension",
|
|
3456
|
+
targets: ["*"]
|
|
3457
|
+
});
|
|
3458
|
+
return `${fm}
|
|
3459
|
+
|
|
3460
|
+
# Deploy
|
|
3461
|
+
|
|
3462
|
+
Stackable hosts the marketplace, the proxy, and the runtime \u2014 but **you host
|
|
3463
|
+
your extension's bundle/runtime**. Deployment is three steps: build for production,
|
|
3464
|
+
upload to a host of your choice, then point your extension's **Bundle URL** at
|
|
3465
|
+
the result.
|
|
3466
|
+
|
|
3467
|
+
## 1. Build the production bundle
|
|
3468
|
+
|
|
3469
|
+
From the project root:
|
|
3470
|
+
|
|
3471
|
+
\`\`\`bash
|
|
3472
|
+
pnpm build
|
|
3473
|
+
\`\`\`
|
|
3474
|
+
|
|
3475
|
+
This runs Vite in production mode and writes the static bundle to
|
|
3476
|
+
\`packages/extension/dist/\` \u2014 a small set of JS/CSS files plus your
|
|
3477
|
+
\`manifest.json\`. That's the artifact you ship.
|
|
3478
|
+
|
|
3479
|
+
> **Tip:** Validate the build before you upload it. Run through the
|
|
3480
|
+
> [Validate guide](/docs/guides/validate) (or ask your AI client to call the
|
|
3481
|
+
> \`validate_manifest\` MCP tool) so manifest typos and missing permissions are
|
|
3482
|
+
> caught before they reach the marketplace.
|
|
3483
|
+
|
|
3484
|
+
## 2. Upload to a hosting provider
|
|
3485
|
+
|
|
3486
|
+
Stackable doesn't care where the bundle lives \u2014 only that it's served over
|
|
3487
|
+
HTTPS with permissive CORS so the host runtime can fetch it. Anything that
|
|
3488
|
+
serves static files works. The most common options:
|
|
3489
|
+
|
|
3490
|
+
| Provider | Notes |
|
|
3491
|
+
|----------|-------|
|
|
3492
|
+
| **Netlify** | Drop \`packages/extension/dist/\` into a site, or wire up Git auto-deploys. CORS is already permissive. |
|
|
3493
|
+
| **Vercel** | Same idea \u2014 link the repo and let Vercel build/deploy on push. Set the build output to \`packages/extension/dist\`. |
|
|
3494
|
+
| **Cloudflare Pages** | Connect the repo, set the output directory, deploys land on the edge globally. |
|
|
3495
|
+
| **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). |
|
|
3496
|
+
| **Your own CDN / object storage** | Anything that returns \`200 OK\` with the right \`Content-Type\` and CORS headers will work. |
|
|
3497
|
+
|
|
3498
|
+
Whichever you pick, the only requirements are:
|
|
3499
|
+
|
|
3500
|
+
- Bundle is reachable over **HTTPS**
|
|
3501
|
+
- CORS allows the host application's origin (most providers do this by default; S3+CloudFront is the one that usually needs explicit configuration)
|
|
3502
|
+
- The URL is **stable** \u2014 every Instance running your extension fetches from this URL on load
|
|
3503
|
+
|
|
3504
|
+
> **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).
|
|
3505
|
+
|
|
3506
|
+
## 3. Register your Bundle URL with Stackable
|
|
3507
|
+
|
|
3508
|
+
Once your extension bundle is deployed/live, tell Stackable where to find it. You have two options:
|
|
3509
|
+
|
|
3510
|
+
- **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.
|
|
3511
|
+
- **CLI** \u2014 from the project directory:
|
|
3512
|
+
|
|
3513
|
+
\`\`\`bash
|
|
3514
|
+
${CLI.update} --bundle-url https://your-host.example.com/manifest.json
|
|
3515
|
+
\`\`\`
|
|
3516
|
+
|
|
3517
|
+
Or, with an explicit extension ID (handy for CI/CD):
|
|
3518
|
+
|
|
3519
|
+
\`\`\`bash
|
|
3520
|
+
${CLI.update} ext_xxx --bundle-url https://your-host.example.com/manifest.json
|
|
3521
|
+
\`\`\`
|
|
3522
|
+
|
|
3523
|
+
Point the URL at your bundle's \`manifest.json\` \u2014 that's the entrypoint the
|
|
3524
|
+
runtime fetches first; everything else is loaded relative to it.
|
|
3525
|
+
|
|
3526
|
+
## 4. Roll out and iterate
|
|
3527
|
+
|
|
3528
|
+
That's the entire deploy loop. Subsequent updates follow the same path: bump
|
|
3529
|
+
\`version\` in \`manifest.json\`, run \`pnpm build\`, re-upload, and (only if the
|
|
3530
|
+
URL changed) re-register. Most teams wire steps 1\u20133 into a single CI/CD job
|
|
3531
|
+
that runs on every merge to \`main\`.
|
|
3532
|
+
|
|
3533
|
+
## Next: list it on the Marketplace
|
|
3534
|
+
|
|
3535
|
+
Deploying makes your extension **runnable** at a stable URL. To make it
|
|
3536
|
+
**installable** by other organizations, you also need to fill in the
|
|
3537
|
+
marketplace listing (icon, screenshots, description, visibility) and submit
|
|
3538
|
+
for review. See [Marketplace Listing](/docs/reference/marketplace-listing).
|
|
3539
|
+
`;
|
|
3540
|
+
};
|
|
3541
|
+
|
|
3542
|
+
// ../../sdk/extension/ai-docs/src/commands/marketplace-listing.ts
|
|
3543
|
+
var generateMarketplaceListing = () => {
|
|
3544
|
+
const fm = frontmatter({
|
|
3545
|
+
description: "Fill in your marketplace listing, choose visibility, and submit your Stackable extension for review and approval",
|
|
3546
|
+
targets: ["*"]
|
|
3547
|
+
});
|
|
3548
|
+
return `${fm}
|
|
3549
|
+
|
|
3550
|
+
# Listing & Submission
|
|
3551
|
+
|
|
3552
|
+
Once your extension is built, hosted, and registered with a Bundle URL (see
|
|
3553
|
+
[Deploy](/docs/guides/deploy)), the last step is to make it discoverable and
|
|
3554
|
+
installable. That happens entirely in the [admin dashboard](https://admin.stackablelabs.com)
|
|
3555
|
+
\u2014 you fill in your marketplace listing, pick a visibility mode, and submit for
|
|
3556
|
+
review.
|
|
3557
|
+
|
|
3558
|
+
## 1. Fill in the listing fields
|
|
3559
|
+
|
|
3560
|
+
Open your extension in the admin dashboard and complete the **Listing** tab.
|
|
3561
|
+
Every field below is part of the marketplace metadata that prospective
|
|
3562
|
+
installers see when browsing or evaluating your extension.
|
|
3563
|
+
|
|
3564
|
+
| Field | Required | What it's for |
|
|
3565
|
+
|-------|----------|---------------|
|
|
3566
|
+
| **Icon** | Yes | Square image rendered on browse cards and the detail page. Square aspect ratio, transparent background recommended. |
|
|
3567
|
+
| **Short description** | Yes | One-line summary used on browse cards and search results. Keep it under ~120 characters. |
|
|
3568
|
+
| **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. |
|
|
3569
|
+
| **Screenshots** | Recommended | Image gallery shown on the detail page. Pick views that demonstrate the extension actually working in a host application. |
|
|
3570
|
+
| **Categories** | Yes | One or more curated categories from the platform taxonomy (e.g., commerce, support, analytics). Drives marketplace browse filters. |
|
|
3571
|
+
| **Tags** | Optional | Freeform searchable tags. Used for keyword search across the marketplace. |
|
|
3572
|
+
| **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. |
|
|
3573
|
+
|
|
3574
|
+
> **Tip:** The icon and screenshots are what make a listing feel real. A
|
|
3575
|
+
> placeholder icon and no screenshots is the single biggest reason listings
|
|
3576
|
+
> get a "needs more info" review response.
|
|
3577
|
+
|
|
3578
|
+
## 2. Choose a visibility mode
|
|
3579
|
+
|
|
3580
|
+
Every listing has a **visibility** setting that controls who can install it:
|
|
3581
|
+
|
|
3582
|
+
| Visibility | Who can find and install it |
|
|
3583
|
+
|------------|------------------------------|
|
|
3584
|
+
| **Public** | Anyone browsing the marketplace. Goes through full marketplace review. Best for extensions you want available to everyone. |
|
|
3585
|
+
| **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. |
|
|
3586
|
+
|
|
3587
|
+
Visibility can be changed later from the same Listing tab. Switching from
|
|
3588
|
+
**Protected** \u2192 **Public** triggers a fresh marketplace review.
|
|
3589
|
+
|
|
3590
|
+
## 3. Submit for review
|
|
3591
|
+
|
|
3592
|
+
Click **Submit for review**. The platform runs a two-stage review:
|
|
3593
|
+
|
|
3594
|
+
### Stage 1 \u2014 Automated Bundle scan
|
|
3595
|
+
|
|
3596
|
+
Runs immediately on submit. The platform fetches your bundle from the
|
|
3597
|
+
registered Bundle URL and runs deterministic checks:
|
|
3598
|
+
|
|
3599
|
+
- Bundle reachable, valid \`manifest.json\`, sane bundle size
|
|
3600
|
+
- Declared permissions match observed capability usage
|
|
3601
|
+
- Declared targets match the surfaces your code actually registers
|
|
3602
|
+
- Declared \`allowedDomains\` match the endpoints \`data.fetch\` actually hits
|
|
3603
|
+
- No use of forbidden browser globals (\`document\`, \`window.location\`, etc.)
|
|
3604
|
+
|
|
3605
|
+
Findings come back categorized as **error**, **warning**, or **info**. Errors
|
|
3606
|
+
must be fixed before the listing can move forward. Warnings don't block
|
|
3607
|
+
submission but factor into the review.
|
|
3608
|
+
|
|
3609
|
+
### Stage 2 \u2014 Advanced review + Stackable approval
|
|
3610
|
+
|
|
3611
|
+
If Stage 1 passes, the platform runs an advanced review of your bundle \u2014
|
|
3612
|
+
framework analysis, permission surface, network behavior, and any
|
|
3613
|
+
security-relevant findings get scored and assessed against your listing
|
|
3614
|
+
content. From there, Stackable will either:
|
|
3615
|
+
|
|
3616
|
+
- **Approve** \u2014 your listing moves to **Published** and becomes installable.
|
|
3617
|
+
- **Request changes** \u2014 you'll see structured feedback on the listing page. Address it, re-deploy if needed, and resubmit.
|
|
3618
|
+
|
|
3619
|
+
Most reviews complete within a few business days. You'll get an email when
|
|
3620
|
+
the status changes; the listing's review status is also visible in the admin
|
|
3621
|
+
dashboard at all times.
|
|
3622
|
+
|
|
3623
|
+
## Review status lifecycle
|
|
3624
|
+
|
|
3625
|
+
| Status | Meaning |
|
|
3626
|
+
|--------|---------|
|
|
3627
|
+
| **Draft** | Listing is being edited; not submitted. |
|
|
3628
|
+
| **Pending review** | Submitted; waiting on Stage 1 + Stage 2 + Stackable approval. |
|
|
3629
|
+
| **Rejected** | Stackable requested changes. Address the feedback and resubmit. |
|
|
3630
|
+
| **Published** | Live in the marketplace. Installable per your visibility setting. |
|
|
3631
|
+
| **Suspended** | Pulled from the marketplace by Stackable (security issue, ToS violation, etc.). Contact support. |
|
|
3632
|
+
|
|
3633
|
+
## After publishing
|
|
3634
|
+
|
|
3635
|
+
Re-deploys to the same Bundle URL don't require re-submission \u2014 your existing
|
|
3636
|
+
Instances pick up the new bundle automatically. **Listing edits** (description,
|
|
3637
|
+
screenshots, categories, visibility) and **major version bumps** do go through
|
|
3638
|
+
review again. Patch updates that don't change permissions, targets, or
|
|
3639
|
+
\`allowedDomains\` typically clear review quickly.
|
|
3062
3640
|
`;
|
|
3063
3641
|
};
|
|
3064
3642
|
|
|
@@ -3152,6 +3730,12 @@ var SKILLS = [
|
|
|
3152
3730
|
type: "knowledge",
|
|
3153
3731
|
content: () => generateStylingAndTheming()
|
|
3154
3732
|
},
|
|
3733
|
+
{
|
|
3734
|
+
id: "instance-settings",
|
|
3735
|
+
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.",
|
|
3736
|
+
type: "knowledge",
|
|
3737
|
+
content: () => generateInstanceSettings()
|
|
3738
|
+
},
|
|
3155
3739
|
// ── Docs-only knowledge skills ──────────────────────────────
|
|
3156
3740
|
{
|
|
3157
3741
|
id: "quick-start",
|
|
@@ -3181,6 +3765,13 @@ var SKILLS = [
|
|
|
3181
3765
|
scopes: ["docs"],
|
|
3182
3766
|
content: () => generateExtensionStudio()
|
|
3183
3767
|
},
|
|
3768
|
+
{
|
|
3769
|
+
id: "ai-accelerated-development",
|
|
3770
|
+
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.",
|
|
3771
|
+
type: "knowledge",
|
|
3772
|
+
scopes: ["docs"],
|
|
3773
|
+
content: () => generateAIAcceleratedDevelopment()
|
|
3774
|
+
},
|
|
3184
3775
|
// ── Cookbook skills (docs-only) ────────────────────────────────
|
|
3185
3776
|
{
|
|
3186
3777
|
id: "cookbook-structural",
|
|
@@ -3203,6 +3794,13 @@ var SKILLS = [
|
|
|
3203
3794
|
scopes: ["docs"],
|
|
3204
3795
|
content: () => generateCookbookEvents()
|
|
3205
3796
|
},
|
|
3797
|
+
{
|
|
3798
|
+
id: "marketplace-listing",
|
|
3799
|
+
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.",
|
|
3800
|
+
type: "knowledge",
|
|
3801
|
+
scopes: ["docs"],
|
|
3802
|
+
content: () => generateMarketplaceListing()
|
|
3803
|
+
},
|
|
3206
3804
|
// ── Action skills ─────────────────────────────────────────────
|
|
3207
3805
|
{
|
|
3208
3806
|
id: "add-surface",
|
|
@@ -3227,6 +3825,15 @@ var SKILLS = [
|
|
|
3227
3825
|
description: "Validate this extension for common errors before deploying: manifest, permissions, surfaces, imports. Use before publishing or when debugging issues.",
|
|
3228
3826
|
type: "action",
|
|
3229
3827
|
content: () => generateValidateExtensionCommand()
|
|
3828
|
+
},
|
|
3829
|
+
{
|
|
3830
|
+
id: "deploy",
|
|
3831
|
+
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.",
|
|
3832
|
+
type: "action",
|
|
3833
|
+
// TODO: T3 #24 — remove `scopes: ['docs']` once `cli deploy` is implemented so Studio's
|
|
3834
|
+
// AI assistant in the future should be able invoke the deploy flow (not just describe it).
|
|
3835
|
+
scopes: ["docs"],
|
|
3836
|
+
content: () => generateDeployExtensionCommand()
|
|
3230
3837
|
}
|
|
3231
3838
|
];
|
|
3232
3839
|
|
|
@@ -3828,6 +4435,64 @@ pair(EXAMPLE_SNIPPETS, EXAMPLE_SNIPPETS2);
|
|
|
3828
4435
|
pair(CAPABILITY_SNIPPETS, CAPABILITY_SNIPPETS2);
|
|
3829
4436
|
pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
|
|
3830
4437
|
|
|
4438
|
+
// ../../lib/contracts/src/custom.ts
|
|
4439
|
+
var CUSTOM_ROLE = {
|
|
4440
|
+
SUPER_ADMIN: "super_admin"
|
|
4441
|
+
};
|
|
4442
|
+
new Set(Object.values(CUSTOM_ROLE));
|
|
4443
|
+
|
|
4444
|
+
// ../../lib/utils-auth/src/constants.ts
|
|
4445
|
+
var STANDALONE_CLIENT_DATA = {
|
|
4446
|
+
CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
|
|
4447
|
+
MCP: { name: "@stackable-labs/mcp-app-extension", authFile: "mcp-auth.json" }
|
|
4448
|
+
};
|
|
4449
|
+
var STANDALONE_CLIENT = Object.fromEntries(
|
|
4450
|
+
Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.name])
|
|
4451
|
+
);
|
|
4452
|
+
var STANDALONE_CLIENT_AUTH_FILE = Object.fromEntries(
|
|
4453
|
+
Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.authFile])
|
|
4454
|
+
);
|
|
4455
|
+
Object.values(STANDALONE_CLIENT);
|
|
4456
|
+
|
|
4457
|
+
// ../../lib/utils-auth/src/index.ts
|
|
4458
|
+
var AUTH_DIR = join(homedir(), ".stackable");
|
|
4459
|
+
join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.CLI);
|
|
4460
|
+
var MCP_AUTH_FILE = join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.MCP);
|
|
4461
|
+
var resolveAuthFile = (filename) => join(AUTH_DIR, filename ?? STANDALONE_CLIENT_AUTH_FILE.CLI);
|
|
4462
|
+
var readAuthState = async (filename) => {
|
|
4463
|
+
try {
|
|
4464
|
+
const content = await readFile(resolveAuthFile(filename), "utf8");
|
|
4465
|
+
return JSON.parse(content);
|
|
4466
|
+
} catch {
|
|
4467
|
+
return null;
|
|
4468
|
+
}
|
|
4469
|
+
};
|
|
4470
|
+
var decodeJwtPayload = (token) => {
|
|
4471
|
+
try {
|
|
4472
|
+
const [, payload] = token.split(".");
|
|
4473
|
+
if (!payload) {
|
|
4474
|
+
return null;
|
|
4475
|
+
}
|
|
4476
|
+
const json = Buffer.from(payload, "base64url").toString("utf8");
|
|
4477
|
+
return JSON.parse(json);
|
|
4478
|
+
} catch {
|
|
4479
|
+
return null;
|
|
4480
|
+
}
|
|
4481
|
+
};
|
|
4482
|
+
var getToken = async (filename) => {
|
|
4483
|
+
const state = await readAuthState(filename);
|
|
4484
|
+
if (!state) {
|
|
4485
|
+
throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
|
|
4486
|
+
}
|
|
4487
|
+
const payload = decodeJwtPayload(state.token);
|
|
4488
|
+
if (payload?.exp && typeof payload.exp === "number") {
|
|
4489
|
+
if (Date.now() >= payload.exp * 1e3) {
|
|
4490
|
+
throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
return state.token;
|
|
4494
|
+
};
|
|
4495
|
+
|
|
3831
4496
|
// package.json
|
|
3832
4497
|
var package_default = {
|
|
3833
4498
|
version: "0.0.0"};
|
|
@@ -3851,9 +4516,13 @@ var createMcpServer = (options = {}) => {
|
|
|
3851
4516
|
return await getToken(STANDALONE_CLIENT_AUTH_FILE.MCP);
|
|
3852
4517
|
} catch {
|
|
3853
4518
|
}
|
|
3854
|
-
|
|
4519
|
+
if (!options.performOAuthFlow) {
|
|
4520
|
+
throw new Error(
|
|
4521
|
+
"No auth token available. Provide authContext (server) or performOAuthFlow (CLI)."
|
|
4522
|
+
);
|
|
4523
|
+
}
|
|
3855
4524
|
const MCP_API_BASE_URL = process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_API_BASE_URL;
|
|
3856
|
-
return performOAuthFlow(`${MCP_API_BASE_URL}/.well-known/oauth-authorization-server`);
|
|
4525
|
+
return options.performOAuthFlow(`${MCP_API_BASE_URL}/.well-known/oauth-authorization-server`);
|
|
3857
4526
|
};
|
|
3858
4527
|
const authHeaders = (token) => ({
|
|
3859
4528
|
authorization: `Bearer ${token}`,
|
|
@@ -3990,12 +4659,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
|
3990
4659
|
usedPermissions.add(permission);
|
|
3991
4660
|
}
|
|
3992
4661
|
}
|
|
3993
|
-
const
|
|
3994
|
-
useIdentityEvent: "events:identity",
|
|
3995
|
-
useMessagingEvent: "events:messaging",
|
|
3996
|
-
useActivityEvent: "events:activity"
|
|
3997
|
-
};
|
|
3998
|
-
for (const [hook, permission] of Object.entries(eventHookMap)) {
|
|
4662
|
+
for (const [hook, permission] of Object.entries(EVENT_HOOK_PERMISSION_MAP)) {
|
|
3999
4663
|
if (allSource.includes(hook)) {
|
|
4000
4664
|
usedPermissions.add(permission);
|
|
4001
4665
|
}
|