@sentientui/mcp 0.17.7 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -0
- package/README.md +5 -0
- package/dist/{chunk-5PR2LCNN.js → chunk-HEMPCGWX.js} +224 -9
- package/dist/index.cjs +224 -9
- package/dist/index.js +1 -1
- package/dist/lib.cjs +224 -9
- package/dist/lib.js +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -43,6 +43,9 @@ Server keys start with `sk_` — required for the management API. Public keys (`
|
|
|
43
43
|
| `get_variant_brief` | **Start here to optimize a component.** Insight-driven brief for writing a new CODE-NATIVE variant: performance, audience, insights, data-sufficiency + best-practice fallback, and code instructions |
|
|
44
44
|
| `create_variant` | Creates a no-code **managed text** DRAFT — user must activate in dashboard. Fallback only; not for code-native variants |
|
|
45
45
|
| `pause_variant` | Stops traffic; no MCP resume |
|
|
46
|
+
| `get_cell_matrix` | "Who sees what": per-(visitor type, region) generation state + traffic share. A missing cell = they see the original |
|
|
47
|
+
| `get_cell_detail` | One cell's generated option, its stored rationale, and a plain-language performance verdict |
|
|
48
|
+
| `generate_cell` | Queues async AI generation for one cell (also retries failed ones). Paid plan; check `get_cell_matrix` for progress |
|
|
46
49
|
|
|
47
50
|
## Canonical Workflows
|
|
48
51
|
|
package/README.md
CHANGED
|
@@ -101,7 +101,12 @@ A sandboxed demo token is provisioned automatically — 10 calls/month, read-onl
|
|
|
101
101
|
| `get_variant_brief` | Insight-driven brief for writing a new **code-native** variant: performance, audience, insights, data-sufficiency (with a best-practice fallback when there's no data yet), and step-by-step code instructions |
|
|
102
102
|
| `create_variant` | Create a no-code (managed) text variant (Starter+) — fallback for text-only variants without a code change |
|
|
103
103
|
| `pause_variant` | Pause a variant to stop traffic assignment |
|
|
104
|
+
| `get_cell_matrix` | The "Who sees what" matrix: which visitor types have an AI-generated version live in each personalizable region, with traffic share and auto-fill status |
|
|
105
|
+
| `get_cell_detail` | One cell's story: the generated option, why it was written (stored rationale), and how it performs vs the original |
|
|
106
|
+
| `generate_cell` | Queue AI generation of a version for one (visitor type, region) cell — brand-locked, live in under a minute (paid plan) |
|
|
104
107
|
|
|
108
|
+
> **Generated versions.** An `<Adaptive id="…">` that wraps your original as children (no `variants`) registers as a personalizable region once it is mounted and deployed. Its versions per visitor type are written in SentientUI — inspect and fill them with `get_cell_matrix`, `get_cell_detail`, and `generate_cell` — with no code change or redeploy.
|
|
109
|
+
>
|
|
105
110
|
> **Code-native vs no-code variants.** Variants you declare in your app (`<Adaptive variants={{…}}>`) register automatically the first time the SDK requests an assignment after you deploy — they go live immediately and need **no** `create_variant` call. Use `create_variant` only for **no-code** variants whose content is stored in SentientUI and rendered without a code change; these start as drafts you activate from the dashboard.
|
|
106
111
|
>
|
|
107
112
|
> **Optimizing a component?** Call `get_variant_brief` first. It returns the component's performance, audience, insights, and a data-sufficiency assessment — then your AI assistant writes a new on-brand variant directly into your code (which auto-registers on deploy). When there's no data yet, the brief falls back to best-practice priors for your project's context type so the assistant still makes a sensible change.
|
|
@@ -1789,8 +1789,9 @@ import { z as z13 } from "zod";
|
|
|
1789
1789
|
var GUIDE = `# SentientUI integration guide \u2014 the adaptive ladder
|
|
1790
1790
|
|
|
1791
1791
|
SentientUI adapts a site per visitor type (personas), learning from real conversions. Each
|
|
1792
|
-
project has its own persona vocabulary (dashboard -> Settings -> Personas
|
|
1793
|
-
|
|
1792
|
+
project has its own persona vocabulary (dashboard -> Settings -> Personas), and it starts
|
|
1793
|
+
EMPTY: every visitor is 'unknown' until a persona exists, and the optimizer learns across all
|
|
1794
|
+
traffic pooled in the meantime. Personas are DISCOVERED from behavior \u2014 or, far faster,
|
|
1794
1795
|
DECLARED by the app when it already knows the visitor's role: init({ persona: 'admin' }) /
|
|
1795
1796
|
<AdaptiveProvider persona=...> / window.sentient.persona. Declared beats inferred and serves
|
|
1796
1797
|
at full confidence; unrecognized values are ignored server-side and surfaced in the dashboard
|
|
@@ -1807,8 +1808,9 @@ time.
|
|
|
1807
1808
|
other React apps use <AdaptiveProvider> from '@sentientui/react') and add
|
|
1808
1809
|
suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes pre-paint.
|
|
1809
1810
|
Nothing adapts and nothing is tracked until this wrap is in place.
|
|
1810
|
-
3. \`npm run dev\`, then open the app with \`?sentient_persona=
|
|
1811
|
-
\`?sentient_persona=
|
|
1811
|
+
3. \`npm run dev\`, then open the app with \`?sentient_persona=a\` vs
|
|
1812
|
+
\`?sentient_persona=b\` to see it adapt \u2014 in keyless local mode any key drives the built-in
|
|
1813
|
+
heuristic. No API key needed. Against a real project the key must be one you declared.
|
|
1812
1814
|
4. To learn from real traffic: create a project at https://sentient-ui.com and set
|
|
1813
1815
|
NEXT_PUBLIC_SENTIENT_API_KEY=pk_... in .env.local.
|
|
1814
1816
|
|
|
@@ -1817,8 +1819,8 @@ time.
|
|
|
1817
1819
|
Persona attributes on <html> (zero declaration; the attribute carries the project's own
|
|
1818
1820
|
vocabulary keys \u2014 declared or inferred):
|
|
1819
1821
|
|
|
1820
|
-
html[data-sentient-persona='
|
|
1821
|
-
html[data-sentient-confidence='low'] .
|
|
1822
|
+
html[data-sentient-persona='admin'] .admin-tools { display: block; }
|
|
1823
|
+
html[data-sentient-confidence='low'] .admin-tools { display: none; }
|
|
1822
1824
|
|
|
1823
1825
|
Learned style tokens (element-scoped, SSR-safe):
|
|
1824
1826
|
|
|
@@ -1833,14 +1835,35 @@ prefers-reduced-motion: reduce override in CSS.
|
|
|
1833
1835
|
|
|
1834
1836
|
## Rung 2 \u2014 Swap (alternate content)
|
|
1835
1837
|
|
|
1838
|
+
Generated versions (start here) \u2014 wrap what the page shows today; the children are the
|
|
1839
|
+
original:
|
|
1840
|
+
|
|
1841
|
+
<Adaptive id="hero-cta" goal="signup_click">
|
|
1842
|
+
<a href="/signup">Start free trial</a>
|
|
1843
|
+
</Adaptive>
|
|
1844
|
+
|
|
1845
|
+
Mount + deploy registers the region. SentientUI writes versions per visitor type in the
|
|
1846
|
+
dashboard ("Who sees what") \u2014 no redeploy. The children render for holdout traffic, unknown
|
|
1847
|
+
visitor types, types with no version yet, and every error path. No server preload: mounted
|
|
1848
|
+
regions are requested in one batched call. First visit shows the original briefly, then swaps;
|
|
1849
|
+
return visits start from the last version. Keyless local mode renders originals only. Pass
|
|
1850
|
+
onFormSubmit to allow form versions; set reportBaselineText={false} on regions wrapping
|
|
1851
|
+
personalized or account content. (Formerly <AdaptiveSlot>.)
|
|
1852
|
+
|
|
1853
|
+
Code variants \u2014 when you write the alternatives yourself:
|
|
1854
|
+
|
|
1855
|
+
<Adaptive id="buy-box" goal="buy_click" // goal REQUIRED
|
|
1856
|
+
variants={{ control: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }} /> // first key = baseline
|
|
1857
|
+
|
|
1858
|
+
// hook form:
|
|
1836
1859
|
const { value, bind } = useAdaptive('buy-box', {
|
|
1837
1860
|
variants: { calm: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }, // first key = baseline
|
|
1838
1861
|
goal: 'buy_click', // REQUIRED
|
|
1839
1862
|
});
|
|
1840
1863
|
return <div {...bind}>{value}</div>;
|
|
1841
1864
|
|
|
1842
|
-
|
|
1843
|
-
|
|
1865
|
+
Children or variants on one <Adaptive>, never both. With the hook, always attach bind \u2014 it
|
|
1866
|
+
wires exposure tracking and goal listeners. <AdaptiveText> swaps dashboard-managed text.
|
|
1844
1867
|
|
|
1845
1868
|
## Rung 3 \u2014 Reorder (structure)
|
|
1846
1869
|
|
|
@@ -1856,6 +1879,23 @@ useLayoutOrder(). Non-React sites: sections: ['#hero', '#pricing', '#faq'] (CSS
|
|
|
1856
1879
|
the theme's natural order) in window.sentient \u2014 the snippet reorders those elements within
|
|
1857
1880
|
their shared parent, and applies nothing if any selector drifts.
|
|
1858
1881
|
|
|
1882
|
+
## No-code install order (snippet)
|
|
1883
|
+
|
|
1884
|
+
Two tags in <head>, in this order: one inline <script> that assigns the window.sentient config
|
|
1885
|
+
and then, in the same tag after the ';', runs the inline pre-paint script; then the deferred
|
|
1886
|
+
loader. The pre-paint script reads window.sentient when it runs, so sharing the tag is safe. It
|
|
1887
|
+
is optional but recommended \u2014 the loader is deferred, so on a return visit the page can paint
|
|
1888
|
+
its original state before the loader runs; the inline script applies the last served decision
|
|
1889
|
+
from the visitor's own device first, with no network call and no cloak. Get the exact bytes from
|
|
1890
|
+
the dashboard's Install page, or from renderSnippetInstall({ config }) in
|
|
1891
|
+
'@sentientui/snippet/install' (renderSnippetPrePaintScript() for the script alone).
|
|
1892
|
+
|
|
1893
|
+
Strict Content-Security-Policy (script hashes): use the three-tag form instead \u2014 config, the
|
|
1894
|
+
pre-paint script alone, then the loader (renderSnippetInstall({ config, split: true })). The
|
|
1895
|
+
combined tag contains the site's config, so its hash differs per site; the split pre-paint tag is
|
|
1896
|
+
byte-identical everywhere, so one hash covers it. Existing three-tag and config + loader installs
|
|
1897
|
+
keep working.
|
|
1898
|
+
|
|
1859
1899
|
## Testing the integration
|
|
1860
1900
|
|
|
1861
1901
|
Use '@sentientui/react/testing': renderWithSentient(ui, { variants, slots, persona }) forces
|
|
@@ -2017,8 +2057,182 @@ function registerAgentTrafficTools(server, client) {
|
|
|
2017
2057
|
);
|
|
2018
2058
|
}
|
|
2019
2059
|
|
|
2060
|
+
// src/tools/cells.ts
|
|
2061
|
+
import { z as z15 } from "zod";
|
|
2062
|
+
var personaOutput = z15.object({
|
|
2063
|
+
key: z15.string(),
|
|
2064
|
+
displayName: z15.string(),
|
|
2065
|
+
description: z15.string().nullable(),
|
|
2066
|
+
share: z15.number().describe("Fraction of 30-day decided sessions")
|
|
2067
|
+
});
|
|
2068
|
+
var cellOutput = z15.object({
|
|
2069
|
+
slotId: z15.string(),
|
|
2070
|
+
persona: z15.string(),
|
|
2071
|
+
status: z15.string().describe("pending | generating | live | error | rejected"),
|
|
2072
|
+
requestedBy: z15.string(),
|
|
2073
|
+
armId: z15.string().nullable(),
|
|
2074
|
+
rationale: z15.string().nullable(),
|
|
2075
|
+
error: z15.string().nullable(),
|
|
2076
|
+
updatedAt: z15.string(),
|
|
2077
|
+
// These ride the API response too; omitting them from the schema made zod
|
|
2078
|
+
// STRIP them, so agents could never see refinement or retry state.
|
|
2079
|
+
refinedFrom: z15.string().nullable().optional().describe("Incumbent arm this cell is improving on \u2014 a pending cell with this set still SERVES the incumbent"),
|
|
2080
|
+
notBefore: z15.string().nullable().optional().describe("ISO time the queue will next consider this cell (retry backoff / daily-cap deferral); null when due now"),
|
|
2081
|
+
attempts: z15.number().optional().describe("Generation attempts so far"),
|
|
2082
|
+
performance: z15.object({
|
|
2083
|
+
shown: z15.number(),
|
|
2084
|
+
conversions: z15.number(),
|
|
2085
|
+
deltaPct: z15.number().nullable().describe("Signed % vs the original conversion rate"),
|
|
2086
|
+
tier: z15.string().describe("early | no_baseline | winning | losing | flat")
|
|
2087
|
+
}).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
|
|
2088
|
+
});
|
|
2089
|
+
function registerCellTools(server, client) {
|
|
2090
|
+
server.registerTool(
|
|
2091
|
+
"get_cell_matrix",
|
|
2092
|
+
{
|
|
2093
|
+
title: "Persona \xD7 slot cell matrix",
|
|
2094
|
+
description: 'The "Who sees what" matrix: which visitor types (personas) have an AI-generated version live in each personalizable region (slot), with per-persona traffic share and the first-miss auto-fill setting. A cell with no row means that visitor type sees the original.' + UNTRUSTED_FIELDS_NOTE,
|
|
2095
|
+
inputSchema: { projectId: projectIdSchema },
|
|
2096
|
+
outputSchema: {
|
|
2097
|
+
slots: z15.array(
|
|
2098
|
+
z15.object({
|
|
2099
|
+
slotId: z15.string(),
|
|
2100
|
+
displayName: z15.string(),
|
|
2101
|
+
supportsForms: z15.boolean(),
|
|
2102
|
+
// API sends this; without it zod stripped the draft-vs-live signal.
|
|
2103
|
+
live: z15.boolean().optional().describe("False for an sdk-registered draft column \u2014 nothing serves until its first generation publishes it")
|
|
2104
|
+
})
|
|
2105
|
+
),
|
|
2106
|
+
personas: z15.array(personaOutput),
|
|
2107
|
+
cells: z15.array(cellOutput),
|
|
2108
|
+
autoFill: z15.boolean(),
|
|
2109
|
+
personaSource: z15.string().describe("Active persona-set source: default | declared | discovered"),
|
|
2110
|
+
undecidedShare: z15.number()
|
|
2111
|
+
},
|
|
2112
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2113
|
+
},
|
|
2114
|
+
withApiErrorGuidance(async ({ projectId }) => {
|
|
2115
|
+
const id = encodeURIComponent(projectId);
|
|
2116
|
+
const data = await client.get(`/projects/${id}/cells`);
|
|
2117
|
+
const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
|
|
2118
|
+
const lines = [
|
|
2119
|
+
`Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
|
|
2120
|
+
`Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
|
|
2121
|
+
];
|
|
2122
|
+
for (const p of data.personas) {
|
|
2123
|
+
const states = data.slots.map((s) => {
|
|
2124
|
+
const c = byCell.get(`${s.slotId}::${p.key}`);
|
|
2125
|
+
let state = c ? c.status : "original";
|
|
2126
|
+
if (c && (c.status === "pending" || c.status === "generating") && c.refinedFrom) {
|
|
2127
|
+
state = `${c.status} (improving on ${untrusted(c.refinedFrom, 64)} \u2014 current version still live)`;
|
|
2128
|
+
} else if (c && c.status === "pending" && c.notBefore && Date.parse(c.notBefore) > Date.now()) {
|
|
2129
|
+
state = `pending (waiting until ${c.notBefore})`;
|
|
2130
|
+
}
|
|
2131
|
+
return `${untrusted(s.displayName, 40)}: ${state}`;
|
|
2132
|
+
});
|
|
2133
|
+
lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
|
|
2134
|
+
}
|
|
2135
|
+
lines.push(`Undecided visitors (${Math.round(data.undecidedShare * 100)}%) always see the original.`);
|
|
2136
|
+
return {
|
|
2137
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2138
|
+
structuredContent: data
|
|
2139
|
+
};
|
|
2140
|
+
})
|
|
2141
|
+
);
|
|
2142
|
+
server.registerTool(
|
|
2143
|
+
"get_cell_detail",
|
|
2144
|
+
{
|
|
2145
|
+
title: "Cell story",
|
|
2146
|
+
description: "One cell's full story: the generated option a visitor type sees in a slot, why it was written (the stored generation rationale), and how it is performing vs the original (deterministic plain-language verdict)." + UNTRUSTED_FIELDS_NOTE,
|
|
2147
|
+
inputSchema: {
|
|
2148
|
+
projectId: projectIdSchema,
|
|
2149
|
+
slotId: z15.string().min(1).max(128).describe("The slot id (a matrix column)"),
|
|
2150
|
+
persona: z15.string().min(1).max(64).describe("The persona key (a matrix row)")
|
|
2151
|
+
},
|
|
2152
|
+
outputSchema: {
|
|
2153
|
+
cell: cellOutput.nullable(),
|
|
2154
|
+
arm: z15.object({
|
|
2155
|
+
id: z15.string(),
|
|
2156
|
+
displayName: z15.string().nullable(),
|
|
2157
|
+
content: z15.string().nullable(),
|
|
2158
|
+
blocks: z15.unknown().nullable()
|
|
2159
|
+
}).nullable(),
|
|
2160
|
+
performance: z15.object({
|
|
2161
|
+
shown: z15.number(),
|
|
2162
|
+
conversions: z15.number(),
|
|
2163
|
+
conversionRate: z15.number(),
|
|
2164
|
+
baselineShown: z15.number(),
|
|
2165
|
+
baselineConversions: z15.number(),
|
|
2166
|
+
baselineConversionRate: z15.number()
|
|
2167
|
+
}).nullable(),
|
|
2168
|
+
verdict: z15.string()
|
|
2169
|
+
},
|
|
2170
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2171
|
+
},
|
|
2172
|
+
withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
|
|
2173
|
+
var _a, _b, _c;
|
|
2174
|
+
const id = encodeURIComponent(projectId);
|
|
2175
|
+
const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
|
|
2176
|
+
const lines = [];
|
|
2177
|
+
if (data.arm) {
|
|
2178
|
+
lines.push(
|
|
2179
|
+
data.arm.content !== null ? `They see: "${untrusted(data.arm.content, 300)}"` : `They see a designed layout (option ${untrusted(data.arm.id, 64)}).`
|
|
2180
|
+
);
|
|
2181
|
+
} else {
|
|
2182
|
+
lines.push("No generated version is live for this cell.");
|
|
2183
|
+
}
|
|
2184
|
+
if ((_a = data.cell) == null ? void 0 : _a.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
|
|
2185
|
+
if (((_b = data.cell) == null ? void 0 : _b.refinedFrom) && (data.cell.status === "pending" || data.cell.status === "generating")) {
|
|
2186
|
+
lines.push(
|
|
2187
|
+
`An improved take on ${untrusted(data.cell.refinedFrom, 64)} is being written \u2014 the current version keeps serving meanwhile.`
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
if ((_c = data.cell) == null ? void 0 : _c.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
|
|
2191
|
+
lines.push(data.verdict);
|
|
2192
|
+
if (data.performance) {
|
|
2193
|
+
lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
|
|
2194
|
+
}
|
|
2195
|
+
return {
|
|
2196
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2197
|
+
structuredContent: data
|
|
2198
|
+
};
|
|
2199
|
+
})
|
|
2200
|
+
);
|
|
2201
|
+
server.registerTool(
|
|
2202
|
+
"generate_cell",
|
|
2203
|
+
{
|
|
2204
|
+
title: "Generate a cell version",
|
|
2205
|
+
description: "Queue AI generation of a version for one (persona, slot) cell. The worker writes it asynchronously (usually under a minute), brand-locked, and it goes live as a named option the optimizer explores. Also works to regenerate or retry a failed cell. Requires a paid plan (anonymous demo tokens are read-only).",
|
|
2206
|
+
inputSchema: {
|
|
2207
|
+
projectId: projectIdSchema,
|
|
2208
|
+
slotId: z15.string().min(1).max(128).describe("The slot id (a matrix column)"),
|
|
2209
|
+
persona: z15.string().min(1).max(64).describe("The persona key (a matrix row)")
|
|
2210
|
+
},
|
|
2211
|
+
outputSchema: {
|
|
2212
|
+
slotId: z15.string(),
|
|
2213
|
+
persona: z15.string(),
|
|
2214
|
+
status: z15.literal("pending").describe("Queued \u2014 check get_cell_matrix for progress")
|
|
2215
|
+
},
|
|
2216
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
2217
|
+
},
|
|
2218
|
+
withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
|
|
2219
|
+
const id = encodeURIComponent(projectId);
|
|
2220
|
+
await client.post(`/projects/${id}/cells/generate`, { slotId, persona });
|
|
2221
|
+
return {
|
|
2222
|
+
content: [
|
|
2223
|
+
{
|
|
2224
|
+
type: "text",
|
|
2225
|
+
text: `Writing a version of ${untrusted(slotId, 64)} for ${untrusted(persona, 64)} \u2014 it usually goes live in under a minute. Check get_cell_matrix (status flips pending \u2192 live) or get_cell_detail for the story once it lands.`
|
|
2226
|
+
}
|
|
2227
|
+
],
|
|
2228
|
+
structuredContent: { slotId, persona, status: "pending" }
|
|
2229
|
+
};
|
|
2230
|
+
})
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2020
2234
|
// src/server.ts
|
|
2021
|
-
var PKG_VERSION = true ? "0.
|
|
2235
|
+
var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
|
|
2022
2236
|
function createMcpServer(client) {
|
|
2023
2237
|
const server = new McpServer(
|
|
2024
2238
|
{
|
|
@@ -2053,6 +2267,7 @@ function createMcpServer(client) {
|
|
|
2053
2267
|
registerVariantWriteTools(server, client);
|
|
2054
2268
|
registerIntegrationGuideTools(server);
|
|
2055
2269
|
registerAgentTrafficTools(server, client);
|
|
2270
|
+
registerCellTools(server, client);
|
|
2056
2271
|
return server;
|
|
2057
2272
|
}
|
|
2058
2273
|
|
package/dist/index.cjs
CHANGED
|
@@ -1795,8 +1795,9 @@ var import_zod13 = require("zod");
|
|
|
1795
1795
|
var GUIDE = `# SentientUI integration guide \u2014 the adaptive ladder
|
|
1796
1796
|
|
|
1797
1797
|
SentientUI adapts a site per visitor type (personas), learning from real conversions. Each
|
|
1798
|
-
project has its own persona vocabulary (dashboard -> Settings -> Personas
|
|
1799
|
-
|
|
1798
|
+
project has its own persona vocabulary (dashboard -> Settings -> Personas), and it starts
|
|
1799
|
+
EMPTY: every visitor is 'unknown' until a persona exists, and the optimizer learns across all
|
|
1800
|
+
traffic pooled in the meantime. Personas are DISCOVERED from behavior \u2014 or, far faster,
|
|
1800
1801
|
DECLARED by the app when it already knows the visitor's role: init({ persona: 'admin' }) /
|
|
1801
1802
|
<AdaptiveProvider persona=...> / window.sentient.persona. Declared beats inferred and serves
|
|
1802
1803
|
at full confidence; unrecognized values are ignored server-side and surfaced in the dashboard
|
|
@@ -1813,8 +1814,9 @@ time.
|
|
|
1813
1814
|
other React apps use <AdaptiveProvider> from '@sentientui/react') and add
|
|
1814
1815
|
suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes pre-paint.
|
|
1815
1816
|
Nothing adapts and nothing is tracked until this wrap is in place.
|
|
1816
|
-
3. \`npm run dev\`, then open the app with \`?sentient_persona=
|
|
1817
|
-
\`?sentient_persona=
|
|
1817
|
+
3. \`npm run dev\`, then open the app with \`?sentient_persona=a\` vs
|
|
1818
|
+
\`?sentient_persona=b\` to see it adapt \u2014 in keyless local mode any key drives the built-in
|
|
1819
|
+
heuristic. No API key needed. Against a real project the key must be one you declared.
|
|
1818
1820
|
4. To learn from real traffic: create a project at https://sentient-ui.com and set
|
|
1819
1821
|
NEXT_PUBLIC_SENTIENT_API_KEY=pk_... in .env.local.
|
|
1820
1822
|
|
|
@@ -1823,8 +1825,8 @@ time.
|
|
|
1823
1825
|
Persona attributes on <html> (zero declaration; the attribute carries the project's own
|
|
1824
1826
|
vocabulary keys \u2014 declared or inferred):
|
|
1825
1827
|
|
|
1826
|
-
html[data-sentient-persona='
|
|
1827
|
-
html[data-sentient-confidence='low'] .
|
|
1828
|
+
html[data-sentient-persona='admin'] .admin-tools { display: block; }
|
|
1829
|
+
html[data-sentient-confidence='low'] .admin-tools { display: none; }
|
|
1828
1830
|
|
|
1829
1831
|
Learned style tokens (element-scoped, SSR-safe):
|
|
1830
1832
|
|
|
@@ -1839,14 +1841,35 @@ prefers-reduced-motion: reduce override in CSS.
|
|
|
1839
1841
|
|
|
1840
1842
|
## Rung 2 \u2014 Swap (alternate content)
|
|
1841
1843
|
|
|
1844
|
+
Generated versions (start here) \u2014 wrap what the page shows today; the children are the
|
|
1845
|
+
original:
|
|
1846
|
+
|
|
1847
|
+
<Adaptive id="hero-cta" goal="signup_click">
|
|
1848
|
+
<a href="/signup">Start free trial</a>
|
|
1849
|
+
</Adaptive>
|
|
1850
|
+
|
|
1851
|
+
Mount + deploy registers the region. SentientUI writes versions per visitor type in the
|
|
1852
|
+
dashboard ("Who sees what") \u2014 no redeploy. The children render for holdout traffic, unknown
|
|
1853
|
+
visitor types, types with no version yet, and every error path. No server preload: mounted
|
|
1854
|
+
regions are requested in one batched call. First visit shows the original briefly, then swaps;
|
|
1855
|
+
return visits start from the last version. Keyless local mode renders originals only. Pass
|
|
1856
|
+
onFormSubmit to allow form versions; set reportBaselineText={false} on regions wrapping
|
|
1857
|
+
personalized or account content. (Formerly <AdaptiveSlot>.)
|
|
1858
|
+
|
|
1859
|
+
Code variants \u2014 when you write the alternatives yourself:
|
|
1860
|
+
|
|
1861
|
+
<Adaptive id="buy-box" goal="buy_click" // goal REQUIRED
|
|
1862
|
+
variants={{ control: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }} /> // first key = baseline
|
|
1863
|
+
|
|
1864
|
+
// hook form:
|
|
1842
1865
|
const { value, bind } = useAdaptive('buy-box', {
|
|
1843
1866
|
variants: { calm: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }, // first key = baseline
|
|
1844
1867
|
goal: 'buy_click', // REQUIRED
|
|
1845
1868
|
});
|
|
1846
1869
|
return <div {...bind}>{value}</div>;
|
|
1847
1870
|
|
|
1848
|
-
|
|
1849
|
-
|
|
1871
|
+
Children or variants on one <Adaptive>, never both. With the hook, always attach bind \u2014 it
|
|
1872
|
+
wires exposure tracking and goal listeners. <AdaptiveText> swaps dashboard-managed text.
|
|
1850
1873
|
|
|
1851
1874
|
## Rung 3 \u2014 Reorder (structure)
|
|
1852
1875
|
|
|
@@ -1862,6 +1885,23 @@ useLayoutOrder(). Non-React sites: sections: ['#hero', '#pricing', '#faq'] (CSS
|
|
|
1862
1885
|
the theme's natural order) in window.sentient \u2014 the snippet reorders those elements within
|
|
1863
1886
|
their shared parent, and applies nothing if any selector drifts.
|
|
1864
1887
|
|
|
1888
|
+
## No-code install order (snippet)
|
|
1889
|
+
|
|
1890
|
+
Two tags in <head>, in this order: one inline <script> that assigns the window.sentient config
|
|
1891
|
+
and then, in the same tag after the ';', runs the inline pre-paint script; then the deferred
|
|
1892
|
+
loader. The pre-paint script reads window.sentient when it runs, so sharing the tag is safe. It
|
|
1893
|
+
is optional but recommended \u2014 the loader is deferred, so on a return visit the page can paint
|
|
1894
|
+
its original state before the loader runs; the inline script applies the last served decision
|
|
1895
|
+
from the visitor's own device first, with no network call and no cloak. Get the exact bytes from
|
|
1896
|
+
the dashboard's Install page, or from renderSnippetInstall({ config }) in
|
|
1897
|
+
'@sentientui/snippet/install' (renderSnippetPrePaintScript() for the script alone).
|
|
1898
|
+
|
|
1899
|
+
Strict Content-Security-Policy (script hashes): use the three-tag form instead \u2014 config, the
|
|
1900
|
+
pre-paint script alone, then the loader (renderSnippetInstall({ config, split: true })). The
|
|
1901
|
+
combined tag contains the site's config, so its hash differs per site; the split pre-paint tag is
|
|
1902
|
+
byte-identical everywhere, so one hash covers it. Existing three-tag and config + loader installs
|
|
1903
|
+
keep working.
|
|
1904
|
+
|
|
1865
1905
|
## Testing the integration
|
|
1866
1906
|
|
|
1867
1907
|
Use '@sentientui/react/testing': renderWithSentient(ui, { variants, slots, persona }) forces
|
|
@@ -2023,8 +2063,182 @@ function registerAgentTrafficTools(server, client) {
|
|
|
2023
2063
|
);
|
|
2024
2064
|
}
|
|
2025
2065
|
|
|
2066
|
+
// src/tools/cells.ts
|
|
2067
|
+
var import_zod15 = require("zod");
|
|
2068
|
+
var personaOutput = import_zod15.z.object({
|
|
2069
|
+
key: import_zod15.z.string(),
|
|
2070
|
+
displayName: import_zod15.z.string(),
|
|
2071
|
+
description: import_zod15.z.string().nullable(),
|
|
2072
|
+
share: import_zod15.z.number().describe("Fraction of 30-day decided sessions")
|
|
2073
|
+
});
|
|
2074
|
+
var cellOutput = import_zod15.z.object({
|
|
2075
|
+
slotId: import_zod15.z.string(),
|
|
2076
|
+
persona: import_zod15.z.string(),
|
|
2077
|
+
status: import_zod15.z.string().describe("pending | generating | live | error | rejected"),
|
|
2078
|
+
requestedBy: import_zod15.z.string(),
|
|
2079
|
+
armId: import_zod15.z.string().nullable(),
|
|
2080
|
+
rationale: import_zod15.z.string().nullable(),
|
|
2081
|
+
error: import_zod15.z.string().nullable(),
|
|
2082
|
+
updatedAt: import_zod15.z.string(),
|
|
2083
|
+
// These ride the API response too; omitting them from the schema made zod
|
|
2084
|
+
// STRIP them, so agents could never see refinement or retry state.
|
|
2085
|
+
refinedFrom: import_zod15.z.string().nullable().optional().describe("Incumbent arm this cell is improving on \u2014 a pending cell with this set still SERVES the incumbent"),
|
|
2086
|
+
notBefore: import_zod15.z.string().nullable().optional().describe("ISO time the queue will next consider this cell (retry backoff / daily-cap deferral); null when due now"),
|
|
2087
|
+
attempts: import_zod15.z.number().optional().describe("Generation attempts so far"),
|
|
2088
|
+
performance: import_zod15.z.object({
|
|
2089
|
+
shown: import_zod15.z.number(),
|
|
2090
|
+
conversions: import_zod15.z.number(),
|
|
2091
|
+
deltaPct: import_zod15.z.number().nullable().describe("Signed % vs the original conversion rate"),
|
|
2092
|
+
tier: import_zod15.z.string().describe("early | no_baseline | winning | losing | flat")
|
|
2093
|
+
}).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
|
|
2094
|
+
});
|
|
2095
|
+
function registerCellTools(server, client) {
|
|
2096
|
+
server.registerTool(
|
|
2097
|
+
"get_cell_matrix",
|
|
2098
|
+
{
|
|
2099
|
+
title: "Persona \xD7 slot cell matrix",
|
|
2100
|
+
description: 'The "Who sees what" matrix: which visitor types (personas) have an AI-generated version live in each personalizable region (slot), with per-persona traffic share and the first-miss auto-fill setting. A cell with no row means that visitor type sees the original.' + UNTRUSTED_FIELDS_NOTE,
|
|
2101
|
+
inputSchema: { projectId: projectIdSchema },
|
|
2102
|
+
outputSchema: {
|
|
2103
|
+
slots: import_zod15.z.array(
|
|
2104
|
+
import_zod15.z.object({
|
|
2105
|
+
slotId: import_zod15.z.string(),
|
|
2106
|
+
displayName: import_zod15.z.string(),
|
|
2107
|
+
supportsForms: import_zod15.z.boolean(),
|
|
2108
|
+
// API sends this; without it zod stripped the draft-vs-live signal.
|
|
2109
|
+
live: import_zod15.z.boolean().optional().describe("False for an sdk-registered draft column \u2014 nothing serves until its first generation publishes it")
|
|
2110
|
+
})
|
|
2111
|
+
),
|
|
2112
|
+
personas: import_zod15.z.array(personaOutput),
|
|
2113
|
+
cells: import_zod15.z.array(cellOutput),
|
|
2114
|
+
autoFill: import_zod15.z.boolean(),
|
|
2115
|
+
personaSource: import_zod15.z.string().describe("Active persona-set source: default | declared | discovered"),
|
|
2116
|
+
undecidedShare: import_zod15.z.number()
|
|
2117
|
+
},
|
|
2118
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2119
|
+
},
|
|
2120
|
+
withApiErrorGuidance(async ({ projectId }) => {
|
|
2121
|
+
const id = encodeURIComponent(projectId);
|
|
2122
|
+
const data = await client.get(`/projects/${id}/cells`);
|
|
2123
|
+
const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
|
|
2124
|
+
const lines = [
|
|
2125
|
+
`Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
|
|
2126
|
+
`Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
|
|
2127
|
+
];
|
|
2128
|
+
for (const p of data.personas) {
|
|
2129
|
+
const states = data.slots.map((s) => {
|
|
2130
|
+
const c = byCell.get(`${s.slotId}::${p.key}`);
|
|
2131
|
+
let state = c ? c.status : "original";
|
|
2132
|
+
if (c && (c.status === "pending" || c.status === "generating") && c.refinedFrom) {
|
|
2133
|
+
state = `${c.status} (improving on ${untrusted(c.refinedFrom, 64)} \u2014 current version still live)`;
|
|
2134
|
+
} else if (c && c.status === "pending" && c.notBefore && Date.parse(c.notBefore) > Date.now()) {
|
|
2135
|
+
state = `pending (waiting until ${c.notBefore})`;
|
|
2136
|
+
}
|
|
2137
|
+
return `${untrusted(s.displayName, 40)}: ${state}`;
|
|
2138
|
+
});
|
|
2139
|
+
lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
|
|
2140
|
+
}
|
|
2141
|
+
lines.push(`Undecided visitors (${Math.round(data.undecidedShare * 100)}%) always see the original.`);
|
|
2142
|
+
return {
|
|
2143
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2144
|
+
structuredContent: data
|
|
2145
|
+
};
|
|
2146
|
+
})
|
|
2147
|
+
);
|
|
2148
|
+
server.registerTool(
|
|
2149
|
+
"get_cell_detail",
|
|
2150
|
+
{
|
|
2151
|
+
title: "Cell story",
|
|
2152
|
+
description: "One cell's full story: the generated option a visitor type sees in a slot, why it was written (the stored generation rationale), and how it is performing vs the original (deterministic plain-language verdict)." + UNTRUSTED_FIELDS_NOTE,
|
|
2153
|
+
inputSchema: {
|
|
2154
|
+
projectId: projectIdSchema,
|
|
2155
|
+
slotId: import_zod15.z.string().min(1).max(128).describe("The slot id (a matrix column)"),
|
|
2156
|
+
persona: import_zod15.z.string().min(1).max(64).describe("The persona key (a matrix row)")
|
|
2157
|
+
},
|
|
2158
|
+
outputSchema: {
|
|
2159
|
+
cell: cellOutput.nullable(),
|
|
2160
|
+
arm: import_zod15.z.object({
|
|
2161
|
+
id: import_zod15.z.string(),
|
|
2162
|
+
displayName: import_zod15.z.string().nullable(),
|
|
2163
|
+
content: import_zod15.z.string().nullable(),
|
|
2164
|
+
blocks: import_zod15.z.unknown().nullable()
|
|
2165
|
+
}).nullable(),
|
|
2166
|
+
performance: import_zod15.z.object({
|
|
2167
|
+
shown: import_zod15.z.number(),
|
|
2168
|
+
conversions: import_zod15.z.number(),
|
|
2169
|
+
conversionRate: import_zod15.z.number(),
|
|
2170
|
+
baselineShown: import_zod15.z.number(),
|
|
2171
|
+
baselineConversions: import_zod15.z.number(),
|
|
2172
|
+
baselineConversionRate: import_zod15.z.number()
|
|
2173
|
+
}).nullable(),
|
|
2174
|
+
verdict: import_zod15.z.string()
|
|
2175
|
+
},
|
|
2176
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2177
|
+
},
|
|
2178
|
+
withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
|
|
2179
|
+
var _a2, _b, _c;
|
|
2180
|
+
const id = encodeURIComponent(projectId);
|
|
2181
|
+
const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
|
|
2182
|
+
const lines = [];
|
|
2183
|
+
if (data.arm) {
|
|
2184
|
+
lines.push(
|
|
2185
|
+
data.arm.content !== null ? `They see: "${untrusted(data.arm.content, 300)}"` : `They see a designed layout (option ${untrusted(data.arm.id, 64)}).`
|
|
2186
|
+
);
|
|
2187
|
+
} else {
|
|
2188
|
+
lines.push("No generated version is live for this cell.");
|
|
2189
|
+
}
|
|
2190
|
+
if ((_a2 = data.cell) == null ? void 0 : _a2.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
|
|
2191
|
+
if (((_b = data.cell) == null ? void 0 : _b.refinedFrom) && (data.cell.status === "pending" || data.cell.status === "generating")) {
|
|
2192
|
+
lines.push(
|
|
2193
|
+
`An improved take on ${untrusted(data.cell.refinedFrom, 64)} is being written \u2014 the current version keeps serving meanwhile.`
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
if ((_c = data.cell) == null ? void 0 : _c.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
|
|
2197
|
+
lines.push(data.verdict);
|
|
2198
|
+
if (data.performance) {
|
|
2199
|
+
lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
|
|
2200
|
+
}
|
|
2201
|
+
return {
|
|
2202
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2203
|
+
structuredContent: data
|
|
2204
|
+
};
|
|
2205
|
+
})
|
|
2206
|
+
);
|
|
2207
|
+
server.registerTool(
|
|
2208
|
+
"generate_cell",
|
|
2209
|
+
{
|
|
2210
|
+
title: "Generate a cell version",
|
|
2211
|
+
description: "Queue AI generation of a version for one (persona, slot) cell. The worker writes it asynchronously (usually under a minute), brand-locked, and it goes live as a named option the optimizer explores. Also works to regenerate or retry a failed cell. Requires a paid plan (anonymous demo tokens are read-only).",
|
|
2212
|
+
inputSchema: {
|
|
2213
|
+
projectId: projectIdSchema,
|
|
2214
|
+
slotId: import_zod15.z.string().min(1).max(128).describe("The slot id (a matrix column)"),
|
|
2215
|
+
persona: import_zod15.z.string().min(1).max(64).describe("The persona key (a matrix row)")
|
|
2216
|
+
},
|
|
2217
|
+
outputSchema: {
|
|
2218
|
+
slotId: import_zod15.z.string(),
|
|
2219
|
+
persona: import_zod15.z.string(),
|
|
2220
|
+
status: import_zod15.z.literal("pending").describe("Queued \u2014 check get_cell_matrix for progress")
|
|
2221
|
+
},
|
|
2222
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
2223
|
+
},
|
|
2224
|
+
withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
|
|
2225
|
+
const id = encodeURIComponent(projectId);
|
|
2226
|
+
await client.post(`/projects/${id}/cells/generate`, { slotId, persona });
|
|
2227
|
+
return {
|
|
2228
|
+
content: [
|
|
2229
|
+
{
|
|
2230
|
+
type: "text",
|
|
2231
|
+
text: `Writing a version of ${untrusted(slotId, 64)} for ${untrusted(persona, 64)} \u2014 it usually goes live in under a minute. Check get_cell_matrix (status flips pending \u2192 live) or get_cell_detail for the story once it lands.`
|
|
2232
|
+
}
|
|
2233
|
+
],
|
|
2234
|
+
structuredContent: { slotId, persona, status: "pending" }
|
|
2235
|
+
};
|
|
2236
|
+
})
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2026
2240
|
// src/server.ts
|
|
2027
|
-
var PKG_VERSION = true ? "0.
|
|
2241
|
+
var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
|
|
2028
2242
|
function createMcpServer(client) {
|
|
2029
2243
|
const server = new import_mcp.McpServer(
|
|
2030
2244
|
{
|
|
@@ -2059,6 +2273,7 @@ function createMcpServer(client) {
|
|
|
2059
2273
|
registerVariantWriteTools(server, client);
|
|
2060
2274
|
registerIntegrationGuideTools(server);
|
|
2061
2275
|
registerAgentTrafficTools(server, client);
|
|
2276
|
+
registerCellTools(server, client);
|
|
2062
2277
|
return server;
|
|
2063
2278
|
}
|
|
2064
2279
|
|
package/dist/index.js
CHANGED
package/dist/lib.cjs
CHANGED
|
@@ -1819,8 +1819,9 @@ var import_zod13 = require("zod");
|
|
|
1819
1819
|
var GUIDE = `# SentientUI integration guide \u2014 the adaptive ladder
|
|
1820
1820
|
|
|
1821
1821
|
SentientUI adapts a site per visitor type (personas), learning from real conversions. Each
|
|
1822
|
-
project has its own persona vocabulary (dashboard -> Settings -> Personas
|
|
1823
|
-
|
|
1822
|
+
project has its own persona vocabulary (dashboard -> Settings -> Personas), and it starts
|
|
1823
|
+
EMPTY: every visitor is 'unknown' until a persona exists, and the optimizer learns across all
|
|
1824
|
+
traffic pooled in the meantime. Personas are DISCOVERED from behavior \u2014 or, far faster,
|
|
1824
1825
|
DECLARED by the app when it already knows the visitor's role: init({ persona: 'admin' }) /
|
|
1825
1826
|
<AdaptiveProvider persona=...> / window.sentient.persona. Declared beats inferred and serves
|
|
1826
1827
|
at full confidence; unrecognized values are ignored server-side and surfaced in the dashboard
|
|
@@ -1837,8 +1838,9 @@ time.
|
|
|
1837
1838
|
other React apps use <AdaptiveProvider> from '@sentientui/react') and add
|
|
1838
1839
|
suppressHydrationWarning to <html> \u2014 an inline script sets persona attributes pre-paint.
|
|
1839
1840
|
Nothing adapts and nothing is tracked until this wrap is in place.
|
|
1840
|
-
3. \`npm run dev\`, then open the app with \`?sentient_persona=
|
|
1841
|
-
\`?sentient_persona=
|
|
1841
|
+
3. \`npm run dev\`, then open the app with \`?sentient_persona=a\` vs
|
|
1842
|
+
\`?sentient_persona=b\` to see it adapt \u2014 in keyless local mode any key drives the built-in
|
|
1843
|
+
heuristic. No API key needed. Against a real project the key must be one you declared.
|
|
1842
1844
|
4. To learn from real traffic: create a project at https://sentient-ui.com and set
|
|
1843
1845
|
NEXT_PUBLIC_SENTIENT_API_KEY=pk_... in .env.local.
|
|
1844
1846
|
|
|
@@ -1847,8 +1849,8 @@ time.
|
|
|
1847
1849
|
Persona attributes on <html> (zero declaration; the attribute carries the project's own
|
|
1848
1850
|
vocabulary keys \u2014 declared or inferred):
|
|
1849
1851
|
|
|
1850
|
-
html[data-sentient-persona='
|
|
1851
|
-
html[data-sentient-confidence='low'] .
|
|
1852
|
+
html[data-sentient-persona='admin'] .admin-tools { display: block; }
|
|
1853
|
+
html[data-sentient-confidence='low'] .admin-tools { display: none; }
|
|
1852
1854
|
|
|
1853
1855
|
Learned style tokens (element-scoped, SSR-safe):
|
|
1854
1856
|
|
|
@@ -1863,14 +1865,35 @@ prefers-reduced-motion: reduce override in CSS.
|
|
|
1863
1865
|
|
|
1864
1866
|
## Rung 2 \u2014 Swap (alternate content)
|
|
1865
1867
|
|
|
1868
|
+
Generated versions (start here) \u2014 wrap what the page shows today; the children are the
|
|
1869
|
+
original:
|
|
1870
|
+
|
|
1871
|
+
<Adaptive id="hero-cta" goal="signup_click">
|
|
1872
|
+
<a href="/signup">Start free trial</a>
|
|
1873
|
+
</Adaptive>
|
|
1874
|
+
|
|
1875
|
+
Mount + deploy registers the region. SentientUI writes versions per visitor type in the
|
|
1876
|
+
dashboard ("Who sees what") \u2014 no redeploy. The children render for holdout traffic, unknown
|
|
1877
|
+
visitor types, types with no version yet, and every error path. No server preload: mounted
|
|
1878
|
+
regions are requested in one batched call. First visit shows the original briefly, then swaps;
|
|
1879
|
+
return visits start from the last version. Keyless local mode renders originals only. Pass
|
|
1880
|
+
onFormSubmit to allow form versions; set reportBaselineText={false} on regions wrapping
|
|
1881
|
+
personalized or account content. (Formerly <AdaptiveSlot>.)
|
|
1882
|
+
|
|
1883
|
+
Code variants \u2014 when you write the alternatives yourself:
|
|
1884
|
+
|
|
1885
|
+
<Adaptive id="buy-box" goal="buy_click" // goal REQUIRED
|
|
1886
|
+
variants={{ control: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }} /> // first key = baseline
|
|
1887
|
+
|
|
1888
|
+
// hook form:
|
|
1866
1889
|
const { value, bind } = useAdaptive('buy-box', {
|
|
1867
1890
|
variants: { calm: <CalmBuyBox/>, urgent: <UrgentBuyBox/> }, // first key = baseline
|
|
1868
1891
|
goal: 'buy_click', // REQUIRED
|
|
1869
1892
|
});
|
|
1870
1893
|
return <div {...bind}>{value}</div>;
|
|
1871
1894
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1895
|
+
Children or variants on one <Adaptive>, never both. With the hook, always attach bind \u2014 it
|
|
1896
|
+
wires exposure tracking and goal listeners. <AdaptiveText> swaps dashboard-managed text.
|
|
1874
1897
|
|
|
1875
1898
|
## Rung 3 \u2014 Reorder (structure)
|
|
1876
1899
|
|
|
@@ -1886,6 +1909,23 @@ useLayoutOrder(). Non-React sites: sections: ['#hero', '#pricing', '#faq'] (CSS
|
|
|
1886
1909
|
the theme's natural order) in window.sentient \u2014 the snippet reorders those elements within
|
|
1887
1910
|
their shared parent, and applies nothing if any selector drifts.
|
|
1888
1911
|
|
|
1912
|
+
## No-code install order (snippet)
|
|
1913
|
+
|
|
1914
|
+
Two tags in <head>, in this order: one inline <script> that assigns the window.sentient config
|
|
1915
|
+
and then, in the same tag after the ';', runs the inline pre-paint script; then the deferred
|
|
1916
|
+
loader. The pre-paint script reads window.sentient when it runs, so sharing the tag is safe. It
|
|
1917
|
+
is optional but recommended \u2014 the loader is deferred, so on a return visit the page can paint
|
|
1918
|
+
its original state before the loader runs; the inline script applies the last served decision
|
|
1919
|
+
from the visitor's own device first, with no network call and no cloak. Get the exact bytes from
|
|
1920
|
+
the dashboard's Install page, or from renderSnippetInstall({ config }) in
|
|
1921
|
+
'@sentientui/snippet/install' (renderSnippetPrePaintScript() for the script alone).
|
|
1922
|
+
|
|
1923
|
+
Strict Content-Security-Policy (script hashes): use the three-tag form instead \u2014 config, the
|
|
1924
|
+
pre-paint script alone, then the loader (renderSnippetInstall({ config, split: true })). The
|
|
1925
|
+
combined tag contains the site's config, so its hash differs per site; the split pre-paint tag is
|
|
1926
|
+
byte-identical everywhere, so one hash covers it. Existing three-tag and config + loader installs
|
|
1927
|
+
keep working.
|
|
1928
|
+
|
|
1889
1929
|
## Testing the integration
|
|
1890
1930
|
|
|
1891
1931
|
Use '@sentientui/react/testing': renderWithSentient(ui, { variants, slots, persona }) forces
|
|
@@ -2047,8 +2087,182 @@ function registerAgentTrafficTools(server, client) {
|
|
|
2047
2087
|
);
|
|
2048
2088
|
}
|
|
2049
2089
|
|
|
2090
|
+
// src/tools/cells.ts
|
|
2091
|
+
var import_zod15 = require("zod");
|
|
2092
|
+
var personaOutput = import_zod15.z.object({
|
|
2093
|
+
key: import_zod15.z.string(),
|
|
2094
|
+
displayName: import_zod15.z.string(),
|
|
2095
|
+
description: import_zod15.z.string().nullable(),
|
|
2096
|
+
share: import_zod15.z.number().describe("Fraction of 30-day decided sessions")
|
|
2097
|
+
});
|
|
2098
|
+
var cellOutput = import_zod15.z.object({
|
|
2099
|
+
slotId: import_zod15.z.string(),
|
|
2100
|
+
persona: import_zod15.z.string(),
|
|
2101
|
+
status: import_zod15.z.string().describe("pending | generating | live | error | rejected"),
|
|
2102
|
+
requestedBy: import_zod15.z.string(),
|
|
2103
|
+
armId: import_zod15.z.string().nullable(),
|
|
2104
|
+
rationale: import_zod15.z.string().nullable(),
|
|
2105
|
+
error: import_zod15.z.string().nullable(),
|
|
2106
|
+
updatedAt: import_zod15.z.string(),
|
|
2107
|
+
// These ride the API response too; omitting them from the schema made zod
|
|
2108
|
+
// STRIP them, so agents could never see refinement or retry state.
|
|
2109
|
+
refinedFrom: import_zod15.z.string().nullable().optional().describe("Incumbent arm this cell is improving on \u2014 a pending cell with this set still SERVES the incumbent"),
|
|
2110
|
+
notBefore: import_zod15.z.string().nullable().optional().describe("ISO time the queue will next consider this cell (retry backoff / daily-cap deferral); null when due now"),
|
|
2111
|
+
attempts: import_zod15.z.number().optional().describe("Generation attempts so far"),
|
|
2112
|
+
performance: import_zod15.z.object({
|
|
2113
|
+
shown: import_zod15.z.number(),
|
|
2114
|
+
conversions: import_zod15.z.number(),
|
|
2115
|
+
deltaPct: import_zod15.z.number().nullable().describe("Signed % vs the original conversion rate"),
|
|
2116
|
+
tier: import_zod15.z.string().describe("early | no_baseline | winning | losing | flat")
|
|
2117
|
+
}).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
|
|
2118
|
+
});
|
|
2119
|
+
function registerCellTools(server, client) {
|
|
2120
|
+
server.registerTool(
|
|
2121
|
+
"get_cell_matrix",
|
|
2122
|
+
{
|
|
2123
|
+
title: "Persona \xD7 slot cell matrix",
|
|
2124
|
+
description: 'The "Who sees what" matrix: which visitor types (personas) have an AI-generated version live in each personalizable region (slot), with per-persona traffic share and the first-miss auto-fill setting. A cell with no row means that visitor type sees the original.' + UNTRUSTED_FIELDS_NOTE,
|
|
2125
|
+
inputSchema: { projectId: projectIdSchema },
|
|
2126
|
+
outputSchema: {
|
|
2127
|
+
slots: import_zod15.z.array(
|
|
2128
|
+
import_zod15.z.object({
|
|
2129
|
+
slotId: import_zod15.z.string(),
|
|
2130
|
+
displayName: import_zod15.z.string(),
|
|
2131
|
+
supportsForms: import_zod15.z.boolean(),
|
|
2132
|
+
// API sends this; without it zod stripped the draft-vs-live signal.
|
|
2133
|
+
live: import_zod15.z.boolean().optional().describe("False for an sdk-registered draft column \u2014 nothing serves until its first generation publishes it")
|
|
2134
|
+
})
|
|
2135
|
+
),
|
|
2136
|
+
personas: import_zod15.z.array(personaOutput),
|
|
2137
|
+
cells: import_zod15.z.array(cellOutput),
|
|
2138
|
+
autoFill: import_zod15.z.boolean(),
|
|
2139
|
+
personaSource: import_zod15.z.string().describe("Active persona-set source: default | declared | discovered"),
|
|
2140
|
+
undecidedShare: import_zod15.z.number()
|
|
2141
|
+
},
|
|
2142
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2143
|
+
},
|
|
2144
|
+
withApiErrorGuidance(async ({ projectId }) => {
|
|
2145
|
+
const id = encodeURIComponent(projectId);
|
|
2146
|
+
const data = await client.get(`/projects/${id}/cells`);
|
|
2147
|
+
const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
|
|
2148
|
+
const lines = [
|
|
2149
|
+
`Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
|
|
2150
|
+
`Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
|
|
2151
|
+
];
|
|
2152
|
+
for (const p of data.personas) {
|
|
2153
|
+
const states = data.slots.map((s) => {
|
|
2154
|
+
const c = byCell.get(`${s.slotId}::${p.key}`);
|
|
2155
|
+
let state = c ? c.status : "original";
|
|
2156
|
+
if (c && (c.status === "pending" || c.status === "generating") && c.refinedFrom) {
|
|
2157
|
+
state = `${c.status} (improving on ${untrusted(c.refinedFrom, 64)} \u2014 current version still live)`;
|
|
2158
|
+
} else if (c && c.status === "pending" && c.notBefore && Date.parse(c.notBefore) > Date.now()) {
|
|
2159
|
+
state = `pending (waiting until ${c.notBefore})`;
|
|
2160
|
+
}
|
|
2161
|
+
return `${untrusted(s.displayName, 40)}: ${state}`;
|
|
2162
|
+
});
|
|
2163
|
+
lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
|
|
2164
|
+
}
|
|
2165
|
+
lines.push(`Undecided visitors (${Math.round(data.undecidedShare * 100)}%) always see the original.`);
|
|
2166
|
+
return {
|
|
2167
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2168
|
+
structuredContent: data
|
|
2169
|
+
};
|
|
2170
|
+
})
|
|
2171
|
+
);
|
|
2172
|
+
server.registerTool(
|
|
2173
|
+
"get_cell_detail",
|
|
2174
|
+
{
|
|
2175
|
+
title: "Cell story",
|
|
2176
|
+
description: "One cell's full story: the generated option a visitor type sees in a slot, why it was written (the stored generation rationale), and how it is performing vs the original (deterministic plain-language verdict)." + UNTRUSTED_FIELDS_NOTE,
|
|
2177
|
+
inputSchema: {
|
|
2178
|
+
projectId: projectIdSchema,
|
|
2179
|
+
slotId: import_zod15.z.string().min(1).max(128).describe("The slot id (a matrix column)"),
|
|
2180
|
+
persona: import_zod15.z.string().min(1).max(64).describe("The persona key (a matrix row)")
|
|
2181
|
+
},
|
|
2182
|
+
outputSchema: {
|
|
2183
|
+
cell: cellOutput.nullable(),
|
|
2184
|
+
arm: import_zod15.z.object({
|
|
2185
|
+
id: import_zod15.z.string(),
|
|
2186
|
+
displayName: import_zod15.z.string().nullable(),
|
|
2187
|
+
content: import_zod15.z.string().nullable(),
|
|
2188
|
+
blocks: import_zod15.z.unknown().nullable()
|
|
2189
|
+
}).nullable(),
|
|
2190
|
+
performance: import_zod15.z.object({
|
|
2191
|
+
shown: import_zod15.z.number(),
|
|
2192
|
+
conversions: import_zod15.z.number(),
|
|
2193
|
+
conversionRate: import_zod15.z.number(),
|
|
2194
|
+
baselineShown: import_zod15.z.number(),
|
|
2195
|
+
baselineConversions: import_zod15.z.number(),
|
|
2196
|
+
baselineConversionRate: import_zod15.z.number()
|
|
2197
|
+
}).nullable(),
|
|
2198
|
+
verdict: import_zod15.z.string()
|
|
2199
|
+
},
|
|
2200
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2201
|
+
},
|
|
2202
|
+
withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
|
|
2203
|
+
var _a, _b, _c;
|
|
2204
|
+
const id = encodeURIComponent(projectId);
|
|
2205
|
+
const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
|
|
2206
|
+
const lines = [];
|
|
2207
|
+
if (data.arm) {
|
|
2208
|
+
lines.push(
|
|
2209
|
+
data.arm.content !== null ? `They see: "${untrusted(data.arm.content, 300)}"` : `They see a designed layout (option ${untrusted(data.arm.id, 64)}).`
|
|
2210
|
+
);
|
|
2211
|
+
} else {
|
|
2212
|
+
lines.push("No generated version is live for this cell.");
|
|
2213
|
+
}
|
|
2214
|
+
if ((_a = data.cell) == null ? void 0 : _a.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
|
|
2215
|
+
if (((_b = data.cell) == null ? void 0 : _b.refinedFrom) && (data.cell.status === "pending" || data.cell.status === "generating")) {
|
|
2216
|
+
lines.push(
|
|
2217
|
+
`An improved take on ${untrusted(data.cell.refinedFrom, 64)} is being written \u2014 the current version keeps serving meanwhile.`
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
if ((_c = data.cell) == null ? void 0 : _c.error) lines.push(`Last attempt failed: ${untrusted(data.cell.error, 300)}`);
|
|
2221
|
+
lines.push(data.verdict);
|
|
2222
|
+
if (data.performance) {
|
|
2223
|
+
lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
|
|
2224
|
+
}
|
|
2225
|
+
return {
|
|
2226
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2227
|
+
structuredContent: data
|
|
2228
|
+
};
|
|
2229
|
+
})
|
|
2230
|
+
);
|
|
2231
|
+
server.registerTool(
|
|
2232
|
+
"generate_cell",
|
|
2233
|
+
{
|
|
2234
|
+
title: "Generate a cell version",
|
|
2235
|
+
description: "Queue AI generation of a version for one (persona, slot) cell. The worker writes it asynchronously (usually under a minute), brand-locked, and it goes live as a named option the optimizer explores. Also works to regenerate or retry a failed cell. Requires a paid plan (anonymous demo tokens are read-only).",
|
|
2236
|
+
inputSchema: {
|
|
2237
|
+
projectId: projectIdSchema,
|
|
2238
|
+
slotId: import_zod15.z.string().min(1).max(128).describe("The slot id (a matrix column)"),
|
|
2239
|
+
persona: import_zod15.z.string().min(1).max(64).describe("The persona key (a matrix row)")
|
|
2240
|
+
},
|
|
2241
|
+
outputSchema: {
|
|
2242
|
+
slotId: import_zod15.z.string(),
|
|
2243
|
+
persona: import_zod15.z.string(),
|
|
2244
|
+
status: import_zod15.z.literal("pending").describe("Queued \u2014 check get_cell_matrix for progress")
|
|
2245
|
+
},
|
|
2246
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
2247
|
+
},
|
|
2248
|
+
withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
|
|
2249
|
+
const id = encodeURIComponent(projectId);
|
|
2250
|
+
await client.post(`/projects/${id}/cells/generate`, { slotId, persona });
|
|
2251
|
+
return {
|
|
2252
|
+
content: [
|
|
2253
|
+
{
|
|
2254
|
+
type: "text",
|
|
2255
|
+
text: `Writing a version of ${untrusted(slotId, 64)} for ${untrusted(persona, 64)} \u2014 it usually goes live in under a minute. Check get_cell_matrix (status flips pending \u2192 live) or get_cell_detail for the story once it lands.`
|
|
2256
|
+
}
|
|
2257
|
+
],
|
|
2258
|
+
structuredContent: { slotId, persona, status: "pending" }
|
|
2259
|
+
};
|
|
2260
|
+
})
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2050
2264
|
// src/server.ts
|
|
2051
|
-
var PKG_VERSION = true ? "0.
|
|
2265
|
+
var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
|
|
2052
2266
|
function createMcpServer(client) {
|
|
2053
2267
|
const server = new import_mcp.McpServer(
|
|
2054
2268
|
{
|
|
@@ -2083,6 +2297,7 @@ function createMcpServer(client) {
|
|
|
2083
2297
|
registerVariantWriteTools(server, client);
|
|
2084
2298
|
registerIntegrationGuideTools(server);
|
|
2085
2299
|
registerAgentTrafficTools(server, client);
|
|
2300
|
+
registerCellTools(server, client);
|
|
2086
2301
|
return server;
|
|
2087
2302
|
}
|
|
2088
2303
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/lib.js
CHANGED