@xenia-martech/mcp 1.0.0-dev.6 → 1.0.0-dev.7

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 (2) hide show
  1. package/dist/bin.js +1 -1
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -71,7 +71,7 @@ ${e.map(n=>` ${n.key}: ${zE(n.type)}`).join(`,
71
71
  `)}}function St(t,e){if(!t.includes(`
72
72
  `)||e===0)return t;let r=" ".repeat(e);return t.split(`
73
73
  `).map((n,o)=>o===0?n:`${r}${n}`).join(`
74
- `)}var IE=new Set(["ab-split","split"]),RE=new Set(["mab","contextual-mab"]),jE=t=>t===!0||t==="true"||t==="1",NE=(...t)=>t.find(e=>typeof e=="string"&&e.trim()!=="")?.trim()??"";function OE(t){let e=[];for(let r of t){let n=r.data?.nodeKind??r.data?.paletteId??r.type??"";if(!IE.has(n))continue;let o=r.data?.config??{},s=r.data?.label??r.id;RE.has(String(o.strategy??"").toLowerCase())||(jE(o.persistFlowEnabled)?NE(o.persistFlowVariable,o.persistVariable,o.persistKey)||e.push({nodeId:r.id,label:s,message:"A/B split has persistence on but no field chosen, so it still picks at random. Choose `visitorId` as the field in the dashboard."}):e.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 `visitorId` \u2014 the SDK sends it on every call.'}))}return e}async function l_(t,e){try{let r=await t.request(`/api/journeys/${encodeURIComponent(e)}`);return OE(r?.design?.nodes??[])}catch{return[]}}import{readFile as d_}from"node:fs/promises";import{join as p_}from"node:path";async function f_(t,e){return e.stack!=="nextjs"?AE(e):ZE(await CE(t))}async function CE(t){let e=await ME(p_(t,"package.json")),r={...e?.dependencies,...e?.devDependencies},n=Number(/(\d+)/.exec(r.next??"")?.[1]),o=Number.isFinite(n)?n:null,i=await yd(t,"src/app")||await yd(t,"src/pages")?"src/":"",a=`${i}${o!==null&&o<16?"middleware":"proxy"}.ts`,c=null;for(let u of["proxy.ts","proxy.js","middleware.ts","middleware.js"])if(await yd(t,`${i}${u}`)){c=`${i}${u}`;break}return{nextMajor:o,proxyFile:a,existingProxy:c,libDir:`${i}lib`,hasServerOnly:"server-only"in r}}function ZE(t){let e=t.proxyFile.endsWith("middleware.ts")?"middleware":"proxy",r=t.existingProxy?[`### 2. Merge into \`${t.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 \`${t.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 ${e}(request: NextRequest) {`," return xeniaProxy(request);","}","","export const config = {",' matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],',"};","```"];return[`# Xenia setup for Next.js${t.nextMajor?` ${t.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.","","### 1. Install","`npm i @xenia-martech/sdk-js` (1.1 or later: the `/next` entry, `fallback` and `circuitBreaker`).","",...r,"",`### 3. Create \`${t.libDir}/xenia.ts\``,"```ts",...t.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({"," tenantId: process.env.XENIA_TENANT_ID!,"," environmentKey: process.env.XENIA_ENVIRONMENT!,"," 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 });"," await xenia.track(eventName, { eventId, properties, 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. 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`.","","### 7. 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(`
74
+ `)}var IE=new Set(["ab-split","split"]),RE=new Set(["mab","contextual-mab"]),jE=t=>t===!0||t==="true"||t==="1",NE=(...t)=>t.find(e=>typeof e=="string"&&e.trim()!=="")?.trim()??"";function OE(t){let e=[];for(let r of t){let n=r.data?.nodeKind??r.data?.paletteId??r.type??"";if(!IE.has(n))continue;let o=r.data?.config??{},s=r.data?.label??r.id;RE.has(String(o.strategy??"").toLowerCase())||(jE(o.persistFlowEnabled)?NE(o.persistFlowVariable,o.persistVariable,o.persistKey)||e.push({nodeId:r.id,label:s,message:"A/B split has persistence on but no field chosen, so it still picks at random. Choose `visitorId` as the field in the dashboard."}):e.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 `visitorId` \u2014 the SDK sends it on every call.'}))}return e}async function l_(t,e){try{let r=await t.request(`/api/journeys/${encodeURIComponent(e)}`);return OE(r?.design?.nodes??[])}catch{return[]}}import{readFile as d_}from"node:fs/promises";import{join as p_}from"node:path";async function f_(t,e){return e.stack!=="nextjs"?AE(e):ZE(await CE(t))}async function CE(t){let e=await ME(p_(t,"package.json")),r={...e?.dependencies,...e?.devDependencies},n=Number(/(\d+)/.exec(r.next??"")?.[1]),o=Number.isFinite(n)?n:null,i=await yd(t,"src/app")||await yd(t,"src/pages")?"src/":"",a=`${i}${o!==null&&o<16?"middleware":"proxy"}.ts`,c=null;for(let u of["proxy.ts","proxy.js","middleware.ts","middleware.js"])if(await yd(t,`${i}${u}`)){c=`${i}${u}`;break}return{nextMajor:o,proxyFile:a,existingProxy:c,libDir:`${i}lib`,hasServerOnly:"server-only"in r}}function ZE(t){let e=t.proxyFile.endsWith("middleware.ts")?"middleware":"proxy",r=t.existingProxy?[`### 2. Merge into \`${t.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 \`${t.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 ${e}(request: NextRequest) {`," return xeniaProxy(request);","}","","export const config = {",' matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],',"};","```"];return[`# Xenia setup for Next.js${t.nextMajor?` ${t.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.","","### 1. Install","`npm i @xenia-martech/sdk-js` (1.1 or later: the `/next` entry, `fallback` and `circuitBreaker`).","",...r,"",`### 3. Create \`${t.libDir}/xenia.ts\``,"```ts",...t.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({"," tenantId: process.env.XENIA_TENANT_ID!,"," environmentKey: process.env.XENIA_ENVIRONMENT!,"," 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. 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`.","","### 7. 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(`
75
75
  `)}function AE(t){let e=t.hasServerRuntime;return[`# Xenia setup for ${t.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.","",`1. Install \`@xenia-martech/sdk-js\`${t.stack==="static-html"||t.stack==="laravel"?" \u2014 or use the CDN `<script>` build from the snippet":""}.`,e?"2. Create one server helper that builds `createXeniaServer({ 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.",e?"3. Webhook journeys: `await xenia.webhook(triggerId, data, { fallback: null })` and render the default on `null`. Same for `resolve(key, { fallback: null })`.":"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.",e?'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(`
76
76
  `)}async function ME(t){try{return JSON.parse(await d_(t,"utf8"))}catch{return null}}async function yd(t,e){try{return await d_(p_(t,e)),!0}catch(r){return r.code==="EISDIR"}}function _d(){let t=process.env.XENIA_BASE_URL?.trim()||void 0;return{client:new Eo(t),baseUrl:new Eo(t).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 fe(t){return{content:[{type:"text",text:t}]}}function tr(t){return{content:[{type:"text",text:t}],isError:!0}}function ve(t){return async(...e)=>{try{return await t(...e)}catch(r){return tr(r instanceof Error?r.message:String(r))}}}function m_(t,e){t.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}},ve(async()=>{let r=await e.client.credential();if(r)return fe(`Already connected as ${r.email} (${r.activeScope.orgKey}/${r.activeScope.tenantKey}/${r.activeScope.environmentKey}).
77
77
  Use xenia_logout first if you want to connect a different account.`);let n=await My({baseUrl:e.baseUrl,clientName:e.clientName,...e.clientVersion?{clientVersion:e.clientVersion}:{}});return await Ly({baseUrl:e.baseUrl,deviceCode:n.deviceCode,userCode:n.userCode,intervalSeconds:n.interval,expiresAtMs:Date.now()+n.expiresIn*1e3}),fe(["To connect, open this URL and approve the request:","",` ${n.verificationUri}`,` Code: ${n.userCode}`,"",`The link ${n.verificationUriComplete} has the code prefilled.`,"Once you have approved it in the browser, I'll finish connecting."].join(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenia-martech/mcp",
3
- "version": "1.0.0-dev.6",
3
+ "version": "1.0.0-dev.7",
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",