@sentientui/mcp 0.18.0 → 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/README.md +2 -0
- package/dist/{chunk-PX6RCSB2.js → chunk-HEMPCGWX.js} +76 -21
- package/dist/index.cjs +76 -21
- package/dist/index.js +1 -1
- package/dist/lib.cjs +76 -21
- package/dist/lib.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,8 @@ A sandboxed demo token is provisioned automatically — 10 calls/month, read-onl
|
|
|
105
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
106
|
| `generate_cell` | Queue AI generation of a version for one (visitor type, region) cell — brand-locked, live in under a minute (paid plan) |
|
|
107
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
|
+
>
|
|
108
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.
|
|
109
111
|
>
|
|
110
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
|
|
|
@@ -1858,12 +1881,20 @@ their shared parent, and applies nothing if any selector drifts.
|
|
|
1858
1881
|
|
|
1859
1882
|
## No-code install order (snippet)
|
|
1860
1883
|
|
|
1861
|
-
|
|
1862
|
-
then the
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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.
|
|
1867
1898
|
|
|
1868
1899
|
## Testing the integration
|
|
1869
1900
|
|
|
@@ -2043,12 +2074,17 @@ var cellOutput = z15.object({
|
|
|
2043
2074
|
rationale: z15.string().nullable(),
|
|
2044
2075
|
error: z15.string().nullable(),
|
|
2045
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"),
|
|
2046
2082
|
performance: z15.object({
|
|
2047
2083
|
shown: z15.number(),
|
|
2048
2084
|
conversions: z15.number(),
|
|
2049
2085
|
deltaPct: z15.number().nullable().describe("Signed % vs the original conversion rate"),
|
|
2050
2086
|
tier: z15.string().describe("early | no_baseline | winning | losing | flat")
|
|
2051
|
-
}).optional().describe("Present on live cells: the at-a-glance readout")
|
|
2087
|
+
}).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
|
|
2052
2088
|
});
|
|
2053
2089
|
function registerCellTools(server, client) {
|
|
2054
2090
|
server.registerTool(
|
|
@@ -2058,7 +2094,15 @@ function registerCellTools(server, client) {
|
|
|
2058
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,
|
|
2059
2095
|
inputSchema: { projectId: projectIdSchema },
|
|
2060
2096
|
outputSchema: {
|
|
2061
|
-
slots: z15.array(
|
|
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
|
+
),
|
|
2062
2106
|
personas: z15.array(personaOutput),
|
|
2063
2107
|
cells: z15.array(cellOutput),
|
|
2064
2108
|
autoFill: z15.boolean(),
|
|
@@ -2072,13 +2116,19 @@ function registerCellTools(server, client) {
|
|
|
2072
2116
|
const data = await client.get(`/projects/${id}/cells`);
|
|
2073
2117
|
const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
|
|
2074
2118
|
const lines = [
|
|
2075
|
-
`Slots: ${data.slots.map((s) => untrusted(s.displayName)).join(", ") || "(none published)"}`,
|
|
2119
|
+
`Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
|
|
2076
2120
|
`Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
|
|
2077
2121
|
];
|
|
2078
2122
|
for (const p of data.personas) {
|
|
2079
2123
|
const states = data.slots.map((s) => {
|
|
2080
2124
|
const c = byCell.get(`${s.slotId}::${p.key}`);
|
|
2081
|
-
|
|
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}`;
|
|
2082
2132
|
});
|
|
2083
2133
|
lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
|
|
2084
2134
|
}
|
|
@@ -2120,7 +2170,7 @@ function registerCellTools(server, client) {
|
|
|
2120
2170
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2121
2171
|
},
|
|
2122
2172
|
withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
|
|
2123
|
-
var _a, _b;
|
|
2173
|
+
var _a, _b, _c;
|
|
2124
2174
|
const id = encodeURIComponent(projectId);
|
|
2125
2175
|
const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
|
|
2126
2176
|
const lines = [];
|
|
@@ -2132,7 +2182,12 @@ function registerCellTools(server, client) {
|
|
|
2132
2182
|
lines.push("No generated version is live for this cell.");
|
|
2133
2183
|
}
|
|
2134
2184
|
if ((_a = data.cell) == null ? void 0 : _a.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
|
|
2135
|
-
if ((_b = data.cell) == null ? void 0 : _b.
|
|
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)}`);
|
|
2136
2191
|
lines.push(data.verdict);
|
|
2137
2192
|
if (data.performance) {
|
|
2138
2193
|
lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
|
|
@@ -2177,7 +2232,7 @@ function registerCellTools(server, client) {
|
|
|
2177
2232
|
}
|
|
2178
2233
|
|
|
2179
2234
|
// src/server.ts
|
|
2180
|
-
var PKG_VERSION = true ? "0.18.
|
|
2235
|
+
var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
|
|
2181
2236
|
function createMcpServer(client) {
|
|
2182
2237
|
const server = new McpServer(
|
|
2183
2238
|
{
|
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
|
|
|
@@ -1864,12 +1887,20 @@ their shared parent, and applies nothing if any selector drifts.
|
|
|
1864
1887
|
|
|
1865
1888
|
## No-code install order (snippet)
|
|
1866
1889
|
|
|
1867
|
-
|
|
1868
|
-
then the
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
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.
|
|
1873
1904
|
|
|
1874
1905
|
## Testing the integration
|
|
1875
1906
|
|
|
@@ -2049,12 +2080,17 @@ var cellOutput = import_zod15.z.object({
|
|
|
2049
2080
|
rationale: import_zod15.z.string().nullable(),
|
|
2050
2081
|
error: import_zod15.z.string().nullable(),
|
|
2051
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"),
|
|
2052
2088
|
performance: import_zod15.z.object({
|
|
2053
2089
|
shown: import_zod15.z.number(),
|
|
2054
2090
|
conversions: import_zod15.z.number(),
|
|
2055
2091
|
deltaPct: import_zod15.z.number().nullable().describe("Signed % vs the original conversion rate"),
|
|
2056
2092
|
tier: import_zod15.z.string().describe("early | no_baseline | winning | losing | flat")
|
|
2057
|
-
}).optional().describe("Present on live cells: the at-a-glance readout")
|
|
2093
|
+
}).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
|
|
2058
2094
|
});
|
|
2059
2095
|
function registerCellTools(server, client) {
|
|
2060
2096
|
server.registerTool(
|
|
@@ -2064,7 +2100,15 @@ function registerCellTools(server, client) {
|
|
|
2064
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,
|
|
2065
2101
|
inputSchema: { projectId: projectIdSchema },
|
|
2066
2102
|
outputSchema: {
|
|
2067
|
-
slots: import_zod15.z.array(
|
|
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
|
+
),
|
|
2068
2112
|
personas: import_zod15.z.array(personaOutput),
|
|
2069
2113
|
cells: import_zod15.z.array(cellOutput),
|
|
2070
2114
|
autoFill: import_zod15.z.boolean(),
|
|
@@ -2078,13 +2122,19 @@ function registerCellTools(server, client) {
|
|
|
2078
2122
|
const data = await client.get(`/projects/${id}/cells`);
|
|
2079
2123
|
const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
|
|
2080
2124
|
const lines = [
|
|
2081
|
-
`Slots: ${data.slots.map((s) => untrusted(s.displayName)).join(", ") || "(none published)"}`,
|
|
2125
|
+
`Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
|
|
2082
2126
|
`Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
|
|
2083
2127
|
];
|
|
2084
2128
|
for (const p of data.personas) {
|
|
2085
2129
|
const states = data.slots.map((s) => {
|
|
2086
2130
|
const c = byCell.get(`${s.slotId}::${p.key}`);
|
|
2087
|
-
|
|
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}`;
|
|
2088
2138
|
});
|
|
2089
2139
|
lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
|
|
2090
2140
|
}
|
|
@@ -2126,7 +2176,7 @@ function registerCellTools(server, client) {
|
|
|
2126
2176
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2127
2177
|
},
|
|
2128
2178
|
withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
|
|
2129
|
-
var _a2, _b;
|
|
2179
|
+
var _a2, _b, _c;
|
|
2130
2180
|
const id = encodeURIComponent(projectId);
|
|
2131
2181
|
const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
|
|
2132
2182
|
const lines = [];
|
|
@@ -2138,7 +2188,12 @@ function registerCellTools(server, client) {
|
|
|
2138
2188
|
lines.push("No generated version is live for this cell.");
|
|
2139
2189
|
}
|
|
2140
2190
|
if ((_a2 = data.cell) == null ? void 0 : _a2.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
|
|
2141
|
-
if ((_b = data.cell) == null ? void 0 : _b.
|
|
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)}`);
|
|
2142
2197
|
lines.push(data.verdict);
|
|
2143
2198
|
if (data.performance) {
|
|
2144
2199
|
lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
|
|
@@ -2183,7 +2238,7 @@ function registerCellTools(server, client) {
|
|
|
2183
2238
|
}
|
|
2184
2239
|
|
|
2185
2240
|
// src/server.ts
|
|
2186
|
-
var PKG_VERSION = true ? "0.18.
|
|
2241
|
+
var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
|
|
2187
2242
|
function createMcpServer(client) {
|
|
2188
2243
|
const server = new import_mcp.McpServer(
|
|
2189
2244
|
{
|
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
|
|
|
@@ -1888,12 +1911,20 @@ their shared parent, and applies nothing if any selector drifts.
|
|
|
1888
1911
|
|
|
1889
1912
|
## No-code install order (snippet)
|
|
1890
1913
|
|
|
1891
|
-
|
|
1892
|
-
then the
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
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.
|
|
1897
1928
|
|
|
1898
1929
|
## Testing the integration
|
|
1899
1930
|
|
|
@@ -2073,12 +2104,17 @@ var cellOutput = import_zod15.z.object({
|
|
|
2073
2104
|
rationale: import_zod15.z.string().nullable(),
|
|
2074
2105
|
error: import_zod15.z.string().nullable(),
|
|
2075
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"),
|
|
2076
2112
|
performance: import_zod15.z.object({
|
|
2077
2113
|
shown: import_zod15.z.number(),
|
|
2078
2114
|
conversions: import_zod15.z.number(),
|
|
2079
2115
|
deltaPct: import_zod15.z.number().nullable().describe("Signed % vs the original conversion rate"),
|
|
2080
2116
|
tier: import_zod15.z.string().describe("early | no_baseline | winning | losing | flat")
|
|
2081
|
-
}).optional().describe("Present on live cells: the at-a-glance readout")
|
|
2117
|
+
}).optional().describe("Present on live cells (and, mid-refinement, the incumbent arm): the at-a-glance readout")
|
|
2082
2118
|
});
|
|
2083
2119
|
function registerCellTools(server, client) {
|
|
2084
2120
|
server.registerTool(
|
|
@@ -2088,7 +2124,15 @@ function registerCellTools(server, client) {
|
|
|
2088
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,
|
|
2089
2125
|
inputSchema: { projectId: projectIdSchema },
|
|
2090
2126
|
outputSchema: {
|
|
2091
|
-
slots: import_zod15.z.array(
|
|
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
|
+
),
|
|
2092
2136
|
personas: import_zod15.z.array(personaOutput),
|
|
2093
2137
|
cells: import_zod15.z.array(cellOutput),
|
|
2094
2138
|
autoFill: import_zod15.z.boolean(),
|
|
@@ -2102,13 +2146,19 @@ function registerCellTools(server, client) {
|
|
|
2102
2146
|
const data = await client.get(`/projects/${id}/cells`);
|
|
2103
2147
|
const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
|
|
2104
2148
|
const lines = [
|
|
2105
|
-
`Slots: ${data.slots.map((s) => untrusted(s.displayName)).join(", ") || "(none published)"}`,
|
|
2149
|
+
`Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
|
|
2106
2150
|
`Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${data.personaSource}`
|
|
2107
2151
|
];
|
|
2108
2152
|
for (const p of data.personas) {
|
|
2109
2153
|
const states = data.slots.map((s) => {
|
|
2110
2154
|
const c = byCell.get(`${s.slotId}::${p.key}`);
|
|
2111
|
-
|
|
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}`;
|
|
2112
2162
|
});
|
|
2113
2163
|
lines.push(`- ${untrusted(p.displayName)} (${Math.round(p.share * 100)}% of visitors) \u2014 ${states.join(" \xB7 ")}`);
|
|
2114
2164
|
}
|
|
@@ -2150,7 +2200,7 @@ function registerCellTools(server, client) {
|
|
|
2150
2200
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2151
2201
|
},
|
|
2152
2202
|
withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
|
|
2153
|
-
var _a, _b;
|
|
2203
|
+
var _a, _b, _c;
|
|
2154
2204
|
const id = encodeURIComponent(projectId);
|
|
2155
2205
|
const data = await client.get(`/projects/${id}/cells/detail?slotId=${encodeURIComponent(slotId)}&persona=${encodeURIComponent(persona)}`);
|
|
2156
2206
|
const lines = [];
|
|
@@ -2162,7 +2212,12 @@ function registerCellTools(server, client) {
|
|
|
2162
2212
|
lines.push("No generated version is live for this cell.");
|
|
2163
2213
|
}
|
|
2164
2214
|
if ((_a = data.cell) == null ? void 0 : _a.rationale) lines.push(`Why this exists: ${untrusted(data.cell.rationale, 500)}`);
|
|
2165
|
-
if ((_b = data.cell) == null ? void 0 : _b.
|
|
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)}`);
|
|
2166
2221
|
lines.push(data.verdict);
|
|
2167
2222
|
if (data.performance) {
|
|
2168
2223
|
lines.push(`Shown ${data.performance.shown} times, ${data.performance.conversions} conversions.`);
|
|
@@ -2207,7 +2262,7 @@ function registerCellTools(server, client) {
|
|
|
2207
2262
|
}
|
|
2208
2263
|
|
|
2209
2264
|
// src/server.ts
|
|
2210
|
-
var PKG_VERSION = true ? "0.18.
|
|
2265
|
+
var PKG_VERSION = true ? "0.18.1" : "0.0.0-dev";
|
|
2211
2266
|
function createMcpServer(client) {
|
|
2212
2267
|
const server = new import_mcp.McpServer(
|
|
2213
2268
|
{
|
package/dist/lib.js
CHANGED