@xenia-martech/mcp 1.2.0-dev.1 → 1.2.0-dev.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +9 -0
  2. package/dist/bin.js +10 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -31,6 +31,15 @@ After you choose how to get the SDK token, the assistant does the rest without s
31
31
  timeout and circuit breaker, consent from your cookie banner, and form conversions.
32
32
  - **Each journey** you name, in your real components: server-side with `{ fallback: null }` so an
33
33
  outage renders your default content; `track()` for event triggers.
34
+ - **Edge personalization** for a storefront whose journeys target pages (Personalization Resolve
35
+ with page patterns), using the SDK's helpers (`@xenia-martech/sdk-js` 1.2+) instead of hand-written
36
+ code. In Next.js: `xeniaEdgeProxy()` in `proxy.ts`, with your site's own facts and rewrites in its
37
+ hooks, plus `createManifestRoute()` and `createRevalidateRoute()` for the public manifest and
38
+ Xenia's storefront webhook. In any other proxy: `@xenia-martech/sdk-js/edge`. Personalized sections
39
+ get `data-xenia-slot` attributes for impressions and clicks. The browser client's `pageViews`,
40
+ `pageLeave`, `trail`, `navClicks` and `contentClicks` options handle tracking, and
41
+ `xeniaEdgeProxy({ debug, qa })` with `<XeniaDebugPanel />` shows what was decided on each page,
42
+ in development and QA only.
34
43
  - **Checks** — a type-check/build and `xenia_verify`, plus warnings about the journey itself (an
35
44
  A/B split that would show the same visitor different variants, a journey that's stopped) with the
36
45
  dashboard fix.
package/dist/bin.js CHANGED
@@ -90,15 +90,15 @@ Ask the user to confirm, then call this tool again with confirm: "${e}".`}:{ok:!
90
90
  `:`${i.replace(/\n+$/,"")}
91
91
  ${t}
92
92
  `;return await nz(n,o),"added"}async function cz(e,t={}){let i=(await e.requireCredential()).activeScope,r=[];try{let o=await _f(e),a=o.filter(s=>s.status==="active").length;r.push({name:"Read journeys",status:"ok",detail:`${o.length} journey(s), ${a} active.`})}catch(o){r.push({name:"Read journeys",status:"failed",detail:e_(o)})}return t.contentKey&&t.sdkToken?r.push(await c6(e,t.contentKey,t.sdkToken)):r.push({name:"Resolve content key",status:"skipped",detail:t.contentKey?"No SDK token supplied \u2014 pass the token the app uses to check resolve.":"No content key supplied."}),t.sendTestEvent?r.push(await u6(e,i)):r.push({name:"Send test event",status:"skipped",detail:"Not requested \u2014 set sendTestEvent to write a test event into this environment."}),{scope:`${i.orgKey}/${i.tenantKey}/${i.environmentKey}`,steps:r,ok:r.every(o=>o.status!=="failed")}}async function c6(e,t,n){try{let i=await fetch(`${e.baseUrl}/api/personalization/resolve`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify({contentKey:t})});if(!i.ok)return{name:"Resolve content key",status:"failed",detail:`resolve("${t}") returned ${i.status}. Check the token is for this tenant/environment and has not been revoked.`};let r=await i.json();return{name:"Resolve content key",status:"ok",detail:`resolve("${t}") succeeded (personalized: ${r.personalized??!1}).`}}catch(i){return{name:"Resolve content key",status:"failed",detail:e_(i)}}}async function u6(e,t){try{return await fetch(`${e.baseUrl}/api/events/v1`,{method:"POST",headers:{"Content-Type":"application/json","X-Tenant-Id":t.tenantKey,"X-Environment":t.environmentKey},body:JSON.stringify({eventName:"customEvent",properties:{source:"xenia-devtools-verify"}})}),{name:"Send test event",status:"ok",detail:"Test event accepted. Check Live executions in the dashboard to see it arrive."}}catch(n){return{name:"Send test event",status:"failed",detail:e_(n)}}}function e_(e){return e instanceof We?`${e.message} (HTTP ${e.status})`:e instanceof Error?e.message:String(e)}var t_=e=>e.triggerType==="personalization-resolve";function r_(e,t){return e.triggers.some(t_)&&t.stack!=="static-html"&&t.stack!=="laravel"}function uz(e){let t=["**Wait for all personalized content, or show the page first?**"," - `all` \u2014 the page appears once every personalized section is decided (up to `timeoutMs`); nothing changes after it shows."," - `first` \u2014 the page appears at once with a skeleton in each personalized section; they fill in as they resolve (default content that is about to change is never shown)."];return["## CMS page \u2014 ask the user before wiring","This journey personalizes a CMS page. Put these questions to the user (don't pick for them), then call","`xenia_get_journey` again with `cmsRendering` and `cmsWait` set to their answers to get the code.","",...e.hasServerRuntime?["1. **Where should personalization run?**"," - `server` (recommended) \u2014 the HTML arrives already personalized: no flicker, good for SEO, tokens stay on the server."," - `client` \u2014 the browser fetches and personalizes the page: no server work, but content changes after load and the CMS Delivery token is public."," - `hybrid` \u2014 the server personalizes the top level; the browser finishes the nested levels after load.",`2. ${t[0]} (skip for \`hybrid\`)`,...t.slice(1)]:[`This project has no server, so personalization runs in the browser. ${t[0]}`,...t.slice(1)]].join(`
93
- `)}function lz(e,t,n){return e.triggers.filter(i=>i.triggerId||i.eventName||t_(i)&&(n||!r_(e,t))).map(i=>({trigger:i,language:l6(t),code:t_(i)?p6(i,t,n??{rendering:"client"}):f6(i,t)}))}function l6(e){switch(e.stack){case"static-html":case"laravel":return"html";case"nextjs":case"react-spa":return"tsx";default:return"ts"}}function dz(e){let t=e.method==="GET"?e.queryParams:e.bodyData;return t.length===0?"{}":`{
93
+ `)}function lz(e,t,n){return e.triggers.filter(i=>i.triggerId||i.eventName||t_(i)&&(n||!r_(e,t))).map(i=>({trigger:i,language:l6(t),code:t_(i)?p6(i,t,n??{rendering:"client"},e.contentKeys[0]):f6(i,t)}))}function l6(e){switch(e.stack){case"static-html":case"laravel":return"html";case"nextjs":case"react-spa":return"tsx";default:return"ts"}}function dz(e){let t=e.method==="GET"?e.queryParams:e.bodyData;return t.length===0?"{}":`{
94
94
  ${t.map(i=>` ${i.key}: ${d6(i.type)}`).join(`,
95
95
  `)}
96
- }`}function d6(e){switch(e.toLowerCase()){case"number":case"integer":return"0";case"boolean":return"false";case"object":return"{}";case"array":return"[]";default:return'""'}}function f6(e,t){return e.triggerType==="event-trigger"?h6(e,t):m6(e,t)}function p6(e,t,n){let i=e.label??e.nodeId,r='{ contentType: "page", fields: { slug } }',o=`${r}; // or the page's entry id`,a=t.publicEnvPrefix??"",s=f=>['import { contentfulAdapter } from "@xenia-martech/sdk-js/cms/contentful";',"// The SDK fetches the page and its replacements with these (Delivery API, read-only).","const cms = contentfulAdapter({",` space: process.env.${f}CONTENTFUL_SPACE_ID!,`,` accessToken: process.env.${f}CONTENTFUL_DELIVERY_TOKEN!,`," include: 6, // link depth: deep enough for the page's whole tree","});"],c=f=>f==="hide-until-resolved"?["function Page({ slug }) {",` const ref = ${o}`,' const { content, isLoading } = usePersonalizedPage(ref, { cms, render: "hide-until-resolved", timeoutMs: 800 });'," if (!content) return isLoading ? <PageSkeleton /> : <NotFound />;"," return <PageView page={content} />;","}"]:["function Page({ slug }) {",` const ref = ${o}`,' const result = usePersonalizedPage(ref, { cms, render: "base-first", timeoutMs: 800 });'," if (!result.content) return result.isLoading ? <PageSkeleton /> : <NotFound />;"," return ("," <XeniaContentProvider value={result}>"," <PageView page={result.content} />"," </XeniaContentProvider>"," );","}","","// In each section component: a skeleton while Xenia decides this entry \u2014 status comes from the","// CMS markers from the first render, so its default content never flashes.","function Section({ entry }) {"," const { isLoading } = useXeniaEntry(entry.sys.id);"," return isLoading ? <SectionSkeleton /> : <SectionView entry={entry} />;","}"];if(t.stack==="static-html"||t.stack==="laravel")return[`<!-- ${i}: CMS page personalization runs where the page is fetched \u2014 on your server`," with the npm SDK (createXeniaServer(...).personalizePage), or in a bundled app. -->"].join(`
97
- `);let u=t.hasServerRuntime?n.rendering:"client",l=n.wait??"all";if(u==="client")return[l==="all"?'import { usePersonalizedPage } from "@xenia-martech/sdk-js/client/react";':'import { usePersonalizedPage, useXeniaEntry, XeniaContentProvider } from "@xenia-martech/sdk-js/client/react";',...s(a),"",`// ${i}: in the browser (inside <XeniaProvider>)`,...c(l==="all"?"hide-until-resolved":"base-first")].join(`
98
- `);if(t.stack!=="nextjs")return[`import { createXeniaServer } from "${t.sdkEntryPoint}";`,...s(""),"",`// ${i}: in the request handler that renders the page`,"const xenia = createXeniaServer({ tenantId, environmentKey, token: process.env.XENIA_SDK_TOKEN, visitorId });",`const { content } = await xenia.personalizePage(${r}, { cms, timeoutMs: 800 }); // or the page's entry id`,"// content is the personalized page, or null when it doesn't exist (404). Never throws.",...l==="first"?['// "show first" needs streaming or a browser step \u2014 use cmsRendering "hybrid" for that.']:[]].join(`
99
- `);let d=['import { notFound } from "next/navigation";','import { xeniaServer } from "@/lib/xenia";'];return u==="hybrid"?[`// ${i} \u2014 page.tsx (Server Component): the top level is personalized on the server`,...d,...s(""),"","export default async function Page({ params }) {"," const { slug } = await params;",` const ref = ${o}`," const { content } = await (await xeniaServer()).personalizePage(ref, { cms, maxRounds: 1, timeoutMs: 300 });"," if (!content) notFound();"," return <PageClient page={content} />;","}","","// PageClient.tsx \u2014 the browser finishes the nested levels after load (inside <XeniaProvider>)",'"use client";','import { usePersonalizedContent, useXeniaEntry, XeniaContentProvider } from "@xenia-martech/sdk-js/client/react";',`// cms = contentfulAdapter with ${a}CONTENTFUL_SPACE_ID / ${a}CONTENTFUL_DELIVERY_TOKEN`,"export function PageClient({ page }) {"," const result = usePersonalizedContent(page, { cms }); // already-decided entries aren't asked again"," return ("," <XeniaContentProvider value={result}>"," <PageView page={result.content ?? page} />"," </XeniaContentProvider>"," );","}","","// In each section: the nested entries still to decide render as skeletons \u2014 from the server","// render on, so their default content never flashes.","function Section({ entry }) {"," const { isLoading } = useXeniaEntry(entry.sys.id);"," return isLoading ? <SectionSkeleton /> : <SectionView entry={entry} />;","}"].join(`
100
- `):l==="first"?[...d,'import { Suspense } from "react";','import { pendingTargets } from "@xenia-martech/sdk-js/server";',...s(""),"",`// ${i}: the page renders at once with a skeleton in each personalized section; the`,"// personalized page streams in to fill them. Default content that will change is never shown.","export default async function Page({ params }) {"," const { slug } = await params;",` const ref = ${o}`," // maxRounds: 0 = CMS content only (static swaps applied), no Xenia call"," const base = await (await xeniaServer()).personalizePage(ref, { cms, maxRounds: 0 });"," if (!base.content) notFound();"," // The entries Xenia still has to decide, read from the CMS markers (no network)."," const pending = new Set(pendingTargets(base.content, cms));"," return ("," <Suspense fallback={<PageView page={base.content} skeletonFor={pending} />}>"," <PersonalizedPage pageRef={ref} base={base.content} />"," </Suspense>"," );","}","","// PageView renders a section as a skeleton when skeletonFor.has(entry.sys.id).","async function PersonalizedPage({ pageRef, base }) {"," const { content } = await (await xeniaServer()).personalizePage(pageRef, { cms, timeoutMs: 800 });"," return <PageView page={content ?? base} />;","}"].join(`
101
- `):[...d,...s(""),"",`// ${i}: in the page's Server Component \u2014 the SDK fetches the page and personalizes it`,"export default async function Page({ params }) {"," const { slug } = await params;",` const ref = ${o}`," const { content } = await (await xeniaServer()).personalizePage(ref, { cms, timeoutMs: 800 });"," if (!content) notFound();"," return <PageView page={content} />; // never throws: undecided sections keep their default","}"].join(`
96
+ }`}function d6(e){switch(e.toLowerCase()){case"number":case"integer":return"0";case"boolean":return"false";case"object":return"{}";case"array":return"[]";default:return'""'}}function f6(e,t){return e.triggerType==="event-trigger"?h6(e,t):m6(e,t)}function p6(e,t,n,i="your-content-key"){let r=`contentKey: ${JSON.stringify(i)}`,o=e.label??e.nodeId,a='{ contentType: "page", fields: { slug } }',s=`${a}; // or the page's entry id`,c=t.publicEnvPrefix??"",u=m=>['import { contentfulAdapter } from "@xenia-martech/sdk-js/cms/contentful";',"// The SDK fetches the page and its replacements with these (Delivery API, read-only).","const cms = contentfulAdapter({",` space: process.env.${m}CONTENTFUL_SPACE_ID!,`,` accessToken: process.env.${m}CONTENTFUL_DELIVERY_TOKEN!,`," include: 6, // link depth: deep enough for the page's whole tree","});"],l=m=>m==="hide-until-resolved"?["function Page({ slug }) {",` const ref = ${s}`,` const { content, isLoading } = usePersonalizedPage(ref, { cms, ${r}, render: "hide-until-resolved", timeoutMs: 800 });`," if (!content) return isLoading ? <PageSkeleton /> : <NotFound />;"," return <PageView page={content} />;","}"]:["function Page({ slug }) {",` const ref = ${s}`,` const result = usePersonalizedPage(ref, { cms, ${r}, render: "base-first", timeoutMs: 800 });`," if (!result.content) return result.isLoading ? <PageSkeleton /> : <NotFound />;"," return ("," <XeniaContentProvider value={result}>"," <PageView page={result.content} />"," </XeniaContentProvider>"," );","}","","// In each section component: a skeleton while Xenia decides this entry \u2014 status comes from the","// CMS markers from the first render, so its default content never flashes.","function Section({ entry }) {"," const { isLoading } = useXeniaEntry(entry.sys.id);"," return isLoading ? <SectionSkeleton /> : <SectionView entry={entry} />;","}"];if(t.stack==="static-html"||t.stack==="laravel")return[`<!-- ${o}: CMS page personalization runs where the page is fetched \u2014 on your server`," with the npm SDK (createXeniaServer(...).personalizePage), or in a bundled app. -->"].join(`
97
+ `);let d=t.hasServerRuntime?n.rendering:"client",f=n.wait??"all";if(d==="client")return[f==="all"?'import { usePersonalizedPage } from "@xenia-martech/sdk-js/client/react";':'import { usePersonalizedPage, useXeniaEntry, XeniaContentProvider } from "@xenia-martech/sdk-js/client/react";',...u(c),"",`// ${o}: in the browser (inside <XeniaProvider>)`,...l(f==="all"?"hide-until-resolved":"base-first")].join(`
98
+ `);if(t.stack!=="nextjs")return[`import { createXeniaServer } from "${t.sdkEntryPoint}";`,...u(""),"",`// ${o}: in the request handler that renders the page`,"const xenia = createXeniaServer({ tenantId, environmentKey, token: process.env.XENIA_SDK_TOKEN, visitorId });",`const { content } = await xenia.personalizePage(${a}, { cms, ${r}, timeoutMs: 800 }); // or the page's entry id`,"// content is the personalized page, or null when it doesn't exist (404). Never throws.",...f==="first"?['// "show first" needs streaming or a browser step \u2014 use cmsRendering "hybrid" for that.']:[]].join(`
99
+ `);let p=['import { notFound } from "next/navigation";','import { xeniaServer } from "@/lib/xenia";'];return d==="hybrid"?[`// ${o} \u2014 page.tsx (Server Component): the top level is personalized on the server`,...p,...u(""),"","export default async function Page({ params }) {"," const { slug } = await params;",` const ref = ${s}`,` const { content } = await (await xeniaServer()).personalizePage(ref, { cms, ${r}, maxRounds: 1, timeoutMs: 300 });`," if (!content) notFound();"," return <PageClient page={content} />;","}","","// PageClient.tsx \u2014 the browser finishes the nested levels after load (inside <XeniaProvider>)",'"use client";','import { usePersonalizedContent, useXeniaEntry, XeniaContentProvider } from "@xenia-martech/sdk-js/client/react";',`// cms = contentfulAdapter with ${c}CONTENTFUL_SPACE_ID / ${c}CONTENTFUL_DELIVERY_TOKEN`,"export function PageClient({ page }) {",` const result = usePersonalizedContent(page, { cms, ${r} }); // already-decided entries aren't asked again`," return ("," <XeniaContentProvider value={result}>"," <PageView page={result.content ?? page} />"," </XeniaContentProvider>"," );","}","","// In each section: the nested entries still to decide render as skeletons \u2014 from the server","// render on, so their default content never flashes.","function Section({ entry }) {"," const { isLoading } = useXeniaEntry(entry.sys.id);"," return isLoading ? <SectionSkeleton /> : <SectionView entry={entry} />;","}"].join(`
100
+ `):f==="first"?[...p,'import { Suspense } from "react";','import { pendingTargets } from "@xenia-martech/sdk-js/server";',...u(""),"",`// ${o}: the page renders at once with a skeleton in each personalized section; the`,"// personalized page streams in to fill them. Default content that will change is never shown.","export default async function Page({ params }) {"," const { slug } = await params;",` const ref = ${s}`," // maxRounds: 0 = CMS content only (static swaps applied), no Xenia call",` const base = await (await xeniaServer()).personalizePage(ref, { cms, ${r}, maxRounds: 0 });`," if (!base.content) notFound();"," // The entries Xenia still has to decide, read from the CMS markers (no network)."," const pending = new Set(pendingTargets(base.content, cms));"," return ("," <Suspense fallback={<PageView page={base.content} skeletonFor={pending} />}>"," <PersonalizedPage pageRef={ref} base={base.content} />"," </Suspense>"," );","}","","// PageView renders a section as a skeleton when skeletonFor.has(entry.sys.id).","async function PersonalizedPage({ pageRef, base }) {",` const { content } = await (await xeniaServer()).personalizePage(pageRef, { cms, ${r}, timeoutMs: 800 });`," return <PageView page={content ?? base} />;","}"].join(`
101
+ `):[...p,...u(""),"",`// ${o}: in the page's Server Component \u2014 the SDK fetches the page and personalizes it`,"export default async function Page({ params }) {"," const { slug } = await params;",` const ref = ${s}`,` const { content } = await (await xeniaServer()).personalizePage(ref, { cms, ${r}, timeoutMs: 800 });`," if (!content) notFound();"," return <PageView page={content} />; // never throws: undecided sections keep their default","}"].join(`
102
102
  `)}function m6(e,t){let n=e.triggerId??"",i=dz(e),r=e.label??e.nodeId;switch(t.stack){case"nextjs":return['import { xeniaServer } from "@/lib/xenia";',"",`// ${r}: in the Server Component that renders this content (or a Server Action)`,`const res = await (await xeniaServer()).webhook("${n}", ${ir(i,0)}, { fallback: null });`,"// res is null on timeout, error or an open circuit: render the existing default content.","// For a click whose response shows a popup/banner (xeniaActions) in the browser, use",`// useWebhook("${n}") from "@xenia-martech/sdk-js/client/react" instead.`].join(`
103
103
  `);case"react-spa":return['import { useWebhook } from "@xenia-martech/sdk-js/client/react";',"",`// ${r}`,"function TriggerButton() {",` const { send } = useWebhook("${n}");`," // Any xeniaActions in the response are applied to the page automatically.",` return <button onClick={() => send(${ir(i,2)})}>Go</button>;`,"}"].join(`
104
104
  `);case"static-html":case"laravel":return['<script src="https://cdn.jsdelivr.net/npm/@xenia-martech/sdk-js@latest/dist/xenia-sdk.iife.js"></script>',"<script>",` // ${r}`,` Xenia.request("${n}", ${ir(i,2)});`,"</script>"].join(`
@@ -112,8 +112,8 @@ ${t.map(i=>` ${i.key}: ${d6(i.type)}`).join(`,
112
112
  `)}}function ir(e,t){if(!e.includes(`
113
113
  `)||t===0)return e;let n=" ".repeat(t);return e.split(`
114
114
  `).map((i,r)=>r===0?i:`${n}${i}`).join(`
115
- `)}var g6=new Set(["ab-split","split"]),v6=new Set(["webhook","event-trigger","personalization-resolve"]),y6=new Set(["mab","contextual-mab"]),_6=e=>e===!0||e==="true"||e==="1",$6=(...e)=>e.find(t=>typeof t=="string"&&t.trim()!=="")?.trim()??"",fz=e=>e.data?.nodeKind??e.data?.paletteId??e.type??"";function b6(e){let t=[],i=e.some(r=>v6.has(fz(r)))?"`visitorId` \u2014 the SDK sends it on every call":"a stable customer field such as the customer id or email (this journey isn't called by the SDK, so there is no `visitorId`)";for(let r of e){let o=fz(r);if(!g6.has(o))continue;let a=r.data?.config??{},s=r.data?.label??r.id;y6.has(String(a.strategy??"").toLowerCase())||(_6(a.persistFlowEnabled)?$6(a.persistFlowVariable,a.persistVariable,a.persistKey)||t.push({nodeId:r.id,label:s,message:`A/B split has persistence on but no field chosen, so it still picks at random. Choose ${i} as the field in the dashboard.`}):t.push({nodeId:r.id,label:s,message:`A/B split is not sticky: every call picks a random variant, so the same visitor can see a different variant on each page load and the test's results mix together. Fix in the dashboard: open this node, tick "Keep same variant for same upstream value" (Persist Flow Output Path) and choose ${i}.`}))}return t}async function pz(e,t){try{let n=await e.request(`/api/journeys/${encodeURIComponent(t)}`);return b6(n?.design?.nodes??[])}catch{return[]}}import{readFile as mz}from"node:fs/promises";import{join as hz}from"node:path";async function gz(e,t,n=En){let i=x6(t,n);return t.stack!=="nextjs"?S6(t,i):w6(await k6(e),i)}function x6(e,t){if(t===En)return[];let n=e.tokenType==="server"?"XENIA_URL":`${e.publicEnvPrefix??""}XENIA_URL`;return[`**Engine:** this account is on \`${t}\`, not Xenia's hosted engine. The app must call the`,`same one: make sure \`${n}=${t}\` is in ${e.envFile} (xenia_mint_sdk_token writes it;`,"add it yourself if the user created the token in the dashboard). Without it the app calls the","hosted engine, gets nothing for this tenant, and silently renders default content.",""]}async function k6(e){let t=await I6(hz(e,"package.json")),n={...t?.dependencies,...t?.devDependencies},i=Number(/(\d+)/.exec(n.next??"")?.[1]),r=Number.isFinite(i)?i:null,a=await n_(e,"src/app")||await n_(e,"src/pages")?"src/":"",s=`${a}${r!==null&&r<16?"middleware":"proxy"}.ts`,c=null;for(let u of["proxy.ts","proxy.js","middleware.ts","middleware.js"])if(await n_(e,`${a}${u}`)){c=`${a}${u}`;break}return{nextMajor:r,proxyFile:s,existingProxy:c,libDir:`${a}lib`,hasServerOnly:"server-only"in n}}function w6(e,t){let n=e.proxyFile.endsWith("middleware.ts")?"middleware":"proxy",i=e.existingProxy?[`### 2. Merge into \`${e.existingProxy}\``,"It already exists: keep its logic and return `xeniaProxy(request, { headers })` where it returns","`NextResponse.next(...)`, passing the request headers it built (or `request.headers`). If it","returns a redirect or rewrite on some paths, leave those paths alone. Make sure its `matcher`","covers the pages that call Xenia."]:[`### 2. Create \`${e.proxyFile}\``,"```ts",'import type { NextRequest } from "next/server";','import { xeniaProxy } from "@xenia-martech/sdk-js/next";',"","/** Mints the xenia_vid visitor cookie and passes the page URL to Server Components. */",`export function ${n}(request: NextRequest) {`," return xeniaProxy(request);","}","","export const config = {",' matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],',"};","```"];return[`# Xenia setup for Next.js${e.nextMajor?` ${e.nextMajor}`:""}`,"","Do every step without stopping to ask, except where a step says to ask. Then wire the journeys","with xenia_get_journey and finish with a build and xenia_verify.","",...t,"### 1. Install","`npm i @xenia-martech/sdk-js@^1.1.0` (the `/next` entry, `fallback` and `circuitBreaker` need 1.1;","the range also upgrades a project already on 1.0).","",...i,"",`### 3. Create \`${e.libDir}/xenia.ts\``,"```ts",...e.hasServerOnly?['import "server-only";']:[],'import { after } from "next/server";','import { consentFromNext, xeniaServerFromNext } from "@xenia-martech/sdk-js/next";','import type { AnalyticsEventName, JsonValue } from "@xenia-martech/sdk-js";',"","/** A Xenia client bound to this request's visitor and URL (path, query, UTMs, referrer). */","export function xeniaServer() {"," return xeniaServerFromNext({"," baseUrl: process.env.XENIA_URL, // unset = Xenia's hosted engine"," tenantId: process.env.XENIA_TENANT_ID!,"," environmentKey: process.env.XENIA_ENVIRONMENT!,"," // One token for everything: personalization, events, and webhook triggers that require auth."," token: process.env.XENIA_SDK_TOKEN,"," // The page waits up to this long for a journey; on timeout it renders its default."," timeout: Number(process.env.XENIA_TIMEOUT_MS) || 1500,"," retry: { attempts: 0 },"," // After a failure, skip that journey for 30s instead of every visitor waiting out the timeout."," circuitBreaker: { failureThreshold: 1 },"," });","}","","/**"," * After a successful form submit: link this visitor to the email they gave, then record the"," * conversion (deduplicated on eventId). Only with analytics consent. Runs after the response, never"," * throws, so Xenia can't slow or fail the form."," */","export async function trackSubmission("," eventName: AnalyticsEventName,"," eventId: string,"," email: string,"," properties: Record<string, JsonValue | undefined>,",") {"," const consent = await consentFromNext();"," if (!consent.analytics) return;"," const xenia = await xeniaServer();"," after(async () => {"," try {"," await xenia.identify({ email });",` // Leave out fields the visitor didn't fill in, so reports and audiences don't count "" as a value.`,' const filled = Object.fromEntries(Object.entries(properties).filter(([, v]) => v !== ""));'," await xenia.track(eventName, { eventId, properties: filled, consent });"," } catch (err) {"," console.warn(`[xenia] ${eventName} not recorded`, err);"," }"," });","}","```","The env var names match what xenia_mint_sdk_token writes. Add `XENIA_TIMEOUT_MS` to the env","example file. Never put the server token in a `NEXT_PUBLIC_` variable.","","### 4. Consent","Find the site's cookie/consent banner. Where it records the visitor's choice, also write the","cookie the server reads:","```ts",'import { consentCookie } from "@xenia-martech/sdk-js";','document.cookie = consentCookie({ analytics: granted, consentSource: "site-banner", consentVersion: 1 });',"```","If the banner stores the choice elsewhere (e.g. localStorage), also re-write the cookie from it on","page load, so visitors who chose before this change are covered.","No banner at all? Then `consentFromNext()` reports no consent and nothing is tracked. Ask the user","once whether analytics needs consent where they operate. If not, replace the `consentFromNext()`",'line with `const consent = { analytics: true, consentSource: "no-banner" };`.',"","### 5. Webhook journeys (personalized content)","Call them server-side from the Server Component that renders the content, so there is no flicker","and the token stays on the server:","```ts","const res = await (await xeniaServer()).webhook(TRIGGER_ID, { /* fields from xenia_get_journey */ }, { fallback: null });","// null on timeout, error or an open circuit: render the existing default content.","```","No `try/catch` or `Promise.race` needed. If the response carries a content/entry id that the app","then fetches from a CMS, give that fetch its own `signal: AbortSignal.timeout(ms)` and treat a",'failure as "use the default". Pages that call Xenia render dynamically (they read cookies).',"","### 6. CMS pages (Personalization Resolve journeys on Contentful)","`xenia_get_journey` returns two questions for these journeys \u2014 ask the user, don't pick for them:","where personalization runs (`server` / `client` / `hybrid`) and whether the page waits for all","personalized content or shows first (`all` / `first`). Call it again with `cmsRendering` /","`cmsWait` set to the answers and wire the code it returns. The app passes only the page's identity:","```ts",'const { content } = await (await xeniaServer()).personalizePage({ contentType: "page", fields: { slug } }, { cms });',"```","The SDK fetches the page and its replacements through the `contentfulAdapter`; never throws.","Replace any existing per-section `resolve()` + sequential replacement fetches with this.","","### 7. Forms and conversions","In each Server Action that submits a lead/contact/demo/signup form, after the backend confirms",'success, call `trackSubmission(eventName, "<form>-<referenceId>", email, { form: "<name>", ... })`',"with `formSubmitted`, `demoRequestSubmitted`, etc. Take the id from the backend's response so","a retried submit is not counted twice. Don't put the email or free text in `properties`.","","### 8. Verify","- Type-check and `next build`.","- A new visitor's first response sets `xenia_vid`; a returning visitor's does not.","- With consent declined, submitting a form sends nothing to Xenia; with it accepted, `identify`"," then the event are sent.","- Run xenia_verify. Read any warnings in xenia_get_journey (e.g. an A/B split that isn't sticky)"," and tell the user exactly what to change in the dashboard.","- If personalized content never shows locally, the timeout may be shorter than the round trip"," from this machine: raise `XENIA_TIMEOUT_MS` in `.env.local` to check, and tell the user to size"," it on the deployed site."].join(`
116
- `)}function S6(e,t){let n=e.hasServerRuntime;return[`# Xenia setup for ${e.displayName}`,"","Do every step without stopping to ask, except where a step says to ask. Exact API shapes are in","`node_modules/@xenia-martech/sdk-js/README.md` after install.","",...t,`1. Install \`@xenia-martech/sdk-js@^1.1.0\`${e.stack==="static-html"||e.stack==="laravel"?" \u2014 or use the CDN `<script>` build from the snippet":""}.`,n?"2. Create one server helper that builds `createXeniaServer({ baseUrl: XENIA_URL, tenantId, environmentKey, token, visitorId, context, timeout, retry: { attempts: 0 }, circuitBreaker: { failureThreshold: 1 } })` per request: `visitorId` from `getVisitorId(cookieHeader)`, `context` from `contextFromUrl(requestUrl, referer)`. If no browser SDK sets the `xenia_vid` cookie, mint one (`generateUlid()`) on the first response.":"2. Create the client once at app start (`createXeniaClient` / `<XeniaProvider>`) with tenant and environment from the public env vars; it handles the visitor id, page context and UTMs itself.",n?"3. Webhook journeys: `await xenia.webhook(triggerId, data, { fallback: null })` and render the default on `null`. Same for `resolve(key, { fallback: null })`. For a CMS page (Contentful), ask the user the questions `xenia_get_journey` returns, then wire `await xenia.personalizePage(ref, { cms: contentfulAdapter(...) })` as it shows.":"3. Webhook journeys: `request(triggerId, payload)` / `useWebhook(triggerId)`; any `xeniaActions` in the response are applied to the page.","4. Consent: wherever the site records the visitor's analytics choice, call the client's `consent({ analytics })` or write `consentCookie(...)`. With no banner at all, ask the user once whether analytics needs consent where they operate.",n?'5. Forms: after a successful submit, with analytics consent, `identify({ email })` then `track(eventName, { eventId: "<form>-<backend id>", properties, consent })`, without blocking the response.':"5. Forms: after a successful submit, `identify(visitorId, { email })` and `track(eventName, props)`.","6. Verify: build, check the visitor cookie and one journey call, run xenia_verify, and relay any xenia_get_journey warnings to the user."].join(`
115
+ `)}var g6=new Set(["ab-split","split"]),v6=new Set(["webhook","event-trigger","personalization-resolve"]),y6=new Set(["mab","contextual-mab"]),_6=e=>e===!0||e==="true"||e==="1",$6=(...e)=>e.find(t=>typeof t=="string"&&t.trim()!=="")?.trim()??"",fz=e=>e.data?.nodeKind??e.data?.paletteId??e.type??"";function b6(e){let t=[],i=e.some(r=>v6.has(fz(r)))?"`visitorId` \u2014 the SDK sends it on every call":"a stable customer field such as the customer id or email (this journey isn't called by the SDK, so there is no `visitorId`)";for(let r of e){let o=fz(r);if(!g6.has(o))continue;let a=r.data?.config??{},s=r.data?.label??r.id;y6.has(String(a.strategy??"").toLowerCase())||(_6(a.persistFlowEnabled)?$6(a.persistFlowVariable,a.persistVariable,a.persistKey)||t.push({nodeId:r.id,label:s,message:`A/B split has persistence on but no field chosen, so it still picks at random. Choose ${i} as the field in the dashboard.`}):t.push({nodeId:r.id,label:s,message:`A/B split is not sticky: every call picks a random variant, so the same visitor can see a different variant on each page load and the test's results mix together. Fix in the dashboard: open this node, tick "Keep same variant for same upstream value" (Persist Flow Output Path) and choose ${i}.`}))}return t}async function pz(e,t){try{let n=await e.request(`/api/journeys/${encodeURIComponent(t)}`);return b6(n?.design?.nodes??[])}catch{return[]}}import{readFile as mz}from"node:fs/promises";import{join as hz}from"node:path";async function gz(e,t,n=En){let i=x6(t,n);return t.stack!=="nextjs"?S6(t,i):w6(await k6(e),i)}function x6(e,t){if(t===En)return[];let n=e.tokenType==="server"?"XENIA_URL":`${e.publicEnvPrefix??""}XENIA_URL`;return[`**Engine:** this account is on \`${t}\`, not Xenia's hosted engine. The app must call the`,`same one: make sure \`${n}=${t}\` is in ${e.envFile} (xenia_mint_sdk_token writes it;`,"add it yourself if the user created the token in the dashboard). Without it the app calls the","hosted engine, gets nothing for this tenant, and silently renders default content.",""]}async function k6(e){let t=await I6(hz(e,"package.json")),n={...t?.dependencies,...t?.devDependencies},i=Number(/(\d+)/.exec(n.next??"")?.[1]),r=Number.isFinite(i)?i:null,a=await n_(e,"src/app")||await n_(e,"src/pages")?"src/":"",s=`${a}${r!==null&&r<16?"middleware":"proxy"}.ts`,c=null;for(let u of["proxy.ts","proxy.js","middleware.ts","middleware.js"])if(await n_(e,`${a}${u}`)){c=`${a}${u}`;break}return{nextMajor:r,proxyFile:s,existingProxy:c,libDir:`${a}lib`,hasServerOnly:"server-only"in n}}function w6(e,t){let n=e.proxyFile.endsWith("middleware.ts")?"middleware":"proxy",i=e.existingProxy?[`### 2. Merge into \`${e.existingProxy}\``,"It already exists: keep its logic and return `xeniaProxy(request, { headers })` where it returns","`NextResponse.next(...)`, passing the request headers it built (or `request.headers`). If it","returns a redirect or rewrite on some paths, leave those paths alone. Make sure its `matcher`","covers the pages that call Xenia."]:[`### 2. Create \`${e.proxyFile}\``,"```ts",'import type { NextRequest } from "next/server";','import { xeniaProxy } from "@xenia-martech/sdk-js/next";',"","/** Mints the xenia_vid visitor cookie and passes the page URL to Server Components. */",`export function ${n}(request: NextRequest) {`," return xeniaProxy(request);","}","","export const config = {",' matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],',"};","```"];return[`# Xenia setup for Next.js${e.nextMajor?` ${e.nextMajor}`:""}`,"","Do every step without stopping to ask, except where a step says to ask. Then wire the journeys","with xenia_get_journey and finish with a build and xenia_verify.","",...t,"### 1. Install","`npm i @xenia-martech/sdk-js@^1.1.0` (the `/next` entry, `fallback` and `circuitBreaker` need 1.1;","the range also upgrades a project already on 1.0).","",...i,"",`### 3. Create \`${e.libDir}/xenia.ts\``,"```ts",...e.hasServerOnly?['import "server-only";']:[],'import { after } from "next/server";','import { consentFromNext, xeniaServerFromNext } from "@xenia-martech/sdk-js/next";','import type { AnalyticsEventName, JsonValue } from "@xenia-martech/sdk-js";',"","/** A Xenia client bound to this request's visitor and URL (path, query, UTMs, referrer). */","export function xeniaServer() {"," return xeniaServerFromNext({"," baseUrl: process.env.XENIA_URL, // unset = Xenia's hosted engine"," tenantId: process.env.XENIA_TENANT_ID!,"," environmentKey: process.env.XENIA_ENVIRONMENT!,"," // One token for everything: personalization, events, and webhook triggers that require auth."," token: process.env.XENIA_SDK_TOKEN,"," // The page waits up to this long for a journey; on timeout it renders its default."," timeout: Number(process.env.XENIA_TIMEOUT_MS) || 1500,"," retry: { attempts: 0 },"," // After a failure, skip that journey for 30s instead of every visitor waiting out the timeout."," circuitBreaker: { failureThreshold: 1 },"," });","}","","/**"," * After a successful form submit: link this visitor to the email they gave, then record the"," * conversion (deduplicated on eventId). Only with analytics consent. Runs after the response, never"," * throws, so Xenia can't slow or fail the form."," */","export async function trackSubmission("," eventName: AnalyticsEventName,"," eventId: string,"," email: string,"," properties: Record<string, JsonValue | undefined>,",") {"," const consent = await consentFromNext();"," if (!consent.analytics) return;"," const xenia = await xeniaServer();"," after(async () => {"," try {"," await xenia.identify({ email });",` // Leave out fields the visitor didn't fill in, so reports and audiences don't count "" as a value.`,' const filled = Object.fromEntries(Object.entries(properties).filter(([, v]) => v !== ""));'," await xenia.track(eventName, { eventId, properties: filled, consent });"," } catch (err) {"," console.warn(`[xenia] ${eventName} not recorded`, err);"," }"," });","}","```","The env var names match what xenia_mint_sdk_token writes. Add `XENIA_TIMEOUT_MS` to the env","example file. Never put the server token in a `NEXT_PUBLIC_` variable.","","### 4. Consent","Find the site's cookie/consent banner. Where it records the visitor's choice, also write the","cookie the server reads:","```ts",'import { consentCookie } from "@xenia-martech/sdk-js";','document.cookie = consentCookie({ analytics: granted, consentSource: "site-banner", consentVersion: 1 });',"```","If the banner stores the choice elsewhere (e.g. localStorage), also re-write the cookie from it on","page load, so visitors who chose before this change are covered.","No banner at all? Then `consentFromNext()` reports no consent and nothing is tracked. Ask the user","once whether analytics needs consent where they operate. If not, replace the `consentFromNext()`",'line with `const consent = { analytics: true, consentSource: "no-banner" };`.',"","### 5. Webhook journeys (personalized content)","Call them server-side from the Server Component that renders the content, so there is no flicker","and the token stays on the server:","```ts","const res = await (await xeniaServer()).webhook(TRIGGER_ID, { /* fields from xenia_get_journey */ }, { fallback: null });","// null on timeout, error or an open circuit: render the existing default content.","```","No `try/catch` or `Promise.race` needed. If the response carries a content/entry id that the app","then fetches from a CMS, give that fetch its own `signal: AbortSignal.timeout(ms)` and treat a",'failure as "use the default". Pages that call Xenia render dynamically (they read cookies).',"","### 6. CMS pages (Personalization Resolve journeys on Contentful)","`xenia_get_journey` returns two questions for these journeys \u2014 ask the user, don't pick for them:","where personalization runs (`server` / `client` / `hybrid`) and whether the page waits for all","personalized content or shows first (`all` / `first`). Call it again with `cmsRendering` /","`cmsWait` set to the answers and wire the code it returns. The app passes only the page's identity:","```ts",`const { content } = await (await xeniaServer()).personalizePage({ contentType: "page", fields: { slug } }, { cms, contentKey: "<the journey's content key>" });`,"```","The SDK fetches the page and its replacements through the `contentfulAdapter`; never throws.","Replace any existing per-section `resolve()` + sequential replacement fetches with this.","","### 7. Forms and conversions","In each Server Action that submits a lead/contact/demo/signup form, after the backend confirms",'success, call `trackSubmission(eventName, "<form>-<referenceId>", email, { form: "<name>", ... })`',"with `formSubmitted`, `demoRequestSubmitted`, etc. Take the id from the backend's response so","a retried submit is not counted twice. Don't put the email or free text in `properties`.","","### 8. Verify","- Type-check and `next build`.","- A new visitor's first response sets `xenia_vid`; a returning visitor's does not.","- With consent declined, submitting a form sends nothing to Xenia; with it accepted, `identify`"," then the event are sent.","- Run xenia_verify. Read any warnings in xenia_get_journey (e.g. an A/B split that isn't sticky)"," and tell the user exactly what to change in the dashboard.","- If personalized content never shows locally, the timeout may be shorter than the round trip"," from this machine: raise `XENIA_TIMEOUT_MS` in `.env.local` to check, and tell the user to size"," it on the deployed site."].join(`
116
+ `)}function S6(e,t){let n=e.hasServerRuntime;return[`# Xenia setup for ${e.displayName}`,"","Do every step without stopping to ask, except where a step says to ask. Exact API shapes are in","`node_modules/@xenia-martech/sdk-js/README.md` after install.","",...t,`1. Install \`@xenia-martech/sdk-js@^1.1.0\`${e.stack==="static-html"||e.stack==="laravel"?" \u2014 or use the CDN `<script>` build from the snippet":""}.`,n?"2. Create one server helper that builds `createXeniaServer({ baseUrl: XENIA_URL, tenantId, environmentKey, token, visitorId, context, timeout, retry: { attempts: 0 }, circuitBreaker: { failureThreshold: 1 } })` per request: `visitorId` from `getVisitorId(cookieHeader)`, `context` from `contextFromUrl(requestUrl, referer)`. If no browser SDK sets the `xenia_vid` cookie, mint one (`generateUlid()`) on the first response.":"2. Create the client once at app start (`createXeniaClient` / `<XeniaProvider>`) with tenant and environment from the public env vars; it handles the visitor id, page context and UTMs itself.",n?"3. Webhook journeys: `await xenia.webhook(triggerId, data, { fallback: null })` and render the default on `null`. Same for `resolve(key, { fallback: null })`. For a CMS page (Contentful), ask the user the questions `xenia_get_journey` returns, then wire `await xenia.personalizePage(ref, { cms: contentfulAdapter(...), contentKey })` as it shows.":"3. Webhook journeys: `request(triggerId, payload)` / `useWebhook(triggerId)`; any `xeniaActions` in the response are applied to the page.","4. Consent: wherever the site records the visitor's analytics choice, call the client's `consent({ analytics })` or write `consentCookie(...)`. With no banner at all, ask the user once whether analytics needs consent where they operate.",n?'5. Forms: after a successful submit, with analytics consent, `identify({ email })` then `track(eventName, { eventId: "<form>-<backend id>", properties, consent })`, without blocking the response.':"5. Forms: after a successful submit, `identify(visitorId, { email })` and `track(eventName, props)`.","6. Verify: build, check the visitor cookie and one journey call, run xenia_verify, and relay any xenia_get_journey warnings to the user."].join(`
117
117
  `)}async function I6(e){try{return JSON.parse(await mz(e,"utf8"))}catch{return null}}async function n_(e,t){try{return await mz(hz(e,t)),!0}catch(n){return n.code==="EISDIR"}}function i_(){let e=process.env.XENIA_BASE_URL?.trim()||void 0;return{client:new Es(e),baseUrl:new Es(e).baseUrl,projectRoot:process.env.XENIA_PROJECT_ROOT?.trim()||process.cwd(),clientName:process.env.XENIA_CLIENT_NAME?.trim()||"AI coding assistant",clientVersion:process.env.XENIA_CLIENT_VERSION?.trim()||""}}function ze(e){return{content:[{type:"text",text:e}]}}function Gr(e){return{content:[{type:"text",text:e}],isError:!0}}function Ce(e){return async(...t)=>{try{return await e(...t)}catch(n){return Gr(n instanceof Error?n.message:String(n))}}}function vz(e,t){e.registerTool("xenia_login",{title:"Connect to Xenia",description:"Start connecting this coding assistant to the user's Xenia account. Returns a short code and a URL immediately \u2014 show BOTH to the user verbatim and tell them to open the URL and approve. Then call xenia_login_wait to finish (it blocks until they approve). No password or token is ever exchanged through this conversation.",inputSchema:{},annotations:{readOnlyHint:!1,openWorldHint:!0}},Ce(async()=>{let n=await t.client.credential();if(n)return ze(`Already connected as ${n.email} (${n.activeScope.orgKey}/${n.activeScope.tenantKey}/${n.activeScope.environmentKey}).
118
118
  Use xenia_logout first if you want to connect a different account.`);let i=await U0({baseUrl:t.baseUrl,clientName:t.clientName,...t.clientVersion?{clientVersion:t.clientVersion}:{}});return await L0({baseUrl:t.baseUrl,deviceCode:i.deviceCode,userCode:i.userCode,intervalSeconds:i.interval,expiresAtMs:Date.now()+i.expiresIn*1e3}),ze(["To connect, open this URL and approve the request:","",` ${i.verificationUri}`,` Code: ${i.userCode}`,"",`The link ${i.verificationUriComplete} has the code prefilled.`,"Once you have approved it in the browser, I'll finish connecting."].join(`
119
119
  `))})),e.registerTool("xenia_login_wait",{title:"Finish connecting to Xenia",description:"Complete the connection started by xenia_login. Blocks until the user approves the request in the browser, then stores the session. Call this right after showing the user the code.",inputSchema:{},annotations:{readOnlyHint:!1,openWorldHint:!0}},Ce(async()=>{let n=await V0(t.baseUrl);if(!n)return Gr("No login in progress (or it expired). Run xenia_login first to get a code to approve.");try{let i=await Z0({baseUrl:t.baseUrl,deviceCode:n.deviceCode,intervalSeconds:n.intervalSeconds,timeoutMs:Math.max(n.expiresAtMs-Date.now(),0)}),r=i.activeScope;return ze([`Connected as ${i.fullName} <${i.email}>.`,`Active scope: ${r.orgKey}/${r.tenantKey}/${r.environmentKey}`,"",i.allScopes.length>1?`${i.allScopes.length} scopes available \u2014 use xenia_list_scopes to see them, xenia_select_scope to switch.`:"Next: xenia_list_journeys to see what this workspace has."].join(`
@@ -159,7 +159,7 @@ Never ask the user to paste a Xenia password or SDK token into the conversation,
159
159
 
160
160
  When asked to set up the Xenia SDK in a project (or anything that implies it needs a token), first ask the user how they want to get the SDK token, before calling any tool that touches one: (1) you mint it now via xenia_mint_sdk_token \u2014 written straight to their env file, or (2) they create one themselves in the Xenia dashboard's API Tokens page and paste the resulting env var name in for you to wire up (never its value). Do not default to (1) without asking \u2014 some users are not comfortable with an assistant creating live credentials on their behalf. Run xenia_detect_stack first either way, so the question can name the right token type.
161
161
 
162
- After that, do the whole wiring yourself without asking per step: call xenia_setup_project and carry out every step it lists (create or merge the files, wire consent and forms), then for each journey the user wants, call xenia_get_journey and put its calls into the project's real components. Use the SDK's own helpers instead of hand-rolling them: in Next.js, xeniaProxy/xeniaServerFromNext from @xenia-martech/sdk-js/next; server webhooks with { fallback: null } (no try/catch or Promise.race); circuitBreaker in the config; for a journey with a Personalization Resolve trigger on a Contentful page, personalizePage(ref, { cms: contentfulAdapter(...) }) \u2014 the SDK fetches the page, so no CMS fetch or per-section resolve() calls in app code; ask the user the questions xenia_get_journey returns for it (where it runs, wait for all or show first) before writing it. Finish with a type-check/build and xenia_verify. Stop to ask only for the token choice, the CMS page questions, a production confirmation, or whether analytics needs consent when the site has no banner. Relay every warning from xenia_get_journey to the user: those are dashboard fixes (e.g. an A/B split that isn't sticky), not code changes.`});return vz(t,e),_z(t,e),$z(t,e),t}import{mkdir as R6,readFile as U6,writeFile as xz}from"node:fs/promises";import{homedir as o_}from"node:os";import{dirname as Z6,join as yo}from"node:path";import{fileURLToPath as M6}from"node:url";var a_={"claude-code":{id:"claude-code",displayName:"Claude Code",path:e=>yo(e,".mcp.json"),scope:"project",format:"json",serverKey:["mcpServers"]},cursor:{id:"cursor",displayName:"Cursor",path:e=>yo(e,".cursor","mcp.json"),scope:"project",format:"json",serverKey:["mcpServers"]},windsurf:{id:"windsurf",displayName:"Windsurf",path:()=>yo(o_(),".codeium","windsurf","mcp_config.json"),scope:"user",format:"json",serverKey:["mcpServers"]},copilot:{id:"copilot",displayName:"GitHub Copilot (VS Code)",path:e=>yo(e,".vscode","mcp.json"),scope:"project",format:"json",serverKey:["servers"]},codex:{id:"codex",displayName:"OpenAI Codex CLI",path:()=>yo(o_(),".codex","config.toml"),scope:"user",format:"toml",serverKey:["mcp_servers"]},gemini:{id:"gemini",displayName:"Gemini CLI",path:()=>yo(o_(),".gemini","settings.json"),scope:"user",format:"json",serverKey:["mcpServers"]}};async function kz(e){let t=s_(e,"--client"),n=s_(e,"--project")??process.cwd(),i=s_(e,"--base-url"),r=e.includes("--local");if(!t||!Object.hasOwn(a_,t)){process.stderr.write(`Specify a client: --client <${Object.keys(a_).join("|")}>
162
+ After that, do the whole wiring yourself without asking per step: call xenia_setup_project and carry out every step it lists (create or merge the files, wire consent and forms), then for each journey the user wants, call xenia_get_journey and put its calls into the project's real components. Use the SDK's own helpers instead of hand-rolling them: in Next.js, xeniaProxy/xeniaServerFromNext from @xenia-martech/sdk-js/next; server webhooks with { fallback: null } (no try/catch or Promise.race); circuitBreaker in the config; for a journey with a Personalization Resolve trigger on a Contentful page, personalizePage(ref, { cms: contentfulAdapter(...), contentKey }) with the journey's content key (only that journey then decides the page) \u2014 the SDK fetches the page, so no CMS fetch or per-section resolve() calls in app code; for a storefront proxy that personalizes pages through the edge decide API (journeys whose trigger lists pages), in Next.js xeniaEdgeProxy (proxy.ts) with createManifestRoute and createRevalidateRoute (the public manifest and storefront-webhook routes) from @xenia-martech/sdk-js/next, elsewhere the helpers in @xenia-martech/sdk-js/edge (createXeniaEdge, viewerGeo, edgeConsent, encodeVariant/decodeVariant, verifyWebhook, trimManifest, readTrail/withView) \u2014 never a hand-written manifest client, matcher, geo parser or HMAC check \u2014 with data-xenia-slot/-content-entry-id/-base-entry-id/-bucket on swapped sections for impressions, visitorCookie: { authoritative: true } when the proxy changes the visitor, and the client options pageViews, pageLeave, trail, navClicks and contentClicks instead of hand-written page-view, time-on-page or click tracking, and xeniaEdgeProxy's debug/qa options with <XeniaDebugPanel /> instead of a hand-made debug overlay or QA query parsing; ask the user the questions xenia_get_journey returns for it (where it runs, wait for all or show first) before writing it. Finish with a type-check/build and xenia_verify. Stop to ask only for the token choice, the CMS page questions, a production confirmation, or whether analytics needs consent when the site has no banner. Relay every warning from xenia_get_journey to the user: those are dashboard fixes (e.g. an A/B split that isn't sticky), not code changes.`});return vz(t,e),_z(t,e),$z(t,e),t}import{mkdir as R6,readFile as U6,writeFile as xz}from"node:fs/promises";import{homedir as o_}from"node:os";import{dirname as Z6,join as yo}from"node:path";import{fileURLToPath as M6}from"node:url";var a_={"claude-code":{id:"claude-code",displayName:"Claude Code",path:e=>yo(e,".mcp.json"),scope:"project",format:"json",serverKey:["mcpServers"]},cursor:{id:"cursor",displayName:"Cursor",path:e=>yo(e,".cursor","mcp.json"),scope:"project",format:"json",serverKey:["mcpServers"]},windsurf:{id:"windsurf",displayName:"Windsurf",path:()=>yo(o_(),".codeium","windsurf","mcp_config.json"),scope:"user",format:"json",serverKey:["mcpServers"]},copilot:{id:"copilot",displayName:"GitHub Copilot (VS Code)",path:e=>yo(e,".vscode","mcp.json"),scope:"project",format:"json",serverKey:["servers"]},codex:{id:"codex",displayName:"OpenAI Codex CLI",path:()=>yo(o_(),".codex","config.toml"),scope:"user",format:"toml",serverKey:["mcp_servers"]},gemini:{id:"gemini",displayName:"Gemini CLI",path:()=>yo(o_(),".gemini","settings.json"),scope:"user",format:"json",serverKey:["mcpServers"]}};async function kz(e){let t=s_(e,"--client"),n=s_(e,"--project")??process.cwd(),i=s_(e,"--base-url"),r=e.includes("--local");if(!t||!Object.hasOwn(a_,t)){process.stderr.write(`Specify a client: --client <${Object.keys(a_).join("|")}>
163
163
  `),process.exit(1);return}let o=a_[t],a=o.path(n),s=L6(o,i,r);if(await R6(Z6(a),{recursive:!0}),(o.format==="toml"?await F6(a,o,s):await V6(a,o,s))==="already-configured"){process.stdout.write(`Xenia is already configured for ${o.displayName} in ${a} \u2014 left unchanged.
164
164
  `);return}process.stdout.write([`Configured ${o.displayName}.`,` ${a}${o.scope==="user"?" (applies to all projects)":""}`,"",`Restart ${o.displayName}, then ask it to "connect my Xenia account".`,""].join(`
165
165
  `))}function L6(e,t,n){let i={XENIA_CLIENT_NAME:e.displayName};if(t&&(i.XENIA_BASE_URL=t),n){let r=M6(new URL("./bin.js",import.meta.url));return{command:process.execPath,args:[r],env:i}}return{command:"npx",args:["-y","@xenia-martech/mcp"],env:i}}async function V6(e,t,n){let i=await q6(e)??{},r=t.serverKey[0]??"mcpServers",o=wz(i[r])?i[r]:{};return o.xenia=n,i[r]=o,await xz(e,`${JSON.stringify(i,null,2)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenia-martech/mcp",
3
- "version": "1.2.0-dev.1",
3
+ "version": "1.2.0-dev.3",
4
4
  "description": "Connect your Xenia account to any AI coding assistant — journeys, trigger ids, content keys, and SDK setup over MCP",
5
5
  "license": "MIT",
6
6
  "type": "module",