@waniwani/sdk 0.18.1 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/embed.js +60 -60
- package/dist/chat/embed.js.map +1 -1
- package/dist/chat/index.d.ts +33 -4
- package/dist/chat/index.js +7 -7
- package/dist/chat/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy/index.d.ts +22 -4
- package/dist/legacy/index.js +13 -13
- package/dist/legacy/index.js.map +1 -1
- package/dist/mcp/index.js +5 -5
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/react/skybridge.js +1 -1
- package/dist/mcp/react/skybridge.js.map +1 -1
- package/dist/mcp/react.js +5 -5
- package/dist/mcp/react.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var y=class extends Error{constructor(n,r){super(n);this.status=r;this.name="WaniWaniError"}};import{existsSync as Y,readFileSync as z}from"fs";import{resolve as $}from"path";var H="waniwani.json",m;function b(){if(m!==void 0)return m;try{let e=$(process.cwd(),H);if(!Y(e))return m=null,null;let t=z(e,"utf-8");return m=JSON.parse(t),m}catch{return m=null,null}}var C="__waniwani_config__";function X(e){return globalThis[C]=e,e}function P(){return globalThis[C]}var _=["session.started","page.viewed","tool.called","link.clicked","widget_render","user.identified","price_shown","prices_compared","option_selected","lead_qualified","converted"];function v(e,t){for(let n of t){let r=e[n];if(typeof r=="string"&&r.length>0)return r}}var Q=["waniwani/sessionId","openai/sessionId","openai/session","sessionId","conversationId","mcp-session-id"],G=["waniwani/requestId","openai/requestId","requestId","mcp/requestId"],J=["waniwani/traceId","openai/traceId","traceId","mcp/traceId","openai/requestId","requestId"],Z=["waniwani/userId","openai/userId","externalUserId","userId","actorId"],ee=["waniwani/visitorId","visitorId"],te=["correlationId","openai/requestId"];function M(e){return e?v(e,Q):void 0}function A(e){return e?v(e,G):void 0}function B(e){return e?v(e,J):void 0}function F(e){return e?v(e,Z):void 0}function O(e){return e?v(e,ee):void 0}function D(e){return e?v(e,te):void 0}var ne=[{key:"openai/sessionId",source:"chatgpt"},{key:"openai/session",source:"chatgpt"}],re=[{needle:"claude",source:"claude"},{needle:"chatgpt",source:"chatgpt"},{needle:"openai",source:"chatgpt"},{needle:"gemini",source:"gemini"}];function L(e,t){if(e){let r=e["waniwani/source"];if(typeof r=="string"&&r.length>0)return r;for(let{key:i,source:s}of ne){let a=e[i];if(typeof a=="string"&&a.length>0)return s}}let n=t?.name;if(typeof n=="string"&&n.length>0){let r=n.toLowerCase();for(let{needle:i,source:s}of re)if(r.includes(i))return s}}var ie="@waniwani/sdk";function T(e,t={}){let n=t.now??(()=>new Date),r=t.generateId??U,i=ae(e),s=I(e.meta),a=I(e.metadata),o=ce(e,s),c=h(e.eventId)??r(),g=de(e.timestamp,n),u=h(e.source)??L(s)??t.source??ie,d=x(e)?{...e}:void 0,f={...a};return Object.keys(s).length>0&&(f.meta=s),d&&(f.rawLegacy=d),{id:c,type:"mcp.event",name:i,source:u,timestamp:g,correlation:o,properties:se(e,i),metadata:f,rawLegacy:d}}function U(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?`evt_${crypto.randomUUID()}`:`evt_${Math.random().toString(36).slice(2,10)}_${Date.now().toString(36)}`}function se(e,t){if(!x(e))return I(e.properties);let n=oe(e,t),r=I(e.properties);return{...n,...r}}function oe(e,t){switch(t){case"tool.called":{let n={};return h(e.toolName)&&(n.name=e.toolName),h(e.toolType)&&(n.type=e.toolType),n}case"link.clicked":{let n={};return h(e.linkUrl)&&(n.url=e.linkUrl),n}default:return{}}}function ae(e){return x(e)?e.eventType:e.event}function ce(e,t){let n=h(e.requestId)??A(t),r=h(e.sessionId)??M(t),i=h(e.traceId)??B(t),s=h(e.externalUserId)??F(t),a=h(e.visitorId)??O(t),o=h(e.correlationId)??D(t)??n,c={};return r&&(c.sessionId=r),i&&(c.traceId=i),n&&(c.requestId=n),o&&(c.correlationId=o),s&&(c.externalUserId=s),a&&(c.visitorId=a),c}function de(e,t){if(e instanceof Date)return e.toISOString();if(typeof e=="string"){let n=new Date(e);if(!Number.isNaN(n.getTime()))return n.toISOString()}return t().toISOString()}function I(e){return!e||typeof e!="object"||Array.isArray(e)?{}:e}function h(e){if(typeof e=="string"&&e.trim().length!==0)return e}function x(e){return"eventType"in e}function k(e){return{priceShown:({amount:t,currency:n,itemId:r,label:i,...s})=>e({event:"price_shown",properties:{amount:t,currency:n,itemId:r,label:i},...s}),pricesCompared:({options:t,...n})=>e({event:"prices_compared",properties:{options:t},...n}),optionSelected:({id:t,amount:n,currency:r,...i})=>e({event:"option_selected",properties:{id:t,amount:n,currency:r},...i}),leadQualified:t=>{let{externalId:n,email:r,name:i,...s}=t??{};return e({event:"lead_qualified",properties:{externalId:n,email:r,name:i},...s})},converted:({amount:t,currency:n,occurredAt:r,...i})=>e({event:"converted",properties:{amount:t,currency:n,occurredAt:r},...i})}}var ue="/api/mcp/events/v2/batch";var W="@waniwani/sdk";var pe=new Set([401,403]),le=new Set([408,425,429,500,502,503,504]);function w(e){return new R(e)}var R=class{endpointUrl;flushIntervalMs;maxBatchSize;maxBufferSize;maxRetries;retryBaseDelayMs;retryMaxDelayMs;shutdownTimeoutMs;sdkVersion;fetchFn;logger;now;sleep;apiKey;buffer=[];flushTimer;flushScheduled=!1;flushScheduledTimer;flushInFlight;inFlightCount=0;isStopped=!1;isShuttingDown=!1;constructor(t){this.endpointUrl=ge(t.apiUrl,t.endpointPath??ue),this.flushIntervalMs=t.flushIntervalMs??1e3,this.maxBatchSize=t.maxBatchSize??20,this.maxBufferSize=t.maxBufferSize??1e3,this.maxRetries=t.maxRetries??3,this.retryBaseDelayMs=t.retryBaseDelayMs??200,this.retryMaxDelayMs=t.retryMaxDelayMs??2e3,this.shutdownTimeoutMs=t.shutdownTimeoutMs??2e3,this.fetchFn=t.fetchFn??fetch,this.logger=t.logger??console,this.now=t.now??(()=>new Date),this.sleep=t.sleep??(n=>new Promise(r=>setTimeout(r,n))),this.apiKey=t.apiKey,this.sdkVersion=t.sdkVersion,this.flushIntervalMs>0&&(this.flushTimer=setInterval(()=>{this.flush()},this.flushIntervalMs)),this.registerBrowserTeardown()}enqueue(t){if(this.isStopped||this.isShuttingDown){this.logger.warn("[Waniwani] Tracking transport is stopped, dropping event %s",t.id);return}if(this.buffer.length>=this.maxBufferSize){let n=this.buffer.length-this.maxBufferSize+1;this.buffer.splice(0,n),this.logger.warn("[Waniwani] Tracking buffer overflow, dropped %d oldest event(s)",n)}if(this.buffer.push(t),this.buffer.length>=this.maxBatchSize){this.flush();return}this.scheduleMicroFlush()}pendingEvents(){return this.buffer.length+this.inFlightCount}async flush(){return this.flushInFlight?this.flushInFlight:(this.flushInFlight=this.flushLoop().finally(()=>{this.flushInFlight=void 0}),this.flushInFlight)}async shutdown(t){this.isShuttingDown=!0,this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=void 0),this.flushScheduledTimer&&(clearTimeout(this.flushScheduledTimer),this.flushScheduledTimer=void 0,this.flushScheduled=!1);let n=t?.timeoutMs??this.shutdownTimeoutMs,r=this.flush();if(!Number.isFinite(n)||n<=0)return await r,this.isStopped=!0,{timedOut:!1,pendingEvents:this.pendingEvents()};let i=Symbol("shutdown-timeout");return await Promise.race([r.then(()=>"flushed"),this.sleep(n).then(()=>i)])===i?(this.isStopped=!0,{timedOut:!0,pendingEvents:this.pendingEvents()}):(this.isStopped=!0,{timedOut:!1,pendingEvents:this.pendingEvents()})}registerBrowserTeardown(){if(typeof document>"u"||typeof window>"u")return;let t=()=>{document.visibilityState==="hidden"&&this.keepaliveFlush()};document.addEventListener("visibilitychange",t),window.addEventListener("pagehide",()=>this.keepaliveFlush())}keepaliveFlush(){if(this.isStopped||this.buffer.length===0)return;let t=this.buffer.splice(0,this.buffer.length);this.sendKeepaliveChunk(t)}sendKeepaliveChunk(t){let n=JSON.stringify(this.makeBatchRequest(t));if(n.length>6e4&&t.length>1){let r=Math.ceil(t.length/2);this.sendKeepaliveChunk(t.slice(0,r)),this.sendKeepaliveChunk(t.slice(r));return}try{this.fetchFn(this.endpointUrl,{method:"POST",headers:this.requestHeaders(),body:n,keepalive:!0}).catch(()=>{})}catch{}}requestHeaders(){let t={"Content-Type":"application/json","X-WaniWani-SDK":W};return this.apiKey&&(t.Authorization=`Bearer ${this.apiKey}`),t}scheduleMicroFlush(){this.flushScheduled||(this.flushScheduled=!0,this.flushScheduledTimer=setTimeout(()=>{this.flushScheduledTimer=void 0,this.flushScheduled=!1,this.flush()},0))}async flushLoop(){for(;this.buffer.length>0&&!this.isStopped;){let t=this.buffer.splice(0,this.maxBatchSize);await this.sendBatchWithRetry(t)}}async sendBatchWithRetry(t){let n=0,r=t;for(;r.length>0&&!this.isStopped;){this.inFlightCount=r.length;let i=await this.sendBatchOnce(r);switch(this.inFlightCount=0,i.kind){case"success":return;case"auth":this.stopTransportForAuthFailure(i.status,r.length);return;case"permanent":this.logger.error("[Waniwani] Dropping %d event(s) after permanent failure: %s",r.length,i.reason);return;case"retryable":if(n>=this.maxRetries){this.logger.error("[Waniwani] Dropping %d event(s) after retry exhaustion: %s",r.length,i.reason);return}await this.sleep(this.backoffDelayMs(n)),n+=1;continue;case"partial":if(i.permanent.length>0&&this.logger.error("[Waniwani] Dropping %d event(s) rejected as permanent",i.permanent.length),i.retryable.length===0)return;if(n>=this.maxRetries){this.logger.error("[Waniwani] Dropping %d retryable event(s) after retry exhaustion",i.retryable.length);return}r=i.retryable,await this.sleep(this.backoffDelayMs(n)),n+=1;continue}}}async sendBatchOnce(t){let n;try{n=await this.fetchFn(this.endpointUrl,{method:"POST",headers:this.requestHeaders(),body:JSON.stringify(this.makeBatchRequest(t))})}catch(s){return{kind:"retryable",reason:me(s)}}if(pe.has(n.status))return{kind:"auth",status:n.status};if(le.has(n.status))return{kind:"retryable",reason:`HTTP ${n.status}`};if(!n.ok)return{kind:"permanent",reason:`HTTP ${n.status}`};let r=await he(n);if(!r?.rejected||r.rejected.length===0)return{kind:"success"};let i=this.classifyRejectedEvents(t,r.rejected);return i.retryable.length===0&&i.permanent.length===0?{kind:"success"}:{kind:"partial",retryable:i.retryable,permanent:i.permanent}}makeBatchRequest(t){return{sentAt:this.now().toISOString(),source:{sdk:W,version:this.sdkVersion??"0.0.0"},events:t}}classifyRejectedEvents(t,n){let r=new Map(t.map(a=>[a.id,a])),i=[],s=[];for(let a of n){let o=r.get(a.eventId);if(o){if(fe(a)){i.push(o);continue}s.push(o)}}return{retryable:i,permanent:s}}backoffDelayMs(t){let n=this.retryBaseDelayMs*2**t;return Math.min(n,this.retryMaxDelayMs)}stopTransportForAuthFailure(t,n){this.isStopped=!0;let r=this.buffer.length;this.buffer.splice(0,r),this.logger.error("[Waniwani] Auth failure (HTTP %d). Stopping tracking transport and dropping %d queued event(s)",t,n+r)}};function fe(e){if(e.retryable===!0)return!0;let t=e.code.toLowerCase();return t.includes("timeout")||t.includes("temporary")||t.includes("unavailable")||t.includes("rate_limit")||t.includes("transient")||t.includes("server")}async function he(e){let t=await e.text();if(t)try{return JSON.parse(t)}catch{return}}function ge(e,t){let n=e.endsWith("/")?e:`${e}/`,r=t.startsWith("/")?t.slice(1):t;return`${n}${r}`}function me(e){return e instanceof Error?e.message:String(e)}function ve(e){let{apiUrl:t,endpointPath:n}=Te(e.endpoint),r=w({apiUrl:t,apiKey:e.token,endpointPath:n,flushIntervalMs:e.flushIntervalMs}),i,s=o=>typeof o=="function"?o():o,a=async o=>{let c=e.identity?.()??{},g=s(e.channelId),u=g===void 0?o.properties:{channelId:g,...o.properties},d=T({sessionId:c.sessionId,visitorId:c.visitorId,externalUserId:c.externalUserId??i,traceId:c.traceId,...o,properties:u,metadata:{...e.metadata,...o.metadata}},{source:s(e.source)});return r.enqueue(d),{eventId:d.id}};return{track:Object.assign(a,k(a)),identify(o,c){return i=o,a({event:"user.identified",externalUserId:o,properties:c})},flush:()=>r.flush(),shutdown:o=>r.shutdown(o)}}function Te(e){let t=new URL(e);return{apiUrl:t.origin,endpointPath:`${t.pathname}${t.search}`}}import{AsyncLocalStorage as ye}from"async_hooks";var V=globalThis,E=V.__waniwaniKbRetrievalStore;E||(E=new ye,V.__waniwaniKbRetrievalStore=E);var Ie=E;function K(e){try{Ie.getStore()?.searches.push(e)}catch{}}var ke="@waniwani/sdk";function j(e){let{apiUrl:t,apiKey:n}=e;function r(){if(!n)throw new Error("WANIWANI_API_KEY is not set");return n}async function i(s,a,o){let c=r(),g=`${t.replace(/\/$/,"")}${a}`,u={Authorization:`Bearer ${c}`,"X-WaniWani-SDK":ke},d={method:s,headers:u};o!==void 0&&(u["Content-Type"]="application/json",d.body=JSON.stringify(o));let f=await fetch(g,d);if(!f.ok){let q=await f.text().catch(()=>"");throw new y(q||`KB API error: HTTP ${f.status}`,f.status)}return(await f.json()).data}return{async ingest(s){return i("POST","/api/mcp/kb/ingest",{files:s})},async search(s,a){let o=await i("POST","/api/mcp/kb/search",{query:s,...a});return K({query:s,resultCount:o.length,results:o.map(c=>({source:c.source,heading:c.heading,score:c.score}))}),o},async sources(){return i("GET","/api/mcp/kb/sources")}}}function N(e){let{apiUrl:t,apiKey:n,tracking:r}=e;function i(){if(!n)throw new Error("WANIWANI_API_KEY is not set");return n}let s=n?w({apiUrl:t,apiKey:n,endpointPath:r.endpointPath,flushIntervalMs:r.flushIntervalMs,maxBatchSize:r.maxBatchSize,maxBufferSize:r.maxBufferSize,maxRetries:r.maxRetries,retryBaseDelayMs:r.retryBaseDelayMs,retryMaxDelayMs:r.retryMaxDelayMs,shutdownTimeoutMs:r.shutdownTimeoutMs}):void 0;function a(u){i();let d=T(u);return!d.correlation.sessionId&&!d.correlation.externalUserId&&!d.correlation.visitorId&&console.warn(`[waniwani] event "${d.name}" has no sessionId, externalUserId, or visitorId; the ingest API requires one and will reject it.`),s?.enqueue(d),{eventId:d.id}}let o=async u=>a(u),c=Object.assign(o,k(o)),g={async identify(u,d,f){i();let S=T({event:"user.identified",externalUserId:u,properties:d,meta:f});return s?.enqueue(S),{eventId:S.id}},track:c,async flush(){i(),await s?.flush()},async shutdown(u){return i(),await s?.shutdown({timeoutMs:u?.timeoutMs??r.shutdownTimeoutMs})??{timedOut:!1,pendingEvents:0}}};return s&&we(g,r.shutdownTimeoutMs),g}function we(e,t){if(typeof process>"u"||typeof process.once!="function"||typeof process.on!="function")return;let n=()=>{e.shutdown({timeoutMs:t})};process.once("beforeExit",n),process.once("SIGINT",n),process.once("SIGTERM",n)}function Ee(e){let n=e??b()??P(),r=n?.apiUrl??process.env.WANIWANI_API_URL??"https://app.waniwani.ai",i=n?.apiKey??process.env.WANIWANI_API_KEY,s={endpointPath:n?.tracking?.endpointPath??"/api/mcp/events/v2/batch",flushIntervalMs:n?.tracking?.flushIntervalMs??1e3,maxBatchSize:n?.tracking?.maxBatchSize??20,maxBufferSize:n?.tracking?.maxBufferSize??1e3,maxRetries:n?.tracking?.maxRetries??3,retryBaseDelayMs:n?.tracking?.retryBaseDelayMs??200,retryMaxDelayMs:n?.tracking?.retryMaxDelayMs??2e3,shutdownTimeoutMs:n?.tracking?.shutdownTimeoutMs??2e3},a={apiUrl:r,apiKey:i,tracking:s},o=N(a),c=j(a);return{...o,kb:c,_config:a}}export{_ as EVENT_TYPES,y as WaniWaniError,ve as createFrontendClient,X as defineConfig,Ee as waniwani};
|
|
1
|
+
var y=class extends Error{constructor(n,r){super(n);this.status=r;this.name="WaniWaniError"}};import{existsSync as Y,readFileSync as z}from"fs";import{resolve as $}from"path";var H="waniwani.json",m;function b(){if(m!==void 0)return m;try{let e=$(process.cwd(),H);if(!Y(e))return m=null,null;let t=z(e,"utf-8");return m=JSON.parse(t),m}catch{return m=null,null}}var C="__waniwani_config__";function G(e){return globalThis[C]=e,e}function P(){return globalThis[C]}var _=["session.started","page.viewed","tool.called","link.clicked","widget_render","user.identified","price_shown","prices_compared","option_selected","lead_qualified","converted"];function v(e,t){for(let n of t){let r=e[n];if(typeof r=="string"&&r.length>0)return r}}var X=["waniwani/sessionId","openai/sessionId","openai/session","sessionId","conversationId","mcp-session-id"],Q=["waniwani/requestId","openai/requestId","requestId","mcp/requestId"],J=["waniwani/traceId","openai/traceId","traceId","mcp/traceId","openai/requestId","requestId"],Z=["waniwani/userId","openai/userId","externalUserId","userId","actorId"],ee=["waniwani/visitorId","visitorId"],te=["correlationId","openai/requestId"];function M(e){return e?v(e,X):void 0}function A(e){return e?v(e,Q):void 0}function B(e){return e?v(e,J):void 0}function F(e){return e?v(e,Z):void 0}function O(e){return e?v(e,ee):void 0}function D(e){return e?v(e,te):void 0}var ne=[{key:"openai/sessionId",source:"chatgpt"},{key:"openai/session",source:"chatgpt"}],re=[{needle:"claude",source:"claude"},{needle:"chatgpt",source:"chatgpt"},{needle:"openai",source:"chatgpt"},{needle:"gemini",source:"gemini"}];function U(e,t){if(e){let r=e["waniwani/source"];if(typeof r=="string"&&r.length>0)return r;for(let{key:i,source:s}of ne){let a=e[i];if(typeof a=="string"&&a.length>0)return s}}let n=t?.name;if(typeof n=="string"&&n.length>0){let r=n.toLowerCase();for(let{needle:i,source:s}of re)if(r.includes(i))return s}}var ie="@waniwani/sdk";function T(e,t={}){let n=t.now??(()=>new Date),r=t.generateId??L,i=ae(e),s=I(e.meta),a=I(e.metadata),o=ce(e,s),c=h(e.eventId)??r(),g=ue(e.timestamp,n),d=h(e.source)??U(s)??t.source??ie,u=x(e)?{...e}:void 0,f={...a};return Object.keys(s).length>0&&(f.meta=s),u&&(f.rawLegacy=u),{id:c,type:"mcp.event",name:i,source:d,timestamp:g,correlation:o,properties:se(e,i),metadata:f,rawLegacy:u}}function L(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?`evt_${crypto.randomUUID()}`:`evt_${Math.random().toString(36).slice(2,10)}_${Date.now().toString(36)}`}function se(e,t){if(!x(e))return I(e.properties);let n=oe(e,t),r=I(e.properties);return{...n,...r}}function oe(e,t){switch(t){case"tool.called":{let n={};return h(e.toolName)&&(n.name=e.toolName),h(e.toolType)&&(n.type=e.toolType),n}case"link.clicked":{let n={};return h(e.linkUrl)&&(n.url=e.linkUrl),n}default:return{}}}function ae(e){return x(e)?e.eventType:e.event}function ce(e,t){let n=h(e.requestId)??A(t),r=h(e.sessionId)??M(t),i=h(e.traceId)??B(t),s=h(e.externalUserId)??F(t),a=h(e.visitorId)??O(t),o=h(e.correlationId)??D(t)??n,c={};return r&&(c.sessionId=r),i&&(c.traceId=i),n&&(c.requestId=n),o&&(c.correlationId=o),s&&(c.externalUserId=s),a&&(c.visitorId=a),c}function ue(e,t){if(e instanceof Date)return e.toISOString();if(typeof e=="string"){let n=new Date(e);if(!Number.isNaN(n.getTime()))return n.toISOString()}return t().toISOString()}function I(e){return!e||typeof e!="object"||Array.isArray(e)?{}:e}function h(e){if(typeof e=="string"&&e.trim().length!==0)return e}function x(e){return"eventType"in e}function k(e){return{priceShown:({amount:t,currency:n,itemId:r,label:i,...s})=>e({event:"price_shown",properties:{amount:t,currency:n,itemId:r,label:i},...s}),pricesCompared:({options:t,...n})=>e({event:"prices_compared",properties:{options:t},...n}),optionSelected:({id:t,amount:n,currency:r,...i})=>e({event:"option_selected",properties:{id:t,amount:n,currency:r},...i}),leadQualified:t=>{let{externalId:n,email:r,name:i,...s}=t??{};return e({event:"lead_qualified",properties:{externalId:n,email:r,name:i},...s})},converted:({amount:t,currency:n,occurredAt:r,...i})=>e({event:"converted",properties:{amount:t,currency:n,occurredAt:r},...i})}}var de="/api/mcp/events/v2/batch";var W="@waniwani/sdk";var pe=new Set([401,403]),le=new Set([408,425,429,500,502,503,504]);function w(e){return new R(e)}var R=class{endpointUrl;flushIntervalMs;maxBatchSize;maxBufferSize;maxRetries;retryBaseDelayMs;retryMaxDelayMs;shutdownTimeoutMs;sdkVersion;fetchFn;logger;now;sleep;apiKey;buffer=[];flushTimer;flushScheduled=!1;flushScheduledTimer;flushInFlight;inFlightCount=0;isStopped=!1;isShuttingDown=!1;constructor(t){this.endpointUrl=ge(t.apiUrl,t.endpointPath??de),this.flushIntervalMs=t.flushIntervalMs??1e3,this.maxBatchSize=t.maxBatchSize??20,this.maxBufferSize=t.maxBufferSize??1e3,this.maxRetries=t.maxRetries??3,this.retryBaseDelayMs=t.retryBaseDelayMs??200,this.retryMaxDelayMs=t.retryMaxDelayMs??2e3,this.shutdownTimeoutMs=t.shutdownTimeoutMs??2e3,this.fetchFn=t.fetchFn??fetch,this.logger=t.logger??console,this.now=t.now??(()=>new Date),this.sleep=t.sleep??(n=>new Promise(r=>setTimeout(r,n))),this.apiKey=t.apiKey,this.sdkVersion=t.sdkVersion,this.flushIntervalMs>0&&(this.flushTimer=setInterval(()=>{this.flush()},this.flushIntervalMs)),this.registerBrowserTeardown()}enqueue(t){if(this.isStopped||this.isShuttingDown){this.logger.warn("[Waniwani] Tracking transport is stopped, dropping event %s",t.id);return}if(this.buffer.length>=this.maxBufferSize){let n=this.buffer.length-this.maxBufferSize+1;this.buffer.splice(0,n),this.logger.warn("[Waniwani] Tracking buffer overflow, dropped %d oldest event(s)",n)}if(this.buffer.push(t),this.buffer.length>=this.maxBatchSize){this.flush();return}this.scheduleMicroFlush()}pendingEvents(){return this.buffer.length+this.inFlightCount}async flush(){return this.flushInFlight?this.flushInFlight:(this.flushInFlight=this.flushLoop().finally(()=>{this.flushInFlight=void 0}),this.flushInFlight)}async shutdown(t){this.isShuttingDown=!0,this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=void 0),this.flushScheduledTimer&&(clearTimeout(this.flushScheduledTimer),this.flushScheduledTimer=void 0,this.flushScheduled=!1);let n=t?.timeoutMs??this.shutdownTimeoutMs,r=this.flush();if(!Number.isFinite(n)||n<=0)return await r,this.isStopped=!0,{timedOut:!1,pendingEvents:this.pendingEvents()};let i=Symbol("shutdown-timeout");return await Promise.race([r.then(()=>"flushed"),this.sleep(n).then(()=>i)])===i?(this.isStopped=!0,{timedOut:!0,pendingEvents:this.pendingEvents()}):(this.isStopped=!0,{timedOut:!1,pendingEvents:this.pendingEvents()})}registerBrowserTeardown(){if(typeof document>"u"||typeof window>"u")return;let t=()=>{document.visibilityState==="hidden"&&this.keepaliveFlush()};document.addEventListener("visibilitychange",t),window.addEventListener("pagehide",()=>this.keepaliveFlush())}keepaliveFlush(){if(this.isStopped||this.buffer.length===0)return;let t=this.buffer.splice(0,this.buffer.length);this.sendKeepaliveChunk(t)}sendKeepaliveChunk(t){let n=JSON.stringify(this.makeBatchRequest(t));if(n.length>6e4&&t.length>1){let r=Math.ceil(t.length/2);this.sendKeepaliveChunk(t.slice(0,r)),this.sendKeepaliveChunk(t.slice(r));return}try{this.fetchFn(this.endpointUrl,{method:"POST",headers:this.requestHeaders(),body:n,keepalive:!0}).catch(()=>{})}catch{}}requestHeaders(){let t={"Content-Type":"application/json","X-WaniWani-SDK":W};return this.apiKey&&(t.Authorization=`Bearer ${this.apiKey}`),t}scheduleMicroFlush(){this.flushScheduled||(this.flushScheduled=!0,this.flushScheduledTimer=setTimeout(()=>{this.flushScheduledTimer=void 0,this.flushScheduled=!1,this.flush()},0))}async flushLoop(){for(;this.buffer.length>0&&!this.isStopped;){let t=this.buffer.splice(0,this.maxBatchSize);await this.sendBatchWithRetry(t)}}async sendBatchWithRetry(t){let n=0,r=t;for(;r.length>0&&!this.isStopped;){this.inFlightCount=r.length;let i=await this.sendBatchOnce(r);switch(this.inFlightCount=0,i.kind){case"success":return;case"auth":this.stopTransportForAuthFailure(i.status,r.length);return;case"permanent":this.logger.error("[Waniwani] Dropping %d event(s) after permanent failure: %s",r.length,i.reason);return;case"retryable":if(n>=this.maxRetries){this.logger.error("[Waniwani] Dropping %d event(s) after retry exhaustion: %s",r.length,i.reason);return}await this.sleep(this.backoffDelayMs(n)),n+=1;continue;case"partial":if(i.permanent.length>0&&this.logger.error("[Waniwani] Dropping %d event(s) rejected as permanent",i.permanent.length),i.retryable.length===0)return;if(n>=this.maxRetries){this.logger.error("[Waniwani] Dropping %d retryable event(s) after retry exhaustion",i.retryable.length);return}r=i.retryable,await this.sleep(this.backoffDelayMs(n)),n+=1;continue}}}async sendBatchOnce(t){let n;try{n=await this.fetchFn(this.endpointUrl,{method:"POST",headers:this.requestHeaders(),body:JSON.stringify(this.makeBatchRequest(t))})}catch(s){return{kind:"retryable",reason:me(s)}}if(pe.has(n.status))return{kind:"auth",status:n.status};if(le.has(n.status))return{kind:"retryable",reason:`HTTP ${n.status}`};if(!n.ok)return{kind:"permanent",reason:`HTTP ${n.status}`};let r=await he(n);if(!r?.rejected||r.rejected.length===0)return{kind:"success"};let i=this.classifyRejectedEvents(t,r.rejected);return i.retryable.length===0&&i.permanent.length===0?{kind:"success"}:{kind:"partial",retryable:i.retryable,permanent:i.permanent}}makeBatchRequest(t){return{sentAt:this.now().toISOString(),source:{sdk:W,version:this.sdkVersion??"0.0.0"},events:t}}classifyRejectedEvents(t,n){let r=new Map(t.map(a=>[a.id,a])),i=[],s=[];for(let a of n){let o=r.get(a.eventId);if(o){if(fe(a)){i.push(o);continue}s.push(o)}}return{retryable:i,permanent:s}}backoffDelayMs(t){let n=this.retryBaseDelayMs*2**t;return Math.min(n,this.retryMaxDelayMs)}stopTransportForAuthFailure(t,n){this.isStopped=!0;let r=this.buffer.length;this.buffer.splice(0,r),this.logger.error("[Waniwani] Auth failure (HTTP %d). Stopping tracking transport and dropping %d queued event(s)",t,n+r)}};function fe(e){if(e.retryable===!0)return!0;let t=e.code.toLowerCase();return t.includes("timeout")||t.includes("temporary")||t.includes("unavailable")||t.includes("rate_limit")||t.includes("transient")||t.includes("server")}async function he(e){let t=await e.text();if(t)try{return JSON.parse(t)}catch{return}}function ge(e,t){let n=e.endsWith("/")?e:`${e}/`,r=t.startsWith("/")?t.slice(1):t;return`${n}${r}`}function me(e){return e instanceof Error?e.message:String(e)}function ve(e){let{apiUrl:t,endpointPath:n}=Te(e.endpoint),r=w({apiUrl:t,apiKey:e.token,endpointPath:n,flushIntervalMs:e.flushIntervalMs}),i,s=o=>typeof o=="function"?o():o,a=async o=>{let c=e.identity?.()??{},g=s(e.channelId),d=g===void 0?o.properties:{channelId:g,...o.properties},u=T({sessionId:c.sessionId,visitorId:c.visitorId,externalUserId:c.externalUserId??i,traceId:c.traceId,...o,properties:d,metadata:{...e.metadata,...o.metadata}},{source:s(e.source)});return r.enqueue(u),{eventId:u.id}};return{track:Object.assign(a,k(a)),identify(o,c){return i=o,a({event:"user.identified",externalUserId:o,properties:c})},flush:()=>r.flush(),shutdown:o=>r.shutdown(o)}}function Te(e){let t=new URL(e);return{apiUrl:t.origin,endpointPath:`${t.pathname}${t.search}`}}import{AsyncLocalStorage as ye}from"async_hooks";var K=globalThis,E=K.__waniwaniKbRetrievalStore;E||(E=new ye,K.__waniwaniKbRetrievalStore=E);var Ie=E;function V(e){try{Ie.getStore()?.searches.push(e)}catch{}}var ke="@waniwani/sdk";function j(e){let{apiUrl:t,apiKey:n}=e;function r(){if(!n)throw new Error("WANIWANI_API_KEY is not set");return n}async function i(s,a,o){let c=r(),g=`${t.replace(/\/$/,"")}${a}`,d={Authorization:`Bearer ${c}`,"X-WaniWani-SDK":ke},u={method:s,headers:d};o!==void 0&&(d["Content-Type"]="application/json",u.body=JSON.stringify(o));let f=await fetch(g,u);if(!f.ok){let q=await f.text().catch(()=>"");throw new y(q||`KB API error: HTTP ${f.status}`,f.status)}return(await f.json()).data}return{async ingest(s){return i("POST","/api/mcp/kb/ingest",{files:s})},async search(s,a){let o=await i("POST","/api/mcp/kb/search",{query:s,...a});return V({query:s,resultCount:o.length,results:o.map(c=>({source:c.source,heading:c.heading,score:c.score}))}),o},async sources(){return i("GET","/api/mcp/kb/sources")}}}function N(e){let{apiUrl:t,apiKey:n,tracking:r}=e;function i(){if(!n)throw new Error("WANIWANI_API_KEY is not set");return n}let s=n?w({apiUrl:t,apiKey:n,endpointPath:r.endpointPath,flushIntervalMs:r.flushIntervalMs,maxBatchSize:r.maxBatchSize,maxBufferSize:r.maxBufferSize,maxRetries:r.maxRetries,retryBaseDelayMs:r.retryBaseDelayMs,retryMaxDelayMs:r.retryMaxDelayMs,shutdownTimeoutMs:r.shutdownTimeoutMs}):void 0;function a(d){i();let u=T(d);return!u.correlation.sessionId&&!u.correlation.externalUserId&&!u.correlation.visitorId&&console.warn(`[waniwani] event "${u.name}" has no sessionId, externalUserId, or visitorId; the ingest API requires one and will reject it.`),s?.enqueue(u),{eventId:u.id}}let o=async d=>a(d),c=Object.assign(o,k(o)),g={async identify(d,u,f){i();let S=T({event:"user.identified",externalUserId:d,properties:u,meta:f});return s?.enqueue(S),{eventId:S.id}},track:c,async flush(){i(),await s?.flush()},async shutdown(d){return i(),await s?.shutdown({timeoutMs:d?.timeoutMs??r.shutdownTimeoutMs})??{timedOut:!1,pendingEvents:0}}};return s&&we(g,r.shutdownTimeoutMs),g}function we(e,t){if(typeof process>"u"||typeof process.once!="function"||typeof process.on!="function")return;let n=()=>{e.shutdown({timeoutMs:t})};process.once("beforeExit",n),process.once("SIGINT",n),process.once("SIGTERM",n)}function Ee(e){let n=e??b()??P(),r=n?.apiUrl??process.env.WANIWANI_API_URL??"https://app.waniwani.ai",i=n?.apiKey??process.env.WANIWANI_API_KEY,s={endpointPath:n?.tracking?.endpointPath??"/api/mcp/events/v2/batch",flushIntervalMs:n?.tracking?.flushIntervalMs??1e3,maxBatchSize:n?.tracking?.maxBatchSize??20,maxBufferSize:n?.tracking?.maxBufferSize??1e3,maxRetries:n?.tracking?.maxRetries??3,retryBaseDelayMs:n?.tracking?.retryBaseDelayMs??200,retryMaxDelayMs:n?.tracking?.retryMaxDelayMs??2e3,shutdownTimeoutMs:n?.tracking?.shutdownTimeoutMs??2e3},a={apiUrl:r,apiKey:i,tracking:s},o=N(a),c=j(a);return{...o,kb:c,_config:a}}export{_ as EVENT_TYPES,y as WaniWaniError,ve as createFrontendClient,G as defineConfig,Ee as waniwani};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/error.ts","../src/project-config.ts","../src/tracking/@types.ts","../src/mcp/server/utils.ts","../src/tracking/mapper.ts","../src/tracking/revenue.ts","../src/tracking/transport.ts","../src/tracking/frontend.ts","../src/kb/retrieval-context.ts","../src/kb/client.ts","../src/tracking/index.ts","../src/waniwani.ts"],"sourcesContent":["// Waniwani SDK - Errors\n\nexport class WaniWaniError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"WaniWaniError\";\n\t}\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\n/**\n * Project-level configuration for Waniwani MCP projects.\n *\n * Mirrors the JSON Schema hosted at https://docs.waniwani.ai/waniwani.json.\n * The canonical config file is `waniwani.json` at the project root:\n *\n * ```json\n * {\n * \"$schema\": \"https://docs.waniwani.ai/waniwani.json\",\n * \"orgId\": \"...\",\n * \"projectId\": \"...\"\n * }\n * ```\n *\n * `waniwani()` and the CLI both read this file automatically — no\n * explicit import is required.\n */\nexport interface WaniWaniProjectConfig {\n\t/** URL of the JSON Schema for editor autocomplete. Ignored at runtime. */\n\t$schema?: string;\n\t/** Waniwani organization ID this project belongs to. */\n\torgId?: string;\n\t/** Waniwani MCP project ID. */\n\tprojectId?: string;\n\t/**\n\t * The base URL of the Waniwani API.\n\t * Falls back to the `WANIWANI_API_URL` env var when omitted here, then to\n\t * `https://app.waniwani.ai`.\n\t */\n\tapiUrl?: string;\n\t/**\n\t * Local port the MCP listens on during `waniwani dev`. Overridden by\n\t * `--port`. Defaults to 3000.\n\t */\n\tdevPort?: number;\n}\n\n// ---------------------------------------------------------------------------\n// waniwani.json loader\n// ---------------------------------------------------------------------------\n\nconst CONFIG_FILENAME = \"waniwani.json\";\n\nlet _cached: WaniWaniProjectConfig | null | undefined;\n\n/**\n * Load `waniwani.json` from the current working directory.\n *\n * Returns `null` if the file doesn't exist or the runtime doesn't support\n * synchronous filesystem reads (e.g. edge / worker runtimes). Cached after\n * the first call.\n *\n * @internal\n */\nexport function loadProjectConfig(): WaniWaniProjectConfig | null {\n\tif (_cached !== undefined) {\n\t\treturn _cached;\n\t}\n\n\ttry {\n\t\tconst filePath = resolve(process.cwd(), CONFIG_FILENAME);\n\t\tif (!existsSync(filePath)) {\n\t\t\t_cached = null;\n\t\t\treturn null;\n\t\t}\n\t\tconst raw = readFileSync(filePath, \"utf-8\");\n\t\t_cached = JSON.parse(raw) as WaniWaniProjectConfig;\n\t\treturn _cached;\n\t} catch {\n\t\t_cached = null;\n\t\treturn null;\n\t}\n}\n\n/**\n * Reset the cached config. Test-only.\n *\n * @internal\n */\nexport function resetProjectConfigCache(): void {\n\t_cached = undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Legacy: defineConfig + globalThis registration\n// ---------------------------------------------------------------------------\n\nconst GLOBAL_KEY = \"__waniwani_config__\" as const;\n\n/**\n * Register a Waniwani project configuration on `globalThis`.\n *\n * @deprecated Create a `waniwani.json` at the project root instead. The SDK\n * and CLI both auto-load that file — no `defineConfig` call required.\n * See https://docs.waniwani.ai/waniwani.json for the schema.\n */\nexport function defineConfig(\n\tconfig: WaniWaniProjectConfig,\n): WaniWaniProjectConfig {\n\t(globalThis as Record<string, unknown>)[GLOBAL_KEY] = config;\n\treturn config;\n}\n\n/**\n * Retrieve the globally registered config (set by `defineConfig`).\n *\n * @deprecated Use `loadProjectConfig()` instead. `defineConfig` is going away.\n * @internal\n */\nexport function getGlobalConfig(): WaniWaniProjectConfig | undefined {\n\treturn (globalThis as Record<string, unknown>)[GLOBAL_KEY] as\n\t\t| WaniWaniProjectConfig\n\t\t| undefined;\n}\n","// Tracking Module Types\n\nimport type { KbSearchTrace } from \"../kb/types.js\";\n\n// ============================================\n// Event Types\n// ============================================\n\n/**\n * Every event name in the typed taxonomy, as a runtime list. Single source of\n * truth for {@link EventType} and for surfaces that need to recognize\n * first-class names at runtime (the widget transport passes these through\n * verbatim instead of namespacing them as custom widget events).\n */\nexport const EVENT_TYPES = [\n\t\"session.started\",\n\t\"page.viewed\",\n\t\"tool.called\",\n\t\"link.clicked\",\n\t// Emitted automatically by the frontend client once per widget mount.\n\t\"widget_render\",\n\t\"user.identified\",\n\t// Revenue taxonomy (WAN-387): typed first-class revenue events.\n\t\"price_shown\",\n\t\"prices_compared\",\n\t\"option_selected\",\n\t\"lead_qualified\",\n\t\"converted\",\n] as const;\n\nexport type EventType = (typeof EVENT_TYPES)[number];\n\n// ============================================\n// Event Properties\n// ============================================\n\n/**\n * Properties for `page.viewed` — emitted once when the chat widget initializes\n * on a host page (the top of the funnel). Attributed to the anonymous\n * `visitorId` (mapped to `externalUserId`), never to a session: a page view\n * must not create a session, otherwise sessions would equal page views and the\n * \"landed → started a conversation\" funnel collapses.\n */\nexport interface PageViewedProperties {\n\t/** Full URL of the host page the widget loaded on. */\n\turl?: string;\n\t/** Referrer of the host page, if any. */\n\treferrer?: string;\n\t/** Embed mode the widget initialized in. */\n\tmode?: \"inline\" | \"floating\";\n\t/** Resolved device type from the visitor context. */\n\tdeviceType?: \"mobile\" | \"tablet\" | \"desktop\";\n\t/** Primary browser language (BCP-47). */\n\tlanguage?: string;\n\t/** IANA timezone of the visitor. */\n\ttimezone?: string;\n}\n\nexport interface ToolCalledProperties {\n\tname?: string;\n\ttype?: \"pricing\" | \"product_info\" | \"availability\" | \"support\" | \"other\";\n\t/** Retrieval traces for kb.search() calls made inside this tool handler. */\n\tkbSearch?: KbSearchTrace[];\n}\n\nexport interface LinkClickedProperties {\n\turl?: string;\n}\n\n// ============================================\n// Revenue Taxonomy Properties (WAN-387)\n// ============================================\n\nexport interface PriceShownProperties {\n\tamount: number;\n\tcurrency: string;\n\titemId?: string;\n\tlabel?: string;\n}\n\nexport interface ComparedPriceOption {\n\tid: string;\n\tamount: number;\n\tcurrency: string;\n}\n\nexport interface PricesComparedProperties {\n\toptions: ComparedPriceOption[];\n}\n\nexport interface OptionSelectedProperties {\n\tid: string;\n\tamount: number;\n\tcurrency: string;\n}\n\n/**\n * Properties for `lead_qualified` — your code declaring that a person met your\n * qualification bar (finished the qualifying questions, requested a demo,\n * matched your target profile). Sharing an email mid-conversation is\n * `identify(userId, { email })`, not a qualified lead.\n */\nexport interface LeadQualifiedProperties {\n\t/**\n\t * Your own lead id (e.g. the record id your lead-gen API returns). The\n\t * strongest dedup key and the stable reference back to your system.\n\t */\n\texternalId?: string;\n\t/** The lead's email, if known. */\n\temail?: string;\n\t/** The lead's name, if known. */\n\tname?: string;\n}\n\nexport interface ConvertedProperties {\n\tamount: number;\n\tcurrency: string;\n\t/** When the conversion actually happened — for backdated off-platform sales. */\n\toccurredAt?: string;\n}\n\n// ============================================\n// Legacy + Modern Track Inputs\n// ============================================\n\ninterface TrackingContext {\n\t/**\n\t * MCP request metadata passed through to the API.\n\t *\n\t * Location varies by MCP library:\n\t * - `@vercel/mcp-handler`: `extra._meta`\n\t * - `@modelcontextprotocol/sdk`: `request.params._meta`\n\t */\n\tmeta?: Record<string, unknown>;\n\t/** Legacy metadata field supported for backward compatibility. */\n\tmetadata?: Record<string, unknown>;\n\t/** Optional explicit correlation fields. */\n\tsessionId?: string;\n\ttraceId?: string;\n\trequestId?: string;\n\tcorrelationId?: string;\n\texternalUserId?: string;\n\t/**\n\t * Anonymous visitor id (the analytics \"device id\"). Counts as identity on\n\t * its own: the ingest API accepts `sessionId`, `externalUserId`, or\n\t * `visitorId`. The chat widget uses it for pre-session events like\n\t * `page.viewed`.\n\t */\n\tvisitorId?: string;\n\t/** Optional explicit envelope fields. */\n\teventId?: string;\n\ttimestamp?: string | Date;\n\tsource?: string;\n}\n\n/**\n * Modern tracking shape (preferred).\n */\ninterface BaseTrackEvent extends TrackingContext {\n\tevent: EventType;\n\tproperties?: Record<string, unknown>;\n}\n\nexport type TrackEvent =\n\t| ({ event: \"session.started\" } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"page.viewed\";\n\t\t\tproperties?: PageViewedProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"tool.called\";\n\t\t\tproperties?: ToolCalledProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"link.clicked\";\n\t\t\tproperties?: LinkClickedProperties;\n\t } & BaseTrackEvent)\n\t| ({ event: \"user.identified\" } & BaseTrackEvent)\n\t| ({ event: \"widget_render\" } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"price_shown\";\n\t\t\tproperties?: PriceShownProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"prices_compared\";\n\t\t\tproperties?: PricesComparedProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"option_selected\";\n\t\t\tproperties?: OptionSelectedProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"lead_qualified\";\n\t\t\tproperties?: LeadQualifiedProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"converted\";\n\t\t\tproperties?: ConvertedProperties;\n\t } & BaseTrackEvent);\n\n/**\n * Legacy tracking shape supported for existing integrations.\n */\nexport interface LegacyTrackEvent extends TrackingContext {\n\teventType: EventType;\n\tproperties?: Record<string, unknown>;\n\ttoolName?: string;\n\ttoolType?: ToolCalledProperties[\"type\"];\n\tlinkUrl?: string;\n}\n\n/**\n * Public track input accepted by `client.track()`.\n */\nexport type TrackInput = TrackEvent | LegacyTrackEvent;\n\n// ============================================\n// Revenue Helper Inputs (WAN-386)\n// ============================================\n\n// Each helper input is its typed properties plus the shared tracking context\n// (sessionId / externalUserId / meta / …). At least one identity field\n// (sessionId or externalUserId, possibly derived from `meta`) must be present —\n// the ingest API rejects events without one.\n\nexport interface RevenuePriceShownInput\n\textends TrackingContext,\n\t\tPriceShownProperties {}\n\nexport interface RevenuePricesComparedInput\n\textends TrackingContext,\n\t\tPricesComparedProperties {}\n\nexport interface RevenueOptionSelectedInput\n\textends TrackingContext,\n\t\tOptionSelectedProperties {}\n\n/**\n * Input for `track.leadQualified()` — identity only (`externalId`, `email`,\n * `name`) plus the shared tracking context. There is no acquisition-source\n * property; the envelope `source` (the origin channel) is set automatically.\n */\nexport interface RevenueLeadQualifiedInput\n\textends TrackingContext,\n\t\tLeadQualifiedProperties {}\n\nexport interface RevenueConvertedInput\n\textends TrackingContext,\n\t\tConvertedProperties {}\n\n/**\n * Revenue-oriented helpers, flat on `client.track.*` (e.g.\n * `client.track.priceShown()`, `client.track.converted()`). Decoupled from\n * product primitives — each maps to a typed first-class revenue event.\n */\nexport interface RevenueTrackingApi {\n\tpriceShown: (input: RevenuePriceShownInput) => Promise<{ eventId: string }>;\n\tpricesCompared: (\n\t\tinput: RevenuePricesComparedInput,\n\t) => Promise<{ eventId: string }>;\n\toptionSelected: (\n\t\tinput: RevenueOptionSelectedInput,\n\t) => Promise<{ eventId: string }>;\n\tleadQualified: (\n\t\tinput?: RevenueLeadQualifiedInput,\n\t) => Promise<{ eventId: string }>;\n\tconverted: (input: RevenueConvertedInput) => Promise<{ eventId: string }>;\n}\n\n/**\n * The callable form of `track` — emit one event, without the flat revenue\n * helpers. What internal code and custom/injected trackers need; lets them\n * avoid implementing the revenue methods.\n */\nexport type CallableTrack = (event: TrackInput) => Promise<{ eventId: string }>;\n\n/**\n * `client.track` — callable for generic events (`track(event)`), with the\n * revenue helpers attached flat: `track.priceShown()`, `track.leadQualified()`,\n * `track.converted()`, etc.\n */\nexport interface TrackFn extends RevenueTrackingApi {\n\t(event: TrackInput): Promise<{ eventId: string }>;\n}\n\n// ============================================\n// Runtime Config\n// ============================================\n\nexport interface TrackingConfig {\n\t/** Events API V2 endpoint path. */\n\tendpointPath?: string;\n\t/** Periodic flush interval for buffered events. */\n\tflushIntervalMs?: number;\n\t/** Max events per HTTP batch send. */\n\tmaxBatchSize?: number;\n\t/** Max in-memory buffer size before oldest items are dropped. */\n\tmaxBufferSize?: number;\n\t/** Number of retries for retryable failures. */\n\tmaxRetries?: number;\n\t/** Retry backoff base delay. */\n\tretryBaseDelayMs?: number;\n\t/** Retry backoff max delay. */\n\tretryMaxDelayMs?: number;\n\t/** Default shutdown timeout when none is provided. */\n\tshutdownTimeoutMs?: number;\n}\n\nexport interface TrackingShutdownOptions {\n\ttimeoutMs?: number;\n}\n\nexport interface TrackingShutdownResult {\n\ttimedOut: boolean;\n\tpendingEvents: number;\n}\n\n// ============================================\n// Client\n// ============================================\n\n/**\n * Tracking module methods for WaniWaniClient.\n */\nexport interface TrackingClient {\n\t/**\n\t * Send a one-shot identify event for a user.\n\t * userId can be any string: an email, an internal ID, etc.\n\t */\n\tidentify: (\n\t\tuserId: string,\n\t\tproperties?: Record<string, unknown>,\n\t\tmeta?: Record<string, unknown>,\n\t) => Promise<{ eventId: string }>;\n\t/**\n\t * Track an event using modern or legacy input shape.\n\t * Returns a deterministic event id immediately after enqueue.\n\t *\n\t * Also exposes the revenue helpers flat: `client.track.priceShown()`,\n\t * `client.track.leadQualified()`, `client.track.converted()`, etc.\n\t */\n\ttrack: TrackFn;\n\t/**\n\t * Flush all currently buffered events.\n\t */\n\tflush: () => Promise<void>;\n\t/**\n\t * Flush and stop the transport.\n\t */\n\tshutdown: (\n\t\toptions?: TrackingShutdownOptions,\n\t) => Promise<TrackingShutdownResult>;\n}\n","// ============================================================================\n// Meta key extraction helpers\n// ============================================================================\n\n/** Pick the first non-empty string value from `meta` matching the given keys. */\nfunction pickFirst(\n\tmeta: Record<string, unknown>,\n\tkeys: readonly string[],\n): string | undefined {\n\tfor (const key of keys) {\n\t\tconst value = meta[key];\n\t\tif (typeof value === \"string\" && value.length > 0) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n// --- Key lists (ordered by priority) ---\n\nconst SESSION_ID_KEYS = [\n\t\"waniwani/sessionId\",\n\t\"openai/sessionId\",\n\t\"openai/session\",\n\t\"sessionId\",\n\t\"conversationId\",\n\t\"mcp-session-id\",\n] as const;\n\nconst REQUEST_ID_KEYS = [\n\t\"waniwani/requestId\",\n\t\"openai/requestId\",\n\t\"requestId\",\n\t\"mcp/requestId\",\n] as const;\n\nconst TRACE_ID_KEYS = [\n\t\"waniwani/traceId\",\n\t\"openai/traceId\",\n\t\"traceId\",\n\t\"mcp/traceId\",\n\t\"openai/requestId\",\n\t\"requestId\",\n] as const;\n\nconst EXTERNAL_USER_ID_KEYS = [\n\t\"waniwani/userId\",\n\t\"openai/userId\",\n\t\"externalUserId\",\n\t\"userId\",\n\t\"actorId\",\n] as const;\n\nconst VISITOR_ID_KEYS = [\"waniwani/visitorId\", \"visitorId\"] as const;\n\nconst CORRELATION_ID_KEYS = [\"correlationId\", \"openai/requestId\"] as const;\n\nconst TURN_COUNT_KEYS = [\"waniwani/turnCount\"] as const;\n\n/** Meta key for flow execution path (nodesVisited, flowId). */\nexport const FLOW_META_KEY = \"waniwani/flow\" as const;\n\n/**\n * Meta key under which `withWaniwani` injects the widget tracking config\n * (`{ endpoint, token, sessionId, source, geoLocation }`) into tool response\n * `_meta`. The frontend client reads it to track without any manual wiring.\n * Older SDKs wrote the same object under the bare `waniwani` key.\n */\nexport const WIDGET_CONFIG_META_KEY = \"waniwani/widget\" as const;\n\n// --- Extractors ---\n\nexport function extractSessionId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, SESSION_ID_KEYS) : undefined;\n}\n\nexport function extractRequestId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, REQUEST_ID_KEYS) : undefined;\n}\n\nexport function extractTraceId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, TRACE_ID_KEYS) : undefined;\n}\n\nexport function extractExternalUserId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, EXTERNAL_USER_ID_KEYS) : undefined;\n}\n\nexport function extractVisitorId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, VISITOR_ID_KEYS) : undefined;\n}\n\nexport function extractCorrelationId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, CORRELATION_ID_KEYS) : undefined;\n}\n\n/**\n * Number of user messages in the current chat session, as counted by the\n * Waniwani app before dispatching the MCP request. Useful for MCPs that\n * gate behavior on conversation length (e.g. compulsory email verification\n * after N turns) without each MCP having to track turn state itself.\n *\n * Forwarded as `waniwani/turnCount` (non-negative integer).\n */\nexport function extractTurnCount(\n\tmeta: Record<string, unknown> | undefined,\n): number | undefined {\n\tif (!meta) {\n\t\treturn undefined;\n\t}\n\tfor (const key of TURN_COUNT_KEYS) {\n\t\tconst value = meta[key];\n\t\tif (typeof value === \"number\" && Number.isFinite(value) && value >= 0) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nconst SOURCE_SESSION_KEYS = [\n\t{ key: \"openai/sessionId\", source: \"chatgpt\" },\n\t{ key: \"openai/session\", source: \"chatgpt\" },\n] as const;\n\n/**\n * Client identifiers advertised via MCP `initialize` → `clientInfo.name` that\n * the SDK maps to a known source. Match is case-insensitive substring on the\n * advertised name, so this catches \"Claude\", \"Claude Code\", \"claude-ai\", etc.\n * without having to enumerate every surface.\n *\n * ChatGPT is primarily identified by its `openai/*` session key (see\n * SOURCE_SESSION_KEYS), but a session without that key still resolves here from\n * the advertised client name so assistant traffic is attributed consistently.\n * First match wins, so order the needles most-specific first.\n */\nconst CLIENT_INFO_NAME_SOURCES: ReadonlyArray<{\n\tneedle: string;\n\tsource: string;\n}> = [\n\t{ needle: \"claude\", source: \"claude\" },\n\t{ needle: \"chatgpt\", source: \"chatgpt\" },\n\t{ needle: \"openai\", source: \"chatgpt\" },\n\t{ needle: \"gemini\", source: \"gemini\" },\n];\n\nexport type ExtractSourceClientInfo = {\n\tname?: string;\n\tversion?: string;\n};\n\nexport function extractSource(\n\tmeta: Record<string, unknown> | undefined,\n\tclientInfo?: ExtractSourceClientInfo,\n): string | undefined {\n\tif (meta) {\n\t\tconst explicit = meta[\"waniwani/source\"];\n\t\tif (typeof explicit === \"string\" && explicit.length > 0) {\n\t\t\treturn explicit;\n\t\t}\n\t\tfor (const { key, source } of SOURCE_SESSION_KEYS) {\n\t\t\tconst value = meta[key];\n\t\t\tif (typeof value === \"string\" && value.length > 0) {\n\t\t\t\treturn source;\n\t\t\t}\n\t\t}\n\t}\n\t// Fall back to MCP `initialize` clientInfo.name when no _meta key matches.\n\t// Claude surfaces (Code, Desktop, claude.ai connector, Anthropic API MCP\n\t// connector) don't expose a namespaced session id in _meta but do advertise\n\t// themselves in the handshake. Per-MCP-session clientInfo is captured by the\n\t// MCP transport and exposed via server.getClientVersion().\n\tconst name = clientInfo?.name;\n\tif (typeof name === \"string\" && name.length > 0) {\n\t\tconst lower = name.toLowerCase();\n\t\tfor (const { needle, source } of CLIENT_INFO_NAME_SOURCES) {\n\t\t\tif (lower.includes(needle)) {\n\t\t\t\treturn source;\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * HTTP request headers that identify a known caller when neither `_meta` nor\n * `clientInfo` carries a source. This is the most robust Claude signal: every\n * Claude surface sends `User-Agent: Claude-User` and `x-anthropic-client` on\n * MCP HTTP requests, whereas the MCP transport carries no session id and the\n * `initialize` `clientInfo` may not be surfaced on stateless deployments.\n */\nfunction headerValue(\n\theaders: Record<string, unknown>,\n\tkey: string,\n): string | undefined {\n\t// HTTP header names are case-insensitive; the transport may preserve the\n\t// original casing, so match the key case-insensitively rather than assuming\n\t// it was lowercased.\n\tconst lowerKey = key.toLowerCase();\n\tlet raw = headers[key] ?? headers[lowerKey];\n\tif (raw === undefined) {\n\t\tfor (const headerKey of Object.keys(headers)) {\n\t\t\tif (headerKey.toLowerCase() === lowerKey) {\n\t\t\t\traw = headers[headerKey];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tconst value = Array.isArray(raw) ? raw[0] : raw;\n\treturn typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nexport function extractSourceFromHeaders(\n\theaders: Record<string, unknown> | undefined,\n): string | undefined {\n\tif (!headers) {\n\t\treturn undefined;\n\t}\n\tconst userAgent = headerValue(headers, \"user-agent\");\n\tconst anthropicClient = headerValue(headers, \"x-anthropic-client\");\n\tif (\n\t\t(userAgent && /claude/i.test(userAgent)) ||\n\t\t(anthropicClient && /claude|anthropic/i.test(anthropicClient))\n\t) {\n\t\treturn \"claude\";\n\t}\n\t// ChatGPT / OpenAI surfaces without an `openai/*` session key still send an\n\t// OpenAI user-agent (e.g. `ChatGPT-User`), so attribute them here too.\n\tif (userAgent && /chatgpt|openai/i.test(userAgent)) {\n\t\treturn \"chatgpt\";\n\t}\n\treturn undefined;\n}\n","import {\n\textractCorrelationId,\n\textractExternalUserId,\n\textractRequestId,\n\textractSessionId,\n\textractSource,\n\textractTraceId,\n\textractVisitorId,\n} from \"../mcp/server/utils.js\";\nimport type { EventType, LegacyTrackEvent, TrackInput } from \"./@types.js\";\nimport type { V2CorrelationIds, V2EventEnvelope } from \"./v2-types.js\";\n\nconst DEFAULT_SOURCE = \"@waniwani/sdk\";\n\nexport interface MapTrackEventOptions {\n\tnow?: () => Date;\n\tgenerateId?: () => string;\n\tsource?: string;\n}\n\nexport function mapTrackEventToV2(\n\tinput: TrackInput,\n\toptions: MapTrackEventOptions = {},\n): V2EventEnvelope {\n\tconst now = options.now ?? (() => new Date());\n\tconst generateId = options.generateId ?? createEventId;\n\tconst eventName = resolveEventName(input);\n\tconst meta = toRecord(input.meta);\n\tconst metadata = toRecord(input.metadata);\n\tconst correlation = resolveCorrelationIds(input, meta);\n\tconst eventId = takeNonEmptyString(input.eventId) ?? generateId();\n\tconst timestamp = normalizeTimestamp(input.timestamp, now);\n\tconst source =\n\t\ttakeNonEmptyString(input.source) ??\n\t\textractSource(meta) ??\n\t\toptions.source ??\n\t\tDEFAULT_SOURCE;\n\tconst rawLegacy = isLegacyTrackEvent(input) ? { ...input } : undefined;\n\n\tconst mappedMetadata: Record<string, unknown> = {\n\t\t...metadata,\n\t};\n\tif (Object.keys(meta).length > 0) {\n\t\tmappedMetadata.meta = meta;\n\t}\n\tif (rawLegacy) {\n\t\tmappedMetadata.rawLegacy = rawLegacy;\n\t}\n\n\treturn {\n\t\tid: eventId,\n\t\ttype: \"mcp.event\",\n\t\tname: eventName,\n\t\tsource,\n\t\ttimestamp,\n\t\tcorrelation,\n\t\tproperties: mapProperties(input, eventName),\n\t\tmetadata: mappedMetadata,\n\t\trawLegacy,\n\t};\n}\n\nexport function createEventId(): string {\n\tif (\n\t\ttypeof crypto !== \"undefined\" &&\n\t\ttypeof crypto.randomUUID === \"function\"\n\t) {\n\t\treturn `evt_${crypto.randomUUID()}`;\n\t}\n\n\treturn `evt_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;\n}\n\nfunction mapProperties(\n\tinput: TrackInput,\n\teventName: EventType,\n): Record<string, unknown> {\n\tif (!isLegacyTrackEvent(input)) {\n\t\treturn toRecord(input.properties);\n\t}\n\n\tconst legacyProperties = mapLegacyProperties(input, eventName);\n\tconst explicitProperties = toRecord(input.properties);\n\treturn {\n\t\t...legacyProperties,\n\t\t...explicitProperties,\n\t};\n}\n\nfunction mapLegacyProperties(\n\tinput: LegacyTrackEvent,\n\teventName: EventType,\n): Record<string, unknown> {\n\tswitch (eventName) {\n\t\tcase \"tool.called\": {\n\t\t\tconst properties: Record<string, unknown> = {};\n\t\t\tif (takeNonEmptyString(input.toolName)) {\n\t\t\t\tproperties.name = input.toolName;\n\t\t\t}\n\t\t\tif (takeNonEmptyString(input.toolType)) {\n\t\t\t\tproperties.type = input.toolType;\n\t\t\t}\n\t\t\treturn properties;\n\t\t}\n\t\tcase \"link.clicked\": {\n\t\t\tconst properties: Record<string, unknown> = {};\n\t\t\tif (takeNonEmptyString(input.linkUrl)) {\n\t\t\t\tproperties.url = input.linkUrl;\n\t\t\t}\n\t\t\treturn properties;\n\t\t}\n\t\tdefault:\n\t\t\treturn {};\n\t}\n}\n\nfunction resolveEventName(input: TrackInput): EventType {\n\treturn isLegacyTrackEvent(input) ? input.eventType : input.event;\n}\n\nfunction resolveCorrelationIds(\n\tinput: TrackInput,\n\tmeta: Record<string, unknown>,\n): V2CorrelationIds {\n\tconst requestId =\n\t\ttakeNonEmptyString(input.requestId) ?? extractRequestId(meta);\n\n\tconst sessionId =\n\t\ttakeNonEmptyString(input.sessionId) ?? extractSessionId(meta);\n\n\tconst traceId = takeNonEmptyString(input.traceId) ?? extractTraceId(meta);\n\n\tconst externalUserId =\n\t\ttakeNonEmptyString(input.externalUserId) ?? extractExternalUserId(meta);\n\n\tconst visitorId =\n\t\ttakeNonEmptyString(input.visitorId) ?? extractVisitorId(meta);\n\n\tconst correlationId =\n\t\ttakeNonEmptyString(input.correlationId) ??\n\t\textractCorrelationId(meta) ??\n\t\trequestId;\n\n\tconst correlation: V2CorrelationIds = {};\n\tif (sessionId) {\n\t\tcorrelation.sessionId = sessionId;\n\t}\n\tif (traceId) {\n\t\tcorrelation.traceId = traceId;\n\t}\n\tif (requestId) {\n\t\tcorrelation.requestId = requestId;\n\t}\n\tif (correlationId) {\n\t\tcorrelation.correlationId = correlationId;\n\t}\n\tif (externalUserId) {\n\t\tcorrelation.externalUserId = externalUserId;\n\t}\n\tif (visitorId) {\n\t\tcorrelation.visitorId = visitorId;\n\t}\n\treturn correlation;\n}\n\nfunction normalizeTimestamp(\n\tinput: string | Date | undefined,\n\tnow: () => Date,\n): string {\n\tif (input instanceof Date) {\n\t\treturn input.toISOString();\n\t}\n\tif (typeof input === \"string\") {\n\t\tconst date = new Date(input);\n\t\tif (!Number.isNaN(date.getTime())) {\n\t\t\treturn date.toISOString();\n\t\t}\n\t}\n\treturn now().toISOString();\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn {};\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction takeNonEmptyString(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tif (value.trim().length === 0) {\n\t\treturn undefined;\n\t}\n\treturn value;\n}\n\nfunction isLegacyTrackEvent(input: TrackInput): input is LegacyTrackEvent {\n\treturn \"eventType\" in input;\n}\n","import type { RevenueTrackingApi, TrackInput } from \"./@types.js\";\n\n/** Enqueue one event and resolve to its id. */\ntype Emit = (event: TrackInput) => Promise<{ eventId: string }>;\n\n/**\n * Build the flat `track.*` revenue helpers over an `emit` function. Shared\n * by the top-level client and the request-scoped client (which injects meta\n * into `emit`) so both expose the same revenue API (WAN-386).\n *\n * Each helper maps its flat input to a typed first-class revenue event; the\n * remaining tracking context (sessionId / externalUserId / meta) is forwarded\n * as-is. Identity (one of those, or meta-derived) is required by the ingest\n * API — see the warning emitted from the client's `emit`.\n */\nexport function createRevenueApi(emit: Emit): RevenueTrackingApi {\n\treturn {\n\t\tpriceShown: ({ amount, currency, itemId, label, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"price_shown\",\n\t\t\t\tproperties: { amount, currency, itemId, label },\n\t\t\t\t...context,\n\t\t\t}),\n\t\tpricesCompared: ({ options, ...context }) =>\n\t\t\temit({ event: \"prices_compared\", properties: { options }, ...context }),\n\t\toptionSelected: ({ id, amount, currency, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"option_selected\",\n\t\t\t\tproperties: { id, amount, currency },\n\t\t\t\t...context,\n\t\t\t}),\n\t\tleadQualified: (input) => {\n\t\t\tconst { externalId, email, name, ...context } = input ?? {};\n\t\t\treturn emit({\n\t\t\t\tevent: \"lead_qualified\",\n\t\t\t\tproperties: { externalId, email, name },\n\t\t\t\t...context,\n\t\t\t});\n\t\t},\n\t\tconverted: ({ amount, currency, occurredAt, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"converted\",\n\t\t\t\tproperties: { amount, currency, occurredAt },\n\t\t\t\t...context,\n\t\t\t}),\n\t};\n}\n","import type {\n\tTrackingShutdownOptions,\n\tTrackingShutdownResult,\n} from \"./@types.js\";\nimport type {\n\tV2BatchRejectedEvent,\n\tV2BatchRequest,\n\tV2BatchResponse,\n\tV2EventEnvelope,\n} from \"./v2-types.js\";\n\ntype FetchFn = (\n\tinput: URL | RequestInfo,\n\tinit?: RequestInit,\n) => Promise<Response>;\n\nconst DEFAULT_ENDPOINT_PATH = \"/api/mcp/events/v2/batch\";\nconst DEFAULT_FLUSH_INTERVAL_MS = 1_000;\nconst DEFAULT_MAX_BATCH_SIZE = 20;\nconst DEFAULT_MAX_BUFFER_SIZE = 1_000;\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_RETRY_BASE_DELAY_MS = 200;\nconst DEFAULT_RETRY_MAX_DELAY_MS = 2_000;\nconst DEFAULT_SHUTDOWN_TIMEOUT_MS = 2_000;\nconst SDK_NAME = \"@waniwani/sdk\";\n// Keepalive requests are capped by browsers (64KiB in-flight quota); stay under it.\nconst TEARDOWN_MAX_BODY_BYTES = 60_000;\n\nconst AUTH_FAILURE_STATUS = new Set([401, 403]);\nconst RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);\n\ninterface Logger {\n\twarn: (message: string, ...args: unknown[]) => void;\n\terror: (message: string, ...args: unknown[]) => void;\n}\n\nexport interface V2TransportOptions {\n\tapiUrl: string;\n\t/**\n\t * Bearer credential for the ingest endpoint: secret key (`wwk_`), public\n\t * token (`wwp_`), or widget JWT. Omit only for unauthenticated proxies.\n\t */\n\tapiKey?: string;\n\tendpointPath?: string;\n\tflushIntervalMs?: number;\n\tmaxBatchSize?: number;\n\tmaxBufferSize?: number;\n\tmaxRetries?: number;\n\tretryBaseDelayMs?: number;\n\tretryMaxDelayMs?: number;\n\tshutdownTimeoutMs?: number;\n\tsdkVersion?: string;\n\tfetchFn?: FetchFn;\n\tlogger?: Logger;\n\tnow?: () => Date;\n\tsleep?: (delayMs: number) => Promise<void>;\n}\n\nexport interface V2BatchTransport {\n\tenqueue: (event: V2EventEnvelope) => void;\n\tflush: () => Promise<void>;\n\tshutdown: (\n\t\toptions?: TrackingShutdownOptions,\n\t) => Promise<TrackingShutdownResult>;\n\tpendingEvents: () => number;\n}\n\ntype SendBatchResult =\n\t| { kind: \"success\" }\n\t| { kind: \"retryable\"; reason: string }\n\t| { kind: \"permanent\"; reason: string }\n\t| { kind: \"auth\"; status: number }\n\t| {\n\t\t\tkind: \"partial\";\n\t\t\tretryable: V2EventEnvelope[];\n\t\t\tpermanent: V2EventEnvelope[];\n\t };\n\nexport function createV2BatchTransport(\n\toptions: V2TransportOptions,\n): V2BatchTransport {\n\treturn new BatchingV2Transport(options);\n}\n\nclass BatchingV2Transport implements V2BatchTransport {\n\tprivate readonly endpointUrl: string;\n\tprivate readonly flushIntervalMs: number;\n\tprivate readonly maxBatchSize: number;\n\tprivate readonly maxBufferSize: number;\n\tprivate readonly maxRetries: number;\n\tprivate readonly retryBaseDelayMs: number;\n\tprivate readonly retryMaxDelayMs: number;\n\tprivate readonly shutdownTimeoutMs: number;\n\tprivate readonly sdkVersion?: string;\n\tprivate readonly fetchFn: FetchFn;\n\tprivate readonly logger: Logger;\n\tprivate readonly now: () => Date;\n\tprivate readonly sleep: (delayMs: number) => Promise<void>;\n\tprivate readonly apiKey?: string;\n\n\tprivate readonly buffer: V2EventEnvelope[] = [];\n\tprivate flushTimer: ReturnType<typeof setInterval> | undefined;\n\tprivate flushScheduled = false;\n\tprivate flushScheduledTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate flushInFlight: Promise<void> | undefined;\n\tprivate inFlightCount = 0;\n\tprivate isStopped = false;\n\tprivate isShuttingDown = false;\n\n\tconstructor(options: V2TransportOptions) {\n\t\tthis.endpointUrl = joinUrl(\n\t\t\toptions.apiUrl,\n\t\t\toptions.endpointPath ?? DEFAULT_ENDPOINT_PATH,\n\t\t);\n\t\tthis.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;\n\t\tthis.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;\n\t\tthis.maxBufferSize = options.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;\n\t\tthis.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n\t\tthis.retryBaseDelayMs =\n\t\t\toptions.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;\n\t\tthis.retryMaxDelayMs =\n\t\t\toptions.retryMaxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS;\n\t\tthis.shutdownTimeoutMs =\n\t\t\toptions.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;\n\t\tthis.fetchFn = options.fetchFn ?? fetch;\n\t\tthis.logger = options.logger ?? console;\n\t\tthis.now = options.now ?? (() => new Date());\n\t\tthis.sleep =\n\t\t\toptions.sleep ??\n\t\t\t((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.sdkVersion = options.sdkVersion;\n\n\t\tif (this.flushIntervalMs > 0) {\n\t\t\tthis.flushTimer = setInterval(() => {\n\t\t\t\tvoid this.flush();\n\t\t\t}, this.flushIntervalMs);\n\t\t}\n\n\t\tthis.registerBrowserTeardown();\n\t}\n\n\tenqueue(event: V2EventEnvelope): void {\n\t\tif (this.isStopped || this.isShuttingDown) {\n\t\t\tthis.logger.warn(\n\t\t\t\t\"[Waniwani] Tracking transport is stopped, dropping event %s\",\n\t\t\t\tevent.id,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.buffer.length >= this.maxBufferSize) {\n\t\t\tconst dropCount = this.buffer.length - this.maxBufferSize + 1;\n\t\t\tthis.buffer.splice(0, dropCount);\n\t\t\tthis.logger.warn(\n\t\t\t\t\"[Waniwani] Tracking buffer overflow, dropped %d oldest event(s)\",\n\t\t\t\tdropCount,\n\t\t\t);\n\t\t}\n\n\t\tthis.buffer.push(event);\n\n\t\tif (this.buffer.length >= this.maxBatchSize) {\n\t\t\tvoid this.flush();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.scheduleMicroFlush();\n\t}\n\n\tpendingEvents(): number {\n\t\treturn this.buffer.length + this.inFlightCount;\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tif (this.flushInFlight) {\n\t\t\treturn this.flushInFlight;\n\t\t}\n\t\tthis.flushInFlight = this.flushLoop().finally(() => {\n\t\t\tthis.flushInFlight = undefined;\n\t\t});\n\t\treturn this.flushInFlight;\n\t}\n\n\tasync shutdown(\n\t\toptions?: TrackingShutdownOptions,\n\t): Promise<TrackingShutdownResult> {\n\t\tthis.isShuttingDown = true;\n\t\tif (this.flushTimer) {\n\t\t\tclearInterval(this.flushTimer);\n\t\t\tthis.flushTimer = undefined;\n\t\t}\n\t\tif (this.flushScheduledTimer) {\n\t\t\tclearTimeout(this.flushScheduledTimer);\n\t\t\tthis.flushScheduledTimer = undefined;\n\t\t\tthis.flushScheduled = false;\n\t\t}\n\n\t\tconst timeoutMs = options?.timeoutMs ?? this.shutdownTimeoutMs;\n\t\tconst flushPromise = this.flush();\n\n\t\tif (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n\t\t\tawait flushPromise;\n\t\t\tthis.isStopped = true;\n\t\t\treturn { timedOut: false, pendingEvents: this.pendingEvents() };\n\t\t}\n\n\t\tconst timeoutSignal = Symbol(\"shutdown-timeout\");\n\t\tconst result = await Promise.race([\n\t\t\tflushPromise.then(() => \"flushed\" as const),\n\t\t\tthis.sleep(timeoutMs).then(() => timeoutSignal),\n\t\t]);\n\n\t\tif (result === timeoutSignal) {\n\t\t\tthis.isStopped = true;\n\t\t\treturn { timedOut: true, pendingEvents: this.pendingEvents() };\n\t\t}\n\n\t\tthis.isStopped = true;\n\t\treturn { timedOut: false, pendingEvents: this.pendingEvents() };\n\t}\n\n\t/**\n\t * In browsers, drain the buffer with keepalive fetches when the page is\n\t * hidden or unloading. The regular async flush loop does not survive\n\t * navigation; `keepalive` requests do. No-op outside a DOM environment.\n\t */\n\tprivate registerBrowserTeardown(): void {\n\t\tif (typeof document === \"undefined\" || typeof window === \"undefined\") {\n\t\t\treturn;\n\t\t}\n\n\t\tconst teardownFlush = () => {\n\t\t\tif (document.visibilityState === \"hidden\") {\n\t\t\t\tthis.keepaliveFlush();\n\t\t\t}\n\t\t};\n\t\tdocument.addEventListener(\"visibilitychange\", teardownFlush);\n\t\twindow.addEventListener(\"pagehide\", () => this.keepaliveFlush());\n\t}\n\n\t/** Fire-and-forget flush of everything buffered, chunked under the keepalive body cap. */\n\tprivate keepaliveFlush(): void {\n\t\tif (this.isStopped || this.buffer.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst events = this.buffer.splice(0, this.buffer.length);\n\t\tthis.sendKeepaliveChunk(events);\n\t}\n\n\tprivate sendKeepaliveChunk(events: V2EventEnvelope[]): void {\n\t\tconst body = JSON.stringify(this.makeBatchRequest(events));\n\t\tif (body.length > TEARDOWN_MAX_BODY_BYTES && events.length > 1) {\n\t\t\tconst mid = Math.ceil(events.length / 2);\n\t\t\tthis.sendKeepaliveChunk(events.slice(0, mid));\n\t\t\tthis.sendKeepaliveChunk(events.slice(mid));\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tvoid this.fetchFn(this.endpointUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: this.requestHeaders(),\n\t\t\t\tbody,\n\t\t\t\tkeepalive: true,\n\t\t\t}).catch(() => {});\n\t\t} catch {\n\t\t\t// Teardown is best-effort; never throw during page unload.\n\t\t}\n\t}\n\n\tprivate requestHeaders(): Record<string, string> {\n\t\tconst headers: Record<string, string> = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"X-WaniWani-SDK\": SDK_NAME,\n\t\t};\n\t\tif (this.apiKey) {\n\t\t\theaders.Authorization = `Bearer ${this.apiKey}`;\n\t\t}\n\t\treturn headers;\n\t}\n\n\tprivate scheduleMicroFlush(): void {\n\t\tif (this.flushScheduled) {\n\t\t\treturn;\n\t\t}\n\t\tthis.flushScheduled = true;\n\t\tthis.flushScheduledTimer = setTimeout(() => {\n\t\t\tthis.flushScheduledTimer = undefined;\n\t\t\tthis.flushScheduled = false;\n\t\t\tvoid this.flush();\n\t\t}, 0);\n\t}\n\n\tprivate async flushLoop(): Promise<void> {\n\t\twhile (this.buffer.length > 0 && !this.isStopped) {\n\t\t\tconst batch = this.buffer.splice(0, this.maxBatchSize);\n\t\t\tawait this.sendBatchWithRetry(batch);\n\t\t}\n\t}\n\n\tprivate async sendBatchWithRetry(batch: V2EventEnvelope[]): Promise<void> {\n\t\tlet attempt = 0;\n\t\tlet pendingBatch = batch;\n\n\t\twhile (pendingBatch.length > 0 && !this.isStopped) {\n\t\t\tthis.inFlightCount = pendingBatch.length;\n\t\t\tconst result = await this.sendBatchOnce(pendingBatch);\n\t\t\tthis.inFlightCount = 0;\n\n\t\t\tswitch (result.kind) {\n\t\t\t\tcase \"success\":\n\t\t\t\t\treturn;\n\t\t\t\tcase \"auth\":\n\t\t\t\t\tthis.stopTransportForAuthFailure(result.status, pendingBatch.length);\n\t\t\t\t\treturn;\n\t\t\t\tcase \"permanent\":\n\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) after permanent failure: %s\",\n\t\t\t\t\t\tpendingBatch.length,\n\t\t\t\t\t\tresult.reason,\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\tcase \"retryable\":\n\t\t\t\t\tif (attempt >= this.maxRetries) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) after retry exhaustion: %s\",\n\t\t\t\t\t\t\tpendingBatch.length,\n\t\t\t\t\t\t\tresult.reason,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait this.sleep(this.backoffDelayMs(attempt));\n\t\t\t\t\tattempt += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\tcase \"partial\":\n\t\t\t\t\tif (result.permanent.length > 0) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) rejected as permanent\",\n\t\t\t\t\t\t\tresult.permanent.length,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (result.retryable.length === 0) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (attempt >= this.maxRetries) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d retryable event(s) after retry exhaustion\",\n\t\t\t\t\t\t\tresult.retryable.length,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tpendingBatch = result.retryable;\n\t\t\t\t\tawait this.sleep(this.backoffDelayMs(attempt));\n\t\t\t\t\tattempt += 1;\n\t\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async sendBatchOnce(\n\t\tevents: V2EventEnvelope[],\n\t): Promise<SendBatchResult> {\n\t\tlet response: Response;\n\n\t\ttry {\n\t\t\tresponse = await this.fetchFn(this.endpointUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: this.requestHeaders(),\n\t\t\t\tbody: JSON.stringify(this.makeBatchRequest(events)),\n\t\t\t});\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tkind: \"retryable\",\n\t\t\t\treason: getErrorMessage(error),\n\t\t\t};\n\t\t}\n\n\t\tif (AUTH_FAILURE_STATUS.has(response.status)) {\n\t\t\treturn { kind: \"auth\", status: response.status };\n\t\t}\n\n\t\tif (RETRYABLE_STATUS.has(response.status)) {\n\t\t\treturn {\n\t\t\t\tkind: \"retryable\",\n\t\t\t\treason: `HTTP ${response.status}`,\n\t\t\t};\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\treturn {\n\t\t\t\tkind: \"permanent\",\n\t\t\t\treason: `HTTP ${response.status}`,\n\t\t\t};\n\t\t}\n\n\t\tconst data = await parseJsonResponse<V2BatchResponse>(response);\n\t\tif (!data?.rejected || data.rejected.length === 0) {\n\t\t\treturn { kind: \"success\" };\n\t\t}\n\n\t\tconst partial = this.classifyRejectedEvents(events, data.rejected);\n\t\tif (partial.retryable.length === 0 && partial.permanent.length === 0) {\n\t\t\treturn { kind: \"success\" };\n\t\t}\n\n\t\treturn {\n\t\t\tkind: \"partial\",\n\t\t\tretryable: partial.retryable,\n\t\t\tpermanent: partial.permanent,\n\t\t};\n\t}\n\n\tprivate makeBatchRequest(events: V2EventEnvelope[]): V2BatchRequest {\n\t\treturn {\n\t\t\tsentAt: this.now().toISOString(),\n\t\t\tsource: {\n\t\t\t\tsdk: SDK_NAME,\n\t\t\t\tversion: this.sdkVersion ?? \"0.0.0\",\n\t\t\t},\n\t\t\tevents,\n\t\t};\n\t}\n\n\tprivate classifyRejectedEvents(\n\t\tevents: V2EventEnvelope[],\n\t\trejected: V2BatchRejectedEvent[],\n\t): {\n\t\tretryable: V2EventEnvelope[];\n\t\tpermanent: V2EventEnvelope[];\n\t} {\n\t\tconst byId = new Map(events.map((event) => [event.id, event]));\n\t\tconst retryable: V2EventEnvelope[] = [];\n\t\tconst permanent: V2EventEnvelope[] = [];\n\n\t\tfor (const rejectedEvent of rejected) {\n\t\t\tconst event = byId.get(rejectedEvent.eventId);\n\t\t\tif (!event) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isRetryableRejectedEvent(rejectedEvent)) {\n\t\t\t\tretryable.push(event);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tpermanent.push(event);\n\t\t}\n\n\t\treturn { retryable, permanent };\n\t}\n\n\tprivate backoffDelayMs(attempt: number): number {\n\t\tconst rawDelay = this.retryBaseDelayMs * 2 ** attempt;\n\t\treturn Math.min(rawDelay, this.retryMaxDelayMs);\n\t}\n\n\tprivate stopTransportForAuthFailure(\n\t\tstatus: number,\n\t\trejectedCount: number,\n\t): void {\n\t\tthis.isStopped = true;\n\t\tconst buffered = this.buffer.length;\n\t\tthis.buffer.splice(0, buffered);\n\t\tthis.logger.error(\n\t\t\t\"[Waniwani] Auth failure (HTTP %d). Stopping tracking transport and dropping %d queued event(s)\",\n\t\t\tstatus,\n\t\t\trejectedCount + buffered,\n\t\t);\n\t}\n}\n\nfunction isRetryableRejectedEvent(\n\trejectedEvent: V2BatchRejectedEvent,\n): boolean {\n\tif (rejectedEvent.retryable === true) {\n\t\treturn true;\n\t}\n\tconst code = rejectedEvent.code.toLowerCase();\n\treturn (\n\t\tcode.includes(\"timeout\") ||\n\t\tcode.includes(\"temporary\") ||\n\t\tcode.includes(\"unavailable\") ||\n\t\tcode.includes(\"rate_limit\") ||\n\t\tcode.includes(\"transient\") ||\n\t\tcode.includes(\"server\")\n\t);\n}\n\nasync function parseJsonResponse<T>(\n\tresponse: Response,\n): Promise<T | undefined> {\n\tconst body = await response.text();\n\tif (!body) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn JSON.parse(body) as T;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction joinUrl(apiUrl: string, endpointPath: string): string {\n\tconst normalizedBase = apiUrl.endsWith(\"/\") ? apiUrl : `${apiUrl}/`;\n\tconst normalizedPath = endpointPath.startsWith(\"/\")\n\t\t? endpointPath.slice(1)\n\t\t: endpointPath;\n\treturn `${normalizedBase}${normalizedPath}`;\n}\n\nfunction getErrorMessage(error: unknown): string {\n\tif (error instanceof Error) {\n\t\treturn error.message;\n\t}\n\treturn String(error);\n}\n","import type {\n\tTrackFn,\n\tTrackInput,\n\tTrackingShutdownOptions,\n\tTrackingShutdownResult,\n} from \"./@types.js\";\nimport { mapTrackEventToV2 } from \"./mapper.js\";\nimport { createRevenueApi } from \"./revenue.js\";\nimport { createV2BatchTransport } from \"./transport.js\";\n\n/**\n * Correlation identity read at emit time, so identifiers that only exist\n * later (a server-assigned chat session id, for example) are picked up on\n * every event without re-creating the client.\n */\nexport interface FrontendIdentity {\n\tsessionId?: string;\n\tvisitorId?: string;\n\texternalUserId?: string;\n\ttraceId?: string;\n}\n\nexport interface FrontendClientOptions {\n\t/**\n\t * Full URL of the events ingest endpoint\n\t * (e.g. `https://app.waniwani.ai/api/mcp/events/v2/batch`).\n\t */\n\tendpoint: string;\n\t/**\n\t * Browser-safe credential: an environment public token (`wwp_...`) or a\n\t * widget JWT injected by `withWaniwani` into tool response `_meta`. Omit\n\t * only when the endpoint is your own unauthenticated proxy route.\n\t */\n\ttoken?: string;\n\t/**\n\t * Channel attribution stamped on every event (e.g. `\"chatgpt\"`, `\"web\"`).\n\t * The ingest API rejects events it cannot attribute to a channel, so set\n\t * this (or `channelId`) unless every event carries its own. Accepts a\n\t * getter for values that resolve after client creation.\n\t */\n\tsource?: string | (() => string | undefined);\n\t/**\n\t * Exact channel attribution, stamped as `properties.channelId` on every\n\t * event that does not set its own. Accepts a getter for values that\n\t * resolve after client creation.\n\t */\n\tchannelId?: string | (() => string | undefined);\n\t/** Live identity, called on every emit. */\n\tidentity?: () => FrontendIdentity;\n\t/** Extra fields merged into every event's envelope metadata. */\n\tmetadata?: Record<string, unknown>;\n\t/** Periodic flush interval for buffered events. */\n\tflushIntervalMs?: number;\n}\n\n/**\n * The browser counterpart of the server tracking client: the exact same\n * `track` surface (callable plus the flat revenue helpers) over the same\n * batching transport, with identity stamped automatically.\n */\nexport interface FrontendTrackingClient {\n\ttrack: TrackFn;\n\t/**\n\t * Attach a stable external user id. Emits `user.identified` and stamps\n\t * `externalUserId` on every subsequent event from this client.\n\t */\n\tidentify(\n\t\tuserId: string,\n\t\tproperties?: Record<string, unknown>,\n\t): Promise<{ eventId: string }>;\n\tflush(): Promise<void>;\n\tshutdown(options?: TrackingShutdownOptions): Promise<TrackingShutdownResult>;\n}\n\n/**\n * Create a tracking client for browser surfaces (MCP-app widgets, the chat\n * embed, or any page holding a `wwp_` public token). Events flush in batches,\n * survive page navigation via keepalive teardown, and never throw into the\n * host page.\n */\nexport function createFrontendClient(\n\toptions: FrontendClientOptions,\n): FrontendTrackingClient {\n\tconst { apiUrl, endpointPath } = splitEndpoint(options.endpoint);\n\tconst transport = createV2BatchTransport({\n\t\tapiUrl,\n\t\tapiKey: options.token,\n\t\tendpointPath,\n\t\tflushIntervalMs: options.flushIntervalMs,\n\t});\n\n\tlet identifiedUserId: string | undefined;\n\n\tconst resolve = (\n\t\tvalue: string | (() => string | undefined) | undefined,\n\t): string | undefined => (typeof value === \"function\" ? value() : value);\n\n\tconst emit = async (event: TrackInput): Promise<{ eventId: string }> => {\n\t\tconst identity = options.identity?.() ?? {};\n\t\tconst channelId = resolve(options.channelId);\n\t\tconst properties =\n\t\t\tchannelId === undefined\n\t\t\t\t? event.properties\n\t\t\t\t: { channelId, ...event.properties };\n\t\tconst mapped = mapTrackEventToV2(\n\t\t\t{\n\t\t\t\tsessionId: identity.sessionId,\n\t\t\t\tvisitorId: identity.visitorId,\n\t\t\t\texternalUserId: identity.externalUserId ?? identifiedUserId,\n\t\t\t\ttraceId: identity.traceId,\n\t\t\t\t...event,\n\t\t\t\tproperties,\n\t\t\t\tmetadata: { ...options.metadata, ...event.metadata },\n\t\t\t} as TrackInput,\n\t\t\t{ source: resolve(options.source) },\n\t\t);\n\t\ttransport.enqueue(mapped);\n\t\treturn { eventId: mapped.id };\n\t};\n\n\treturn {\n\t\ttrack: Object.assign(emit, createRevenueApi(emit)),\n\t\tidentify(userId, properties) {\n\t\t\tidentifiedUserId = userId;\n\t\t\treturn emit({\n\t\t\t\tevent: \"user.identified\",\n\t\t\t\texternalUserId: userId,\n\t\t\t\tproperties,\n\t\t\t});\n\t\t},\n\t\tflush: () => transport.flush(),\n\t\tshutdown: (shutdownOptions) => transport.shutdown(shutdownOptions),\n\t};\n}\n\nfunction splitEndpoint(endpoint: string): {\n\tapiUrl: string;\n\tendpointPath: string;\n} {\n\tconst url = new URL(endpoint);\n\treturn { apiUrl: url.origin, endpointPath: `${url.pathname}${url.search}` };\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { KbSearchTrace } from \"./types.js\";\n\n// Per-request bucket the tool wrapper opens; kb.search records into it.\nexport interface RetrievalCollector {\n\tsearches: KbSearchTrace[];\n}\n\n// Global slot, not a module-level instance: core (kb.search) and mcp\n// (withWaniwani) ship as separate bundles, so only globalThis keeps a single\n// shared AsyncLocalStorage per process. Two SDK copies in one process share\n// this slot, so RetrievalCollector's shape must stay backward-compatible.\nconst globalWithStore = globalThis as typeof globalThis & {\n\t__waniwaniKbRetrievalStore?: AsyncLocalStorage<RetrievalCollector>;\n};\n\nlet store = globalWithStore.__waniwaniKbRetrievalStore;\nif (!store) {\n\tstore = new AsyncLocalStorage<RetrievalCollector>();\n\tglobalWithStore.__waniwaniKbRetrievalStore = store;\n}\nexport const retrievalCollectorStore = store;\n\n// Best-effort: recording runs inside kb.search(), so it must never throw back\n// into a user's search (e.g. a corrupted collector from a mixed SDK version).\nexport function recordKbSearch(trace: KbSearchTrace): void {\n\ttry {\n\t\tretrievalCollectorStore.getStore()?.searches.push(trace);\n\t} catch {}\n}\n","// KB Client — thin HTTP wrapper for knowledge base API\n\nimport { WaniWaniError } from \"../error.js\";\nimport type { InternalConfig } from \"../types.js\";\nimport { recordKbSearch } from \"./retrieval-context.js\";\nimport type {\n\tKbClient,\n\tKbIngestFile,\n\tKbIngestResult,\n\tKbSearchOptions,\n\tKbSource,\n\tSearchResult,\n} from \"./types.js\";\n\nconst SDK_NAME = \"@waniwani/sdk\";\n\nexport function createKbClient(config: InternalConfig): KbClient {\n\tconst { apiUrl, apiKey } = config;\n\n\tfunction requireApiKey(): string {\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(\"WANIWANI_API_KEY is not set\");\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\tasync function request<T>(\n\t\tmethod: \"GET\" | \"POST\",\n\t\tpath: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst key = requireApiKey();\n\t\tconst url = `${apiUrl.replace(/\\/$/, \"\")}${path}`;\n\n\t\tconst headers: Record<string, string> = {\n\t\t\tAuthorization: `Bearer ${key}`,\n\t\t\t\"X-WaniWani-SDK\": SDK_NAME,\n\t\t};\n\n\t\tconst init: RequestInit = { method, headers };\n\n\t\tif (body !== undefined) {\n\t\t\theaders[\"Content-Type\"] = \"application/json\";\n\t\t\tinit.body = JSON.stringify(body);\n\t\t}\n\n\t\tconst response = await fetch(url, init);\n\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tthrow new WaniWaniError(\n\t\t\t\ttext || `KB API error: HTTP ${response.status}`,\n\t\t\t\tresponse.status,\n\t\t\t);\n\t\t}\n\n\t\tconst json = (await response.json()) as { data: T };\n\t\treturn json.data;\n\t}\n\n\treturn {\n\t\tasync ingest(files: KbIngestFile[]): Promise<KbIngestResult> {\n\t\t\treturn request<KbIngestResult>(\"POST\", \"/api/mcp/kb/ingest\", {\n\t\t\t\tfiles,\n\t\t\t});\n\t\t},\n\n\t\tasync search(\n\t\t\tquery: string,\n\t\t\toptions?: KbSearchOptions,\n\t\t): Promise<SearchResult[]> {\n\t\t\tconst results = await request<SearchResult[]>(\n\t\t\t\t\"POST\",\n\t\t\t\t\"/api/mcp/kb/search\",\n\t\t\t\t{\n\t\t\t\t\tquery,\n\t\t\t\t\t...options,\n\t\t\t\t},\n\t\t\t);\n\t\t\trecordKbSearch({\n\t\t\t\tquery,\n\t\t\t\tresultCount: results.length,\n\t\t\t\tresults: results.map((r) => ({\n\t\t\t\t\tsource: r.source,\n\t\t\t\t\theading: r.heading,\n\t\t\t\t\tscore: r.score,\n\t\t\t\t})),\n\t\t\t});\n\t\t\treturn results;\n\t\t},\n\n\t\tasync sources(): Promise<KbSource[]> {\n\t\t\treturn request<KbSource[]>(\"GET\", \"/api/mcp/kb/sources\");\n\t\t},\n\t};\n}\n","// Tracking Module\n\nimport type { InternalConfig } from \"../types.js\";\nimport type {\n\tTrackFn,\n\tTrackInput,\n\tTrackingClient,\n\tTrackingShutdownOptions,\n} from \"./@types.js\";\nimport { mapTrackEventToV2 } from \"./mapper.js\";\nimport { createRevenueApi } from \"./revenue.js\";\nimport { createV2BatchTransport } from \"./transport.js\";\n\n// Re-export types\nexport type {\n\tCallableTrack,\n\tComparedPriceOption,\n\tConvertedProperties,\n\tEventType,\n\tLeadQualifiedProperties,\n\tLegacyTrackEvent,\n\tLinkClickedProperties,\n\tOptionSelectedProperties,\n\tPriceShownProperties,\n\tPricesComparedProperties,\n\tRevenueConvertedInput,\n\tRevenueLeadQualifiedInput,\n\tRevenueOptionSelectedInput,\n\tRevenuePriceShownInput,\n\tRevenuePricesComparedInput,\n\tRevenueTrackingApi,\n\tToolCalledProperties,\n\tTrackEvent,\n\tTrackFn,\n\tTrackInput,\n\tTrackingClient,\n\tTrackingConfig,\n\tTrackingShutdownOptions,\n\tTrackingShutdownResult,\n} from \"./@types.js\";\nexport { EVENT_TYPES } from \"./@types.js\";\nexport { createEventId, mapTrackEventToV2 } from \"./mapper.js\";\nexport type {\n\tV2BatchRejectedEvent,\n\tV2BatchRequest,\n\tV2BatchResponse,\n\tV2CorrelationIds,\n\tV2EnvelopeType,\n\tV2EventEnvelope,\n} from \"./v2-types.js\";\n\nexport function createTrackingClient(config: InternalConfig): TrackingClient {\n\tconst { apiUrl, apiKey, tracking } = config;\n\n\tfunction requireApiKey(): string {\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(\"WANIWANI_API_KEY is not set\");\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\tconst transport = apiKey\n\t\t? createV2BatchTransport({\n\t\t\t\tapiUrl,\n\t\t\t\tapiKey,\n\t\t\t\tendpointPath: tracking.endpointPath,\n\t\t\t\tflushIntervalMs: tracking.flushIntervalMs,\n\t\t\t\tmaxBatchSize: tracking.maxBatchSize,\n\t\t\t\tmaxBufferSize: tracking.maxBufferSize,\n\t\t\t\tmaxRetries: tracking.maxRetries,\n\t\t\t\tretryBaseDelayMs: tracking.retryBaseDelayMs,\n\t\t\t\tretryMaxDelayMs: tracking.retryMaxDelayMs,\n\t\t\t\tshutdownTimeoutMs: tracking.shutdownTimeoutMs,\n\t\t\t})\n\t\t: undefined;\n\n\t// Single enqueue path shared by track(), track.* revenue helpers, and identify().\n\tfunction emit(event: TrackInput): { eventId: string } {\n\t\trequireApiKey();\n\t\tconst mappedEvent = mapTrackEventToV2(event);\n\t\t// Identity is required server-side (sessionId, externalUserId, or\n\t\t// visitorId, possibly derived from meta). Warn early: without it the\n\t\t// ingest API rejects the event even though enqueue returns an id here.\n\t\tif (\n\t\t\t!mappedEvent.correlation.sessionId &&\n\t\t\t!mappedEvent.correlation.externalUserId &&\n\t\t\t!mappedEvent.correlation.visitorId\n\t\t) {\n\t\t\tconsole.warn(\n\t\t\t\t`[waniwani] event \"${mappedEvent.name}\" has no sessionId, externalUserId, or visitorId; ` +\n\t\t\t\t\t\"the ingest API requires one and will reject it.\",\n\t\t\t);\n\t\t}\n\t\ttransport?.enqueue(mappedEvent);\n\t\treturn { eventId: mappedEvent.id };\n\t}\n\n\tconst trackOnce = async (event: TrackInput): Promise<{ eventId: string }> =>\n\t\temit(event);\n\t// Revenue helpers attach flat onto `track` (track.priceShown(), …).\n\tconst track: TrackFn = Object.assign(trackOnce, createRevenueApi(trackOnce));\n\n\tconst client: TrackingClient = {\n\t\tasync identify(\n\t\t\tuserId: string,\n\t\t\tproperties?: Record<string, unknown>,\n\t\t\tmeta?: Record<string, unknown>,\n\t\t): Promise<{ eventId: string }> {\n\t\t\trequireApiKey();\n\t\t\tconst mappedEvent = mapTrackEventToV2({\n\t\t\t\tevent: \"user.identified\",\n\t\t\t\texternalUserId: userId,\n\t\t\t\tproperties,\n\t\t\t\tmeta,\n\t\t\t});\n\t\t\ttransport?.enqueue(mappedEvent);\n\t\t\treturn { eventId: mappedEvent.id };\n\t\t},\n\t\ttrack,\n\t\tasync flush(): Promise<void> {\n\t\t\trequireApiKey();\n\t\t\tawait transport?.flush();\n\t\t},\n\t\tasync shutdown(options?: TrackingShutdownOptions) {\n\t\t\trequireApiKey();\n\t\t\treturn (\n\t\t\t\t(await transport?.shutdown({\n\t\t\t\t\ttimeoutMs: options?.timeoutMs ?? tracking.shutdownTimeoutMs,\n\t\t\t\t})) ?? { timedOut: false, pendingEvents: 0 }\n\t\t\t);\n\t\t},\n\t};\n\n\tif (transport) {\n\t\tattachShutdownHooks(client, tracking.shutdownTimeoutMs);\n\t}\n\treturn client;\n}\n\nfunction attachShutdownHooks(\n\tclient: TrackingClient,\n\tdefaultTimeoutMs: number,\n): void {\n\tif (\n\t\ttypeof process === \"undefined\" ||\n\t\ttypeof process.once !== \"function\" ||\n\t\ttypeof process.on !== \"function\"\n\t) {\n\t\treturn;\n\t}\n\n\tconst shutdown = () => {\n\t\tvoid client.shutdown({ timeoutMs: defaultTimeoutMs });\n\t};\n\n\tprocess.once(\"beforeExit\", shutdown);\n\tprocess.once(\"SIGINT\", shutdown);\n\tprocess.once(\"SIGTERM\", shutdown);\n}\n","// Waniwani SDK - Main Entry\n\nimport { createKbClient } from \"./kb/client.js\";\nimport {\n\tgetGlobalConfig,\n\tloadProjectConfig,\n\ttype WaniWaniProjectConfig,\n} from \"./project-config.js\";\nimport { createTrackingClient } from \"./tracking/index.js\";\nimport type { WaniWaniClient, WaniWaniConfig } from \"./types.js\";\n\n/**\n * Create a Waniwani SDK client\n *\n * @param config - Configuration options. When omitted, reads `waniwani.json`\n * from the current working directory, then falls back to env vars.\n * @returns A fully typed Waniwani client\n *\n * @example\n * ```typescript\n * import { waniwani } from \"@waniwani/sdk\";\n * import { toNextJsHandler } from \"@waniwani/sdk/next-js\";\n *\n * const wani = waniwani({ apiKey: \"...\" });\n *\n * // Next.js route handler\n * export const { GET, POST } = toNextJsHandler(wani, {\n * chat: { systemPrompt: \"You are a helpful assistant.\" },\n * });\n * ```\n */\nexport function waniwani(\n\tconfig?: WaniWaniConfig | WaniWaniProjectConfig,\n): WaniWaniClient {\n\tconst projectConfig = config ?? loadProjectConfig() ?? getGlobalConfig();\n\tconst effective = projectConfig as\n\t\t| (WaniWaniConfig & WaniWaniProjectConfig)\n\t\t| undefined;\n\n\tconst apiUrl =\n\t\teffective?.apiUrl ??\n\t\tprocess.env.WANIWANI_API_URL ??\n\t\t\"https://app.waniwani.ai\";\n\tconst apiKey = effective?.apiKey ?? process.env.WANIWANI_API_KEY;\n\tconst trackingConfig = {\n\t\tendpointPath:\n\t\t\teffective?.tracking?.endpointPath ?? \"/api/mcp/events/v2/batch\",\n\t\tflushIntervalMs: effective?.tracking?.flushIntervalMs ?? 1_000,\n\t\tmaxBatchSize: effective?.tracking?.maxBatchSize ?? 20,\n\t\tmaxBufferSize: effective?.tracking?.maxBufferSize ?? 1_000,\n\t\tmaxRetries: effective?.tracking?.maxRetries ?? 3,\n\t\tretryBaseDelayMs: effective?.tracking?.retryBaseDelayMs ?? 200,\n\t\tretryMaxDelayMs: effective?.tracking?.retryMaxDelayMs ?? 2_000,\n\t\tshutdownTimeoutMs: effective?.tracking?.shutdownTimeoutMs ?? 2_000,\n\t};\n\n\tconst internalConfig = { apiUrl, apiKey, tracking: trackingConfig };\n\n\t// Compose client from modules\n\tconst trackingClient = createTrackingClient(internalConfig);\n\tconst kbClient = createKbClient(internalConfig);\n\n\treturn {\n\t\t...trackingClient,\n\t\tkb: kbClient,\n\t\t_config: internalConfig,\n\t};\n}\n"],"mappings":"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","names":["WaniWaniError","message","status","existsSync","readFileSync","resolve","CONFIG_FILENAME","_cached","loadProjectConfig","filePath","raw","GLOBAL_KEY","defineConfig","config","getGlobalConfig","EVENT_TYPES","pickFirst","meta","keys","key","value","SESSION_ID_KEYS","REQUEST_ID_KEYS","TRACE_ID_KEYS","EXTERNAL_USER_ID_KEYS","VISITOR_ID_KEYS","CORRELATION_ID_KEYS","extractSessionId","meta","pickFirst","SESSION_ID_KEYS","extractRequestId","REQUEST_ID_KEYS","extractTraceId","TRACE_ID_KEYS","extractExternalUserId","EXTERNAL_USER_ID_KEYS","extractVisitorId","VISITOR_ID_KEYS","extractCorrelationId","CORRELATION_ID_KEYS","SOURCE_SESSION_KEYS","CLIENT_INFO_NAME_SOURCES","extractSource","meta","clientInfo","explicit","key","source","value","name","lower","needle","DEFAULT_SOURCE","mapTrackEventToV2","input","options","now","generateId","createEventId","eventName","resolveEventName","meta","toRecord","metadata","correlation","resolveCorrelationIds","eventId","takeNonEmptyString","timestamp","normalizeTimestamp","source","extractSource","rawLegacy","isLegacyTrackEvent","mappedMetadata","mapProperties","legacyProperties","mapLegacyProperties","explicitProperties","properties","requestId","extractRequestId","sessionId","extractSessionId","traceId","extractTraceId","externalUserId","extractExternalUserId","visitorId","extractVisitorId","correlationId","extractCorrelationId","date","value","createRevenueApi","emit","amount","currency","itemId","label","context","options","id","input","externalId","email","name","occurredAt","DEFAULT_ENDPOINT_PATH","SDK_NAME","AUTH_FAILURE_STATUS","RETRYABLE_STATUS","createV2BatchTransport","options","BatchingV2Transport","joinUrl","DEFAULT_ENDPOINT_PATH","delayMs","resolve","event","dropCount","timeoutMs","flushPromise","timeoutSignal","teardownFlush","events","body","mid","headers","SDK_NAME","batch","attempt","pendingBatch","result","response","error","getErrorMessage","data","parseJsonResponse","partial","rejected","byId","retryable","permanent","rejectedEvent","isRetryableRejectedEvent","rawDelay","status","rejectedCount","buffered","code","apiUrl","endpointPath","normalizedBase","normalizedPath","createFrontendClient","options","apiUrl","endpointPath","splitEndpoint","transport","createV2BatchTransport","identifiedUserId","resolve","value","emit","event","identity","channelId","properties","mapped","mapTrackEventToV2","createRevenueApi","userId","shutdownOptions","endpoint","url","AsyncLocalStorage","globalWithStore","store","retrievalCollectorStore","recordKbSearch","trace","SDK_NAME","createKbClient","config","apiUrl","apiKey","requireApiKey","request","method","path","body","key","url","headers","init","response","text","WaniWaniError","files","query","options","results","recordKbSearch","r","createTrackingClient","config","apiUrl","apiKey","tracking","requireApiKey","transport","createV2BatchTransport","emit","event","mappedEvent","mapTrackEventToV2","trackOnce","track","createRevenueApi","client","userId","properties","meta","options","attachShutdownHooks","defaultTimeoutMs","shutdown","waniwani","config","effective","loadProjectConfig","getGlobalConfig","apiUrl","apiKey","trackingConfig","internalConfig","trackingClient","createTrackingClient","kbClient","createKbClient"]}
|
|
1
|
+
{"version":3,"sources":["../src/error.ts","../src/project-config.ts","../src/tracking/@types.ts","../src/mcp/server/utils.ts","../src/tracking/mapper.ts","../src/tracking/revenue.ts","../src/tracking/transport.ts","../src/tracking/frontend.ts","../src/kb/retrieval-context.ts","../src/kb/client.ts","../src/tracking/index.ts","../src/waniwani.ts"],"sourcesContent":["// Waniwani SDK - Errors\n\nexport class WaniWaniError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"WaniWaniError\";\n\t}\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\n/**\n * Project-level configuration for Waniwani MCP projects.\n *\n * Mirrors the JSON Schema hosted at https://docs.waniwani.ai/waniwani.json.\n * The canonical config file is `waniwani.json` at the project root:\n *\n * ```json\n * {\n * \"$schema\": \"https://docs.waniwani.ai/waniwani.json\",\n * \"orgId\": \"...\",\n * \"projectId\": \"...\"\n * }\n * ```\n *\n * `waniwani()` and the CLI both read this file automatically — no\n * explicit import is required.\n */\nexport interface WaniWaniProjectConfig {\n\t/** URL of the JSON Schema for editor autocomplete. Ignored at runtime. */\n\t$schema?: string;\n\t/** Waniwani organization ID this project belongs to. */\n\torgId?: string;\n\t/** Waniwani MCP project ID. */\n\tprojectId?: string;\n\t/**\n\t * The base URL of the Waniwani API.\n\t * Falls back to the `WANIWANI_API_URL` env var when omitted here, then to\n\t * `https://app.waniwani.ai`.\n\t */\n\tapiUrl?: string;\n\t/**\n\t * Local port the MCP listens on during `waniwani dev`. Overridden by\n\t * `--port`. Defaults to 3000.\n\t */\n\tdevPort?: number;\n}\n\n// ---------------------------------------------------------------------------\n// waniwani.json loader\n// ---------------------------------------------------------------------------\n\nconst CONFIG_FILENAME = \"waniwani.json\";\n\nlet _cached: WaniWaniProjectConfig | null | undefined;\n\n/**\n * Load `waniwani.json` from the current working directory.\n *\n * Returns `null` if the file doesn't exist or the runtime doesn't support\n * synchronous filesystem reads (e.g. edge / worker runtimes). Cached after\n * the first call.\n *\n * @internal\n */\nexport function loadProjectConfig(): WaniWaniProjectConfig | null {\n\tif (_cached !== undefined) {\n\t\treturn _cached;\n\t}\n\n\ttry {\n\t\tconst filePath = resolve(process.cwd(), CONFIG_FILENAME);\n\t\tif (!existsSync(filePath)) {\n\t\t\t_cached = null;\n\t\t\treturn null;\n\t\t}\n\t\tconst raw = readFileSync(filePath, \"utf-8\");\n\t\t_cached = JSON.parse(raw) as WaniWaniProjectConfig;\n\t\treturn _cached;\n\t} catch {\n\t\t_cached = null;\n\t\treturn null;\n\t}\n}\n\n/**\n * Reset the cached config. Test-only.\n *\n * @internal\n */\nexport function resetProjectConfigCache(): void {\n\t_cached = undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Legacy: defineConfig + globalThis registration\n// ---------------------------------------------------------------------------\n\nconst GLOBAL_KEY = \"__waniwani_config__\" as const;\n\n/**\n * Register a Waniwani project configuration on `globalThis`.\n *\n * @deprecated Create a `waniwani.json` at the project root instead. The SDK\n * and CLI both auto-load that file — no `defineConfig` call required.\n * See https://docs.waniwani.ai/waniwani.json for the schema.\n */\nexport function defineConfig(\n\tconfig: WaniWaniProjectConfig,\n): WaniWaniProjectConfig {\n\t(globalThis as Record<string, unknown>)[GLOBAL_KEY] = config;\n\treturn config;\n}\n\n/**\n * Retrieve the globally registered config (set by `defineConfig`).\n *\n * @deprecated Use `loadProjectConfig()` instead. `defineConfig` is going away.\n * @internal\n */\nexport function getGlobalConfig(): WaniWaniProjectConfig | undefined {\n\treturn (globalThis as Record<string, unknown>)[GLOBAL_KEY] as\n\t\t| WaniWaniProjectConfig\n\t\t| undefined;\n}\n","// Tracking Module Types\n\nimport type { KbSearchTrace } from \"../kb/types.js\";\n\n// ============================================\n// Event Types\n// ============================================\n\n/**\n * Every event name in the typed taxonomy, as a runtime list. Single source of\n * truth for {@link EventType} and for surfaces that need to recognize\n * first-class names at runtime (the widget transport passes these through\n * verbatim instead of namespacing them as custom widget events).\n */\nexport const EVENT_TYPES = [\n\t\"session.started\",\n\t\"page.viewed\",\n\t\"tool.called\",\n\t\"link.clicked\",\n\t// Emitted automatically by the frontend client once per widget mount.\n\t\"widget_render\",\n\t\"user.identified\",\n\t// Revenue taxonomy (WAN-387): typed first-class revenue events.\n\t\"price_shown\",\n\t\"prices_compared\",\n\t\"option_selected\",\n\t\"lead_qualified\",\n\t\"converted\",\n] as const;\n\nexport type EventType = (typeof EVENT_TYPES)[number];\n\n// ============================================\n// Event Properties\n// ============================================\n\n/**\n * Properties for `page.viewed` — emitted once when the chat widget initializes\n * on a host page (the top of the funnel). Attributed to the anonymous\n * `visitorId` (mapped to `externalUserId`), never to a session: a page view\n * must not create a session, otherwise sessions would equal page views and the\n * \"landed → started a conversation\" funnel collapses.\n */\nexport interface PageViewedProperties {\n\t/** Full URL of the host page the widget loaded on. */\n\turl?: string;\n\t/** Referrer of the host page, if any. */\n\treferrer?: string;\n\t/** Embed mode the widget initialized in. */\n\tmode?: \"inline\" | \"floating\";\n\t/** Resolved device type from the visitor context. */\n\tdeviceType?: \"mobile\" | \"tablet\" | \"desktop\";\n\t/** Primary browser language (BCP-47). */\n\tlanguage?: string;\n\t/** IANA timezone of the visitor. */\n\ttimezone?: string;\n}\n\nexport interface ToolCalledProperties {\n\tname?: string;\n\ttype?: \"pricing\" | \"product_info\" | \"availability\" | \"support\" | \"other\";\n\t/** Retrieval traces for kb.search() calls made inside this tool handler. */\n\tkbSearch?: KbSearchTrace[];\n}\n\nexport interface LinkClickedProperties {\n\turl?: string;\n}\n\n// ============================================\n// Revenue Taxonomy Properties (WAN-387)\n// ============================================\n\nexport interface PriceShownProperties {\n\tamount: number;\n\tcurrency: string;\n\titemId?: string;\n\tlabel?: string;\n}\n\nexport interface ComparedPriceOption {\n\tid: string;\n\tamount: number;\n\tcurrency: string;\n}\n\nexport interface PricesComparedProperties {\n\toptions: ComparedPriceOption[];\n}\n\nexport interface OptionSelectedProperties {\n\tid: string;\n\tamount: number;\n\tcurrency: string;\n}\n\n/**\n * Properties for `lead_qualified` — your code declaring that a person met your\n * qualification bar (finished the qualifying questions, requested a demo,\n * matched your target profile). Sharing an email mid-conversation is\n * `identify(userId, { email })`, not a qualified lead.\n */\nexport interface LeadQualifiedProperties {\n\t/**\n\t * Your own lead id (e.g. the record id your lead-gen API returns). The\n\t * strongest dedup key and the stable reference back to your system.\n\t */\n\texternalId?: string;\n\t/** The lead's email, if known. */\n\temail?: string;\n\t/** The lead's name, if known. */\n\tname?: string;\n}\n\nexport interface ConvertedProperties {\n\tamount: number;\n\tcurrency: string;\n\t/** When the conversion actually happened — for backdated off-platform sales. */\n\toccurredAt?: string;\n}\n\n// ============================================\n// Legacy + Modern Track Inputs\n// ============================================\n\ninterface TrackingContext {\n\t/**\n\t * MCP request metadata passed through to the API.\n\t *\n\t * Location varies by MCP library:\n\t * - `@vercel/mcp-handler`: `extra._meta`\n\t * - `@modelcontextprotocol/sdk`: `request.params._meta`\n\t */\n\tmeta?: Record<string, unknown>;\n\t/** Legacy metadata field supported for backward compatibility. */\n\tmetadata?: Record<string, unknown>;\n\t/** Optional explicit correlation fields. */\n\tsessionId?: string;\n\ttraceId?: string;\n\trequestId?: string;\n\tcorrelationId?: string;\n\texternalUserId?: string;\n\t/**\n\t * Anonymous visitor id (the analytics \"device id\"). Counts as identity on\n\t * its own: the ingest API accepts `sessionId`, `externalUserId`, or\n\t * `visitorId`. The chat widget uses it for pre-session events like\n\t * `page.viewed`.\n\t */\n\tvisitorId?: string;\n\t/** Optional explicit envelope fields. */\n\teventId?: string;\n\ttimestamp?: string | Date;\n\tsource?: string;\n}\n\n/**\n * Modern tracking shape (preferred).\n */\ninterface BaseTrackEvent extends TrackingContext {\n\tevent: EventType;\n\tproperties?: Record<string, unknown>;\n}\n\nexport type TrackEvent =\n\t| ({ event: \"session.started\" } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"page.viewed\";\n\t\t\tproperties?: PageViewedProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"tool.called\";\n\t\t\tproperties?: ToolCalledProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"link.clicked\";\n\t\t\tproperties?: LinkClickedProperties;\n\t } & BaseTrackEvent)\n\t| ({ event: \"user.identified\" } & BaseTrackEvent)\n\t| ({ event: \"widget_render\" } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"price_shown\";\n\t\t\tproperties?: PriceShownProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"prices_compared\";\n\t\t\tproperties?: PricesComparedProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"option_selected\";\n\t\t\tproperties?: OptionSelectedProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"lead_qualified\";\n\t\t\tproperties?: LeadQualifiedProperties;\n\t } & BaseTrackEvent)\n\t| ({\n\t\t\tevent: \"converted\";\n\t\t\tproperties?: ConvertedProperties;\n\t } & BaseTrackEvent);\n\n/**\n * Legacy tracking shape supported for existing integrations.\n */\nexport interface LegacyTrackEvent extends TrackingContext {\n\teventType: EventType;\n\tproperties?: Record<string, unknown>;\n\ttoolName?: string;\n\ttoolType?: ToolCalledProperties[\"type\"];\n\tlinkUrl?: string;\n}\n\n/**\n * Public track input accepted by `client.track()`.\n */\nexport type TrackInput = TrackEvent | LegacyTrackEvent;\n\n// ============================================\n// Revenue Helper Inputs (WAN-386)\n// ============================================\n\n// Each helper input is its typed properties plus the shared tracking context\n// (sessionId / externalUserId / meta / …). At least one identity field\n// (sessionId or externalUserId, possibly derived from `meta`) must be present —\n// the ingest API rejects events without one.\n\nexport interface RevenuePriceShownInput\n\textends TrackingContext,\n\t\tPriceShownProperties {}\n\nexport interface RevenuePricesComparedInput\n\textends TrackingContext,\n\t\tPricesComparedProperties {}\n\nexport interface RevenueOptionSelectedInput\n\textends TrackingContext,\n\t\tOptionSelectedProperties {}\n\n/**\n * Input for `track.leadQualified()` — identity only (`externalId`, `email`,\n * `name`) plus the shared tracking context. There is no acquisition-source\n * property; the envelope `source` (the origin channel) is set automatically.\n */\nexport interface RevenueLeadQualifiedInput\n\textends TrackingContext,\n\t\tLeadQualifiedProperties {}\n\nexport interface RevenueConvertedInput\n\textends TrackingContext,\n\t\tConvertedProperties {}\n\n/**\n * Revenue-oriented helpers, flat on `client.track.*` (e.g.\n * `client.track.priceShown()`, `client.track.converted()`). Decoupled from\n * product primitives — each maps to a typed first-class revenue event.\n */\nexport interface RevenueTrackingApi {\n\tpriceShown: (input: RevenuePriceShownInput) => Promise<{ eventId: string }>;\n\tpricesCompared: (\n\t\tinput: RevenuePricesComparedInput,\n\t) => Promise<{ eventId: string }>;\n\toptionSelected: (\n\t\tinput: RevenueOptionSelectedInput,\n\t) => Promise<{ eventId: string }>;\n\tleadQualified: (\n\t\tinput?: RevenueLeadQualifiedInput,\n\t) => Promise<{ eventId: string }>;\n\tconverted: (input: RevenueConvertedInput) => Promise<{ eventId: string }>;\n}\n\n/**\n * The callable form of `track` — emit one event, without the flat revenue\n * helpers. What internal code and custom/injected trackers need; lets them\n * avoid implementing the revenue methods.\n */\nexport type CallableTrack = (event: TrackInput) => Promise<{ eventId: string }>;\n\n/**\n * `client.track` — callable for generic events (`track(event)`), with the\n * revenue helpers attached flat: `track.priceShown()`, `track.leadQualified()`,\n * `track.converted()`, etc.\n */\nexport interface TrackFn extends RevenueTrackingApi {\n\t(event: TrackInput): Promise<{ eventId: string }>;\n}\n\n// ============================================\n// Runtime Config\n// ============================================\n\nexport interface TrackingConfig {\n\t/** Events API V2 endpoint path. */\n\tendpointPath?: string;\n\t/** Periodic flush interval for buffered events. */\n\tflushIntervalMs?: number;\n\t/** Max events per HTTP batch send. */\n\tmaxBatchSize?: number;\n\t/** Max in-memory buffer size before oldest items are dropped. */\n\tmaxBufferSize?: number;\n\t/** Number of retries for retryable failures. */\n\tmaxRetries?: number;\n\t/** Retry backoff base delay. */\n\tretryBaseDelayMs?: number;\n\t/** Retry backoff max delay. */\n\tretryMaxDelayMs?: number;\n\t/** Default shutdown timeout when none is provided. */\n\tshutdownTimeoutMs?: number;\n}\n\nexport interface TrackingShutdownOptions {\n\ttimeoutMs?: number;\n}\n\nexport interface TrackingShutdownResult {\n\ttimedOut: boolean;\n\tpendingEvents: number;\n}\n\n// ============================================\n// Client\n// ============================================\n\n/**\n * Tracking module methods for WaniWaniClient.\n */\nexport interface TrackingClient {\n\t/**\n\t * Send a one-shot identify event for a user.\n\t * userId can be any string: an email, an internal ID, etc.\n\t */\n\tidentify: (\n\t\tuserId: string,\n\t\tproperties?: Record<string, unknown>,\n\t\tmeta?: Record<string, unknown>,\n\t) => Promise<{ eventId: string }>;\n\t/**\n\t * Track an event using modern or legacy input shape.\n\t * Returns a deterministic event id immediately after enqueue.\n\t *\n\t * Also exposes the revenue helpers flat: `client.track.priceShown()`,\n\t * `client.track.leadQualified()`, `client.track.converted()`, etc.\n\t */\n\ttrack: TrackFn;\n\t/**\n\t * Flush all currently buffered events.\n\t */\n\tflush: () => Promise<void>;\n\t/**\n\t * Flush and stop the transport.\n\t */\n\tshutdown: (\n\t\toptions?: TrackingShutdownOptions,\n\t) => Promise<TrackingShutdownResult>;\n}\n","// ============================================================================\n// Meta key extraction helpers\n// ============================================================================\n\n/** Pick the first non-empty string value from `meta` matching the given keys. */\nfunction pickFirst(\n\tmeta: Record<string, unknown>,\n\tkeys: readonly string[],\n): string | undefined {\n\tfor (const key of keys) {\n\t\tconst value = meta[key];\n\t\tif (typeof value === \"string\" && value.length > 0) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n// --- Key lists (ordered by priority) ---\n\nconst SESSION_ID_KEYS = [\n\t\"waniwani/sessionId\",\n\t\"openai/sessionId\",\n\t\"openai/session\",\n\t\"sessionId\",\n\t\"conversationId\",\n\t\"mcp-session-id\",\n] as const;\n\nconst REQUEST_ID_KEYS = [\n\t\"waniwani/requestId\",\n\t\"openai/requestId\",\n\t\"requestId\",\n\t\"mcp/requestId\",\n] as const;\n\nconst TRACE_ID_KEYS = [\n\t\"waniwani/traceId\",\n\t\"openai/traceId\",\n\t\"traceId\",\n\t\"mcp/traceId\",\n\t\"openai/requestId\",\n\t\"requestId\",\n] as const;\n\nconst EXTERNAL_USER_ID_KEYS = [\n\t\"waniwani/userId\",\n\t\"openai/userId\",\n\t\"externalUserId\",\n\t\"userId\",\n\t\"actorId\",\n] as const;\n\nconst VISITOR_ID_KEYS = [\"waniwani/visitorId\", \"visitorId\"] as const;\n\nconst CORRELATION_ID_KEYS = [\"correlationId\", \"openai/requestId\"] as const;\n\nconst TURN_COUNT_KEYS = [\"waniwani/turnCount\"] as const;\n\n/** Meta key for flow execution path (nodesVisited, flowId). */\nexport const FLOW_META_KEY = \"waniwani/flow\" as const;\n\nexport type { SuggestionsMeta } from \"../../shared/meta-keys\";\nexport { SUGGESTIONS_META_KEY } from \"../../shared/meta-keys\";\n\n/**\n * Meta key under which `withWaniwani` injects the widget tracking config\n * (`{ endpoint, token, sessionId, source, geoLocation }`) into tool response\n * `_meta`. The frontend client reads it to track without any manual wiring.\n * Older SDKs wrote the same object under the bare `waniwani` key.\n */\nexport const WIDGET_CONFIG_META_KEY = \"waniwani/widget\" as const;\n\n// --- Extractors ---\n\nexport function extractSessionId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, SESSION_ID_KEYS) : undefined;\n}\n\nexport function extractRequestId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, REQUEST_ID_KEYS) : undefined;\n}\n\nexport function extractTraceId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, TRACE_ID_KEYS) : undefined;\n}\n\nexport function extractExternalUserId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, EXTERNAL_USER_ID_KEYS) : undefined;\n}\n\nexport function extractVisitorId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, VISITOR_ID_KEYS) : undefined;\n}\n\nexport function extractCorrelationId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, CORRELATION_ID_KEYS) : undefined;\n}\n\n/**\n * Number of user messages in the current chat session, as counted by the\n * Waniwani app before dispatching the MCP request. Useful for MCPs that\n * gate behavior on conversation length (e.g. compulsory email verification\n * after N turns) without each MCP having to track turn state itself.\n *\n * Forwarded as `waniwani/turnCount` (non-negative integer).\n */\nexport function extractTurnCount(\n\tmeta: Record<string, unknown> | undefined,\n): number | undefined {\n\tif (!meta) {\n\t\treturn undefined;\n\t}\n\tfor (const key of TURN_COUNT_KEYS) {\n\t\tconst value = meta[key];\n\t\tif (typeof value === \"number\" && Number.isFinite(value) && value >= 0) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nconst SOURCE_SESSION_KEYS = [\n\t{ key: \"openai/sessionId\", source: \"chatgpt\" },\n\t{ key: \"openai/session\", source: \"chatgpt\" },\n] as const;\n\n/**\n * Client identifiers advertised via MCP `initialize` → `clientInfo.name` that\n * the SDK maps to a known source. Match is case-insensitive substring on the\n * advertised name, so this catches \"Claude\", \"Claude Code\", \"claude-ai\", etc.\n * without having to enumerate every surface.\n *\n * ChatGPT is primarily identified by its `openai/*` session key (see\n * SOURCE_SESSION_KEYS), but a session without that key still resolves here from\n * the advertised client name so assistant traffic is attributed consistently.\n * First match wins, so order the needles most-specific first.\n */\nconst CLIENT_INFO_NAME_SOURCES: ReadonlyArray<{\n\tneedle: string;\n\tsource: string;\n}> = [\n\t{ needle: \"claude\", source: \"claude\" },\n\t{ needle: \"chatgpt\", source: \"chatgpt\" },\n\t{ needle: \"openai\", source: \"chatgpt\" },\n\t{ needle: \"gemini\", source: \"gemini\" },\n];\n\nexport type ExtractSourceClientInfo = {\n\tname?: string;\n\tversion?: string;\n};\n\nexport function extractSource(\n\tmeta: Record<string, unknown> | undefined,\n\tclientInfo?: ExtractSourceClientInfo,\n): string | undefined {\n\tif (meta) {\n\t\tconst explicit = meta[\"waniwani/source\"];\n\t\tif (typeof explicit === \"string\" && explicit.length > 0) {\n\t\t\treturn explicit;\n\t\t}\n\t\tfor (const { key, source } of SOURCE_SESSION_KEYS) {\n\t\t\tconst value = meta[key];\n\t\t\tif (typeof value === \"string\" && value.length > 0) {\n\t\t\t\treturn source;\n\t\t\t}\n\t\t}\n\t}\n\t// Fall back to MCP `initialize` clientInfo.name when no _meta key matches.\n\t// Claude surfaces (Code, Desktop, claude.ai connector, Anthropic API MCP\n\t// connector) don't expose a namespaced session id in _meta but do advertise\n\t// themselves in the handshake. Per-MCP-session clientInfo is captured by the\n\t// MCP transport and exposed via server.getClientVersion().\n\tconst name = clientInfo?.name;\n\tif (typeof name === \"string\" && name.length > 0) {\n\t\tconst lower = name.toLowerCase();\n\t\tfor (const { needle, source } of CLIENT_INFO_NAME_SOURCES) {\n\t\t\tif (lower.includes(needle)) {\n\t\t\t\treturn source;\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * HTTP request headers that identify a known caller when neither `_meta` nor\n * `clientInfo` carries a source. This is the most robust Claude signal: every\n * Claude surface sends `User-Agent: Claude-User` and `x-anthropic-client` on\n * MCP HTTP requests, whereas the MCP transport carries no session id and the\n * `initialize` `clientInfo` may not be surfaced on stateless deployments.\n */\nfunction headerValue(\n\theaders: Record<string, unknown>,\n\tkey: string,\n): string | undefined {\n\t// HTTP header names are case-insensitive; the transport may preserve the\n\t// original casing, so match the key case-insensitively rather than assuming\n\t// it was lowercased.\n\tconst lowerKey = key.toLowerCase();\n\tlet raw = headers[key] ?? headers[lowerKey];\n\tif (raw === undefined) {\n\t\tfor (const headerKey of Object.keys(headers)) {\n\t\t\tif (headerKey.toLowerCase() === lowerKey) {\n\t\t\t\traw = headers[headerKey];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tconst value = Array.isArray(raw) ? raw[0] : raw;\n\treturn typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nexport function extractSourceFromHeaders(\n\theaders: Record<string, unknown> | undefined,\n): string | undefined {\n\tif (!headers) {\n\t\treturn undefined;\n\t}\n\tconst userAgent = headerValue(headers, \"user-agent\");\n\tconst anthropicClient = headerValue(headers, \"x-anthropic-client\");\n\tif (\n\t\t(userAgent && /claude/i.test(userAgent)) ||\n\t\t(anthropicClient && /claude|anthropic/i.test(anthropicClient))\n\t) {\n\t\treturn \"claude\";\n\t}\n\t// ChatGPT / OpenAI surfaces without an `openai/*` session key still send an\n\t// OpenAI user-agent (e.g. `ChatGPT-User`), so attribute them here too.\n\tif (userAgent && /chatgpt|openai/i.test(userAgent)) {\n\t\treturn \"chatgpt\";\n\t}\n\treturn undefined;\n}\n","import {\n\textractCorrelationId,\n\textractExternalUserId,\n\textractRequestId,\n\textractSessionId,\n\textractSource,\n\textractTraceId,\n\textractVisitorId,\n} from \"../mcp/server/utils.js\";\nimport type { EventType, LegacyTrackEvent, TrackInput } from \"./@types.js\";\nimport type { V2CorrelationIds, V2EventEnvelope } from \"./v2-types.js\";\n\nconst DEFAULT_SOURCE = \"@waniwani/sdk\";\n\nexport interface MapTrackEventOptions {\n\tnow?: () => Date;\n\tgenerateId?: () => string;\n\tsource?: string;\n}\n\nexport function mapTrackEventToV2(\n\tinput: TrackInput,\n\toptions: MapTrackEventOptions = {},\n): V2EventEnvelope {\n\tconst now = options.now ?? (() => new Date());\n\tconst generateId = options.generateId ?? createEventId;\n\tconst eventName = resolveEventName(input);\n\tconst meta = toRecord(input.meta);\n\tconst metadata = toRecord(input.metadata);\n\tconst correlation = resolveCorrelationIds(input, meta);\n\tconst eventId = takeNonEmptyString(input.eventId) ?? generateId();\n\tconst timestamp = normalizeTimestamp(input.timestamp, now);\n\tconst source =\n\t\ttakeNonEmptyString(input.source) ??\n\t\textractSource(meta) ??\n\t\toptions.source ??\n\t\tDEFAULT_SOURCE;\n\tconst rawLegacy = isLegacyTrackEvent(input) ? { ...input } : undefined;\n\n\tconst mappedMetadata: Record<string, unknown> = {\n\t\t...metadata,\n\t};\n\tif (Object.keys(meta).length > 0) {\n\t\tmappedMetadata.meta = meta;\n\t}\n\tif (rawLegacy) {\n\t\tmappedMetadata.rawLegacy = rawLegacy;\n\t}\n\n\treturn {\n\t\tid: eventId,\n\t\ttype: \"mcp.event\",\n\t\tname: eventName,\n\t\tsource,\n\t\ttimestamp,\n\t\tcorrelation,\n\t\tproperties: mapProperties(input, eventName),\n\t\tmetadata: mappedMetadata,\n\t\trawLegacy,\n\t};\n}\n\nexport function createEventId(): string {\n\tif (\n\t\ttypeof crypto !== \"undefined\" &&\n\t\ttypeof crypto.randomUUID === \"function\"\n\t) {\n\t\treturn `evt_${crypto.randomUUID()}`;\n\t}\n\n\treturn `evt_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;\n}\n\nfunction mapProperties(\n\tinput: TrackInput,\n\teventName: EventType,\n): Record<string, unknown> {\n\tif (!isLegacyTrackEvent(input)) {\n\t\treturn toRecord(input.properties);\n\t}\n\n\tconst legacyProperties = mapLegacyProperties(input, eventName);\n\tconst explicitProperties = toRecord(input.properties);\n\treturn {\n\t\t...legacyProperties,\n\t\t...explicitProperties,\n\t};\n}\n\nfunction mapLegacyProperties(\n\tinput: LegacyTrackEvent,\n\teventName: EventType,\n): Record<string, unknown> {\n\tswitch (eventName) {\n\t\tcase \"tool.called\": {\n\t\t\tconst properties: Record<string, unknown> = {};\n\t\t\tif (takeNonEmptyString(input.toolName)) {\n\t\t\t\tproperties.name = input.toolName;\n\t\t\t}\n\t\t\tif (takeNonEmptyString(input.toolType)) {\n\t\t\t\tproperties.type = input.toolType;\n\t\t\t}\n\t\t\treturn properties;\n\t\t}\n\t\tcase \"link.clicked\": {\n\t\t\tconst properties: Record<string, unknown> = {};\n\t\t\tif (takeNonEmptyString(input.linkUrl)) {\n\t\t\t\tproperties.url = input.linkUrl;\n\t\t\t}\n\t\t\treturn properties;\n\t\t}\n\t\tdefault:\n\t\t\treturn {};\n\t}\n}\n\nfunction resolveEventName(input: TrackInput): EventType {\n\treturn isLegacyTrackEvent(input) ? input.eventType : input.event;\n}\n\nfunction resolveCorrelationIds(\n\tinput: TrackInput,\n\tmeta: Record<string, unknown>,\n): V2CorrelationIds {\n\tconst requestId =\n\t\ttakeNonEmptyString(input.requestId) ?? extractRequestId(meta);\n\n\tconst sessionId =\n\t\ttakeNonEmptyString(input.sessionId) ?? extractSessionId(meta);\n\n\tconst traceId = takeNonEmptyString(input.traceId) ?? extractTraceId(meta);\n\n\tconst externalUserId =\n\t\ttakeNonEmptyString(input.externalUserId) ?? extractExternalUserId(meta);\n\n\tconst visitorId =\n\t\ttakeNonEmptyString(input.visitorId) ?? extractVisitorId(meta);\n\n\tconst correlationId =\n\t\ttakeNonEmptyString(input.correlationId) ??\n\t\textractCorrelationId(meta) ??\n\t\trequestId;\n\n\tconst correlation: V2CorrelationIds = {};\n\tif (sessionId) {\n\t\tcorrelation.sessionId = sessionId;\n\t}\n\tif (traceId) {\n\t\tcorrelation.traceId = traceId;\n\t}\n\tif (requestId) {\n\t\tcorrelation.requestId = requestId;\n\t}\n\tif (correlationId) {\n\t\tcorrelation.correlationId = correlationId;\n\t}\n\tif (externalUserId) {\n\t\tcorrelation.externalUserId = externalUserId;\n\t}\n\tif (visitorId) {\n\t\tcorrelation.visitorId = visitorId;\n\t}\n\treturn correlation;\n}\n\nfunction normalizeTimestamp(\n\tinput: string | Date | undefined,\n\tnow: () => Date,\n): string {\n\tif (input instanceof Date) {\n\t\treturn input.toISOString();\n\t}\n\tif (typeof input === \"string\") {\n\t\tconst date = new Date(input);\n\t\tif (!Number.isNaN(date.getTime())) {\n\t\t\treturn date.toISOString();\n\t\t}\n\t}\n\treturn now().toISOString();\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn {};\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction takeNonEmptyString(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tif (value.trim().length === 0) {\n\t\treturn undefined;\n\t}\n\treturn value;\n}\n\nfunction isLegacyTrackEvent(input: TrackInput): input is LegacyTrackEvent {\n\treturn \"eventType\" in input;\n}\n","import type { RevenueTrackingApi, TrackInput } from \"./@types.js\";\n\n/** Enqueue one event and resolve to its id. */\ntype Emit = (event: TrackInput) => Promise<{ eventId: string }>;\n\n/**\n * Build the flat `track.*` revenue helpers over an `emit` function. Shared\n * by the top-level client and the request-scoped client (which injects meta\n * into `emit`) so both expose the same revenue API (WAN-386).\n *\n * Each helper maps its flat input to a typed first-class revenue event; the\n * remaining tracking context (sessionId / externalUserId / meta) is forwarded\n * as-is. Identity (one of those, or meta-derived) is required by the ingest\n * API — see the warning emitted from the client's `emit`.\n */\nexport function createRevenueApi(emit: Emit): RevenueTrackingApi {\n\treturn {\n\t\tpriceShown: ({ amount, currency, itemId, label, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"price_shown\",\n\t\t\t\tproperties: { amount, currency, itemId, label },\n\t\t\t\t...context,\n\t\t\t}),\n\t\tpricesCompared: ({ options, ...context }) =>\n\t\t\temit({ event: \"prices_compared\", properties: { options }, ...context }),\n\t\toptionSelected: ({ id, amount, currency, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"option_selected\",\n\t\t\t\tproperties: { id, amount, currency },\n\t\t\t\t...context,\n\t\t\t}),\n\t\tleadQualified: (input) => {\n\t\t\tconst { externalId, email, name, ...context } = input ?? {};\n\t\t\treturn emit({\n\t\t\t\tevent: \"lead_qualified\",\n\t\t\t\tproperties: { externalId, email, name },\n\t\t\t\t...context,\n\t\t\t});\n\t\t},\n\t\tconverted: ({ amount, currency, occurredAt, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"converted\",\n\t\t\t\tproperties: { amount, currency, occurredAt },\n\t\t\t\t...context,\n\t\t\t}),\n\t};\n}\n","import type {\n\tTrackingShutdownOptions,\n\tTrackingShutdownResult,\n} from \"./@types.js\";\nimport type {\n\tV2BatchRejectedEvent,\n\tV2BatchRequest,\n\tV2BatchResponse,\n\tV2EventEnvelope,\n} from \"./v2-types.js\";\n\ntype FetchFn = (\n\tinput: URL | RequestInfo,\n\tinit?: RequestInit,\n) => Promise<Response>;\n\nconst DEFAULT_ENDPOINT_PATH = \"/api/mcp/events/v2/batch\";\nconst DEFAULT_FLUSH_INTERVAL_MS = 1_000;\nconst DEFAULT_MAX_BATCH_SIZE = 20;\nconst DEFAULT_MAX_BUFFER_SIZE = 1_000;\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_RETRY_BASE_DELAY_MS = 200;\nconst DEFAULT_RETRY_MAX_DELAY_MS = 2_000;\nconst DEFAULT_SHUTDOWN_TIMEOUT_MS = 2_000;\nconst SDK_NAME = \"@waniwani/sdk\";\n// Keepalive requests are capped by browsers (64KiB in-flight quota); stay under it.\nconst TEARDOWN_MAX_BODY_BYTES = 60_000;\n\nconst AUTH_FAILURE_STATUS = new Set([401, 403]);\nconst RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);\n\ninterface Logger {\n\twarn: (message: string, ...args: unknown[]) => void;\n\terror: (message: string, ...args: unknown[]) => void;\n}\n\nexport interface V2TransportOptions {\n\tapiUrl: string;\n\t/**\n\t * Bearer credential for the ingest endpoint: secret key (`wwk_`), public\n\t * token (`wwp_`), or widget JWT. Omit only for unauthenticated proxies.\n\t */\n\tapiKey?: string;\n\tendpointPath?: string;\n\tflushIntervalMs?: number;\n\tmaxBatchSize?: number;\n\tmaxBufferSize?: number;\n\tmaxRetries?: number;\n\tretryBaseDelayMs?: number;\n\tretryMaxDelayMs?: number;\n\tshutdownTimeoutMs?: number;\n\tsdkVersion?: string;\n\tfetchFn?: FetchFn;\n\tlogger?: Logger;\n\tnow?: () => Date;\n\tsleep?: (delayMs: number) => Promise<void>;\n}\n\nexport interface V2BatchTransport {\n\tenqueue: (event: V2EventEnvelope) => void;\n\tflush: () => Promise<void>;\n\tshutdown: (\n\t\toptions?: TrackingShutdownOptions,\n\t) => Promise<TrackingShutdownResult>;\n\tpendingEvents: () => number;\n}\n\ntype SendBatchResult =\n\t| { kind: \"success\" }\n\t| { kind: \"retryable\"; reason: string }\n\t| { kind: \"permanent\"; reason: string }\n\t| { kind: \"auth\"; status: number }\n\t| {\n\t\t\tkind: \"partial\";\n\t\t\tretryable: V2EventEnvelope[];\n\t\t\tpermanent: V2EventEnvelope[];\n\t };\n\nexport function createV2BatchTransport(\n\toptions: V2TransportOptions,\n): V2BatchTransport {\n\treturn new BatchingV2Transport(options);\n}\n\nclass BatchingV2Transport implements V2BatchTransport {\n\tprivate readonly endpointUrl: string;\n\tprivate readonly flushIntervalMs: number;\n\tprivate readonly maxBatchSize: number;\n\tprivate readonly maxBufferSize: number;\n\tprivate readonly maxRetries: number;\n\tprivate readonly retryBaseDelayMs: number;\n\tprivate readonly retryMaxDelayMs: number;\n\tprivate readonly shutdownTimeoutMs: number;\n\tprivate readonly sdkVersion?: string;\n\tprivate readonly fetchFn: FetchFn;\n\tprivate readonly logger: Logger;\n\tprivate readonly now: () => Date;\n\tprivate readonly sleep: (delayMs: number) => Promise<void>;\n\tprivate readonly apiKey?: string;\n\n\tprivate readonly buffer: V2EventEnvelope[] = [];\n\tprivate flushTimer: ReturnType<typeof setInterval> | undefined;\n\tprivate flushScheduled = false;\n\tprivate flushScheduledTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate flushInFlight: Promise<void> | undefined;\n\tprivate inFlightCount = 0;\n\tprivate isStopped = false;\n\tprivate isShuttingDown = false;\n\n\tconstructor(options: V2TransportOptions) {\n\t\tthis.endpointUrl = joinUrl(\n\t\t\toptions.apiUrl,\n\t\t\toptions.endpointPath ?? DEFAULT_ENDPOINT_PATH,\n\t\t);\n\t\tthis.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;\n\t\tthis.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;\n\t\tthis.maxBufferSize = options.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;\n\t\tthis.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n\t\tthis.retryBaseDelayMs =\n\t\t\toptions.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;\n\t\tthis.retryMaxDelayMs =\n\t\t\toptions.retryMaxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS;\n\t\tthis.shutdownTimeoutMs =\n\t\t\toptions.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;\n\t\tthis.fetchFn = options.fetchFn ?? fetch;\n\t\tthis.logger = options.logger ?? console;\n\t\tthis.now = options.now ?? (() => new Date());\n\t\tthis.sleep =\n\t\t\toptions.sleep ??\n\t\t\t((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.sdkVersion = options.sdkVersion;\n\n\t\tif (this.flushIntervalMs > 0) {\n\t\t\tthis.flushTimer = setInterval(() => {\n\t\t\t\tvoid this.flush();\n\t\t\t}, this.flushIntervalMs);\n\t\t}\n\n\t\tthis.registerBrowserTeardown();\n\t}\n\n\tenqueue(event: V2EventEnvelope): void {\n\t\tif (this.isStopped || this.isShuttingDown) {\n\t\t\tthis.logger.warn(\n\t\t\t\t\"[Waniwani] Tracking transport is stopped, dropping event %s\",\n\t\t\t\tevent.id,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.buffer.length >= this.maxBufferSize) {\n\t\t\tconst dropCount = this.buffer.length - this.maxBufferSize + 1;\n\t\t\tthis.buffer.splice(0, dropCount);\n\t\t\tthis.logger.warn(\n\t\t\t\t\"[Waniwani] Tracking buffer overflow, dropped %d oldest event(s)\",\n\t\t\t\tdropCount,\n\t\t\t);\n\t\t}\n\n\t\tthis.buffer.push(event);\n\n\t\tif (this.buffer.length >= this.maxBatchSize) {\n\t\t\tvoid this.flush();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.scheduleMicroFlush();\n\t}\n\n\tpendingEvents(): number {\n\t\treturn this.buffer.length + this.inFlightCount;\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tif (this.flushInFlight) {\n\t\t\treturn this.flushInFlight;\n\t\t}\n\t\tthis.flushInFlight = this.flushLoop().finally(() => {\n\t\t\tthis.flushInFlight = undefined;\n\t\t});\n\t\treturn this.flushInFlight;\n\t}\n\n\tasync shutdown(\n\t\toptions?: TrackingShutdownOptions,\n\t): Promise<TrackingShutdownResult> {\n\t\tthis.isShuttingDown = true;\n\t\tif (this.flushTimer) {\n\t\t\tclearInterval(this.flushTimer);\n\t\t\tthis.flushTimer = undefined;\n\t\t}\n\t\tif (this.flushScheduledTimer) {\n\t\t\tclearTimeout(this.flushScheduledTimer);\n\t\t\tthis.flushScheduledTimer = undefined;\n\t\t\tthis.flushScheduled = false;\n\t\t}\n\n\t\tconst timeoutMs = options?.timeoutMs ?? this.shutdownTimeoutMs;\n\t\tconst flushPromise = this.flush();\n\n\t\tif (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n\t\t\tawait flushPromise;\n\t\t\tthis.isStopped = true;\n\t\t\treturn { timedOut: false, pendingEvents: this.pendingEvents() };\n\t\t}\n\n\t\tconst timeoutSignal = Symbol(\"shutdown-timeout\");\n\t\tconst result = await Promise.race([\n\t\t\tflushPromise.then(() => \"flushed\" as const),\n\t\t\tthis.sleep(timeoutMs).then(() => timeoutSignal),\n\t\t]);\n\n\t\tif (result === timeoutSignal) {\n\t\t\tthis.isStopped = true;\n\t\t\treturn { timedOut: true, pendingEvents: this.pendingEvents() };\n\t\t}\n\n\t\tthis.isStopped = true;\n\t\treturn { timedOut: false, pendingEvents: this.pendingEvents() };\n\t}\n\n\t/**\n\t * In browsers, drain the buffer with keepalive fetches when the page is\n\t * hidden or unloading. The regular async flush loop does not survive\n\t * navigation; `keepalive` requests do. No-op outside a DOM environment.\n\t */\n\tprivate registerBrowserTeardown(): void {\n\t\tif (typeof document === \"undefined\" || typeof window === \"undefined\") {\n\t\t\treturn;\n\t\t}\n\n\t\tconst teardownFlush = () => {\n\t\t\tif (document.visibilityState === \"hidden\") {\n\t\t\t\tthis.keepaliveFlush();\n\t\t\t}\n\t\t};\n\t\tdocument.addEventListener(\"visibilitychange\", teardownFlush);\n\t\twindow.addEventListener(\"pagehide\", () => this.keepaliveFlush());\n\t}\n\n\t/** Fire-and-forget flush of everything buffered, chunked under the keepalive body cap. */\n\tprivate keepaliveFlush(): void {\n\t\tif (this.isStopped || this.buffer.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst events = this.buffer.splice(0, this.buffer.length);\n\t\tthis.sendKeepaliveChunk(events);\n\t}\n\n\tprivate sendKeepaliveChunk(events: V2EventEnvelope[]): void {\n\t\tconst body = JSON.stringify(this.makeBatchRequest(events));\n\t\tif (body.length > TEARDOWN_MAX_BODY_BYTES && events.length > 1) {\n\t\t\tconst mid = Math.ceil(events.length / 2);\n\t\t\tthis.sendKeepaliveChunk(events.slice(0, mid));\n\t\t\tthis.sendKeepaliveChunk(events.slice(mid));\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tvoid this.fetchFn(this.endpointUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: this.requestHeaders(),\n\t\t\t\tbody,\n\t\t\t\tkeepalive: true,\n\t\t\t}).catch(() => {});\n\t\t} catch {\n\t\t\t// Teardown is best-effort; never throw during page unload.\n\t\t}\n\t}\n\n\tprivate requestHeaders(): Record<string, string> {\n\t\tconst headers: Record<string, string> = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"X-WaniWani-SDK\": SDK_NAME,\n\t\t};\n\t\tif (this.apiKey) {\n\t\t\theaders.Authorization = `Bearer ${this.apiKey}`;\n\t\t}\n\t\treturn headers;\n\t}\n\n\tprivate scheduleMicroFlush(): void {\n\t\tif (this.flushScheduled) {\n\t\t\treturn;\n\t\t}\n\t\tthis.flushScheduled = true;\n\t\tthis.flushScheduledTimer = setTimeout(() => {\n\t\t\tthis.flushScheduledTimer = undefined;\n\t\t\tthis.flushScheduled = false;\n\t\t\tvoid this.flush();\n\t\t}, 0);\n\t}\n\n\tprivate async flushLoop(): Promise<void> {\n\t\twhile (this.buffer.length > 0 && !this.isStopped) {\n\t\t\tconst batch = this.buffer.splice(0, this.maxBatchSize);\n\t\t\tawait this.sendBatchWithRetry(batch);\n\t\t}\n\t}\n\n\tprivate async sendBatchWithRetry(batch: V2EventEnvelope[]): Promise<void> {\n\t\tlet attempt = 0;\n\t\tlet pendingBatch = batch;\n\n\t\twhile (pendingBatch.length > 0 && !this.isStopped) {\n\t\t\tthis.inFlightCount = pendingBatch.length;\n\t\t\tconst result = await this.sendBatchOnce(pendingBatch);\n\t\t\tthis.inFlightCount = 0;\n\n\t\t\tswitch (result.kind) {\n\t\t\t\tcase \"success\":\n\t\t\t\t\treturn;\n\t\t\t\tcase \"auth\":\n\t\t\t\t\tthis.stopTransportForAuthFailure(result.status, pendingBatch.length);\n\t\t\t\t\treturn;\n\t\t\t\tcase \"permanent\":\n\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) after permanent failure: %s\",\n\t\t\t\t\t\tpendingBatch.length,\n\t\t\t\t\t\tresult.reason,\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\tcase \"retryable\":\n\t\t\t\t\tif (attempt >= this.maxRetries) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) after retry exhaustion: %s\",\n\t\t\t\t\t\t\tpendingBatch.length,\n\t\t\t\t\t\t\tresult.reason,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait this.sleep(this.backoffDelayMs(attempt));\n\t\t\t\t\tattempt += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\tcase \"partial\":\n\t\t\t\t\tif (result.permanent.length > 0) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) rejected as permanent\",\n\t\t\t\t\t\t\tresult.permanent.length,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (result.retryable.length === 0) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (attempt >= this.maxRetries) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d retryable event(s) after retry exhaustion\",\n\t\t\t\t\t\t\tresult.retryable.length,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tpendingBatch = result.retryable;\n\t\t\t\t\tawait this.sleep(this.backoffDelayMs(attempt));\n\t\t\t\t\tattempt += 1;\n\t\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async sendBatchOnce(\n\t\tevents: V2EventEnvelope[],\n\t): Promise<SendBatchResult> {\n\t\tlet response: Response;\n\n\t\ttry {\n\t\t\tresponse = await this.fetchFn(this.endpointUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: this.requestHeaders(),\n\t\t\t\tbody: JSON.stringify(this.makeBatchRequest(events)),\n\t\t\t});\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tkind: \"retryable\",\n\t\t\t\treason: getErrorMessage(error),\n\t\t\t};\n\t\t}\n\n\t\tif (AUTH_FAILURE_STATUS.has(response.status)) {\n\t\t\treturn { kind: \"auth\", status: response.status };\n\t\t}\n\n\t\tif (RETRYABLE_STATUS.has(response.status)) {\n\t\t\treturn {\n\t\t\t\tkind: \"retryable\",\n\t\t\t\treason: `HTTP ${response.status}`,\n\t\t\t};\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\treturn {\n\t\t\t\tkind: \"permanent\",\n\t\t\t\treason: `HTTP ${response.status}`,\n\t\t\t};\n\t\t}\n\n\t\tconst data = await parseJsonResponse<V2BatchResponse>(response);\n\t\tif (!data?.rejected || data.rejected.length === 0) {\n\t\t\treturn { kind: \"success\" };\n\t\t}\n\n\t\tconst partial = this.classifyRejectedEvents(events, data.rejected);\n\t\tif (partial.retryable.length === 0 && partial.permanent.length === 0) {\n\t\t\treturn { kind: \"success\" };\n\t\t}\n\n\t\treturn {\n\t\t\tkind: \"partial\",\n\t\t\tretryable: partial.retryable,\n\t\t\tpermanent: partial.permanent,\n\t\t};\n\t}\n\n\tprivate makeBatchRequest(events: V2EventEnvelope[]): V2BatchRequest {\n\t\treturn {\n\t\t\tsentAt: this.now().toISOString(),\n\t\t\tsource: {\n\t\t\t\tsdk: SDK_NAME,\n\t\t\t\tversion: this.sdkVersion ?? \"0.0.0\",\n\t\t\t},\n\t\t\tevents,\n\t\t};\n\t}\n\n\tprivate classifyRejectedEvents(\n\t\tevents: V2EventEnvelope[],\n\t\trejected: V2BatchRejectedEvent[],\n\t): {\n\t\tretryable: V2EventEnvelope[];\n\t\tpermanent: V2EventEnvelope[];\n\t} {\n\t\tconst byId = new Map(events.map((event) => [event.id, event]));\n\t\tconst retryable: V2EventEnvelope[] = [];\n\t\tconst permanent: V2EventEnvelope[] = [];\n\n\t\tfor (const rejectedEvent of rejected) {\n\t\t\tconst event = byId.get(rejectedEvent.eventId);\n\t\t\tif (!event) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isRetryableRejectedEvent(rejectedEvent)) {\n\t\t\t\tretryable.push(event);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tpermanent.push(event);\n\t\t}\n\n\t\treturn { retryable, permanent };\n\t}\n\n\tprivate backoffDelayMs(attempt: number): number {\n\t\tconst rawDelay = this.retryBaseDelayMs * 2 ** attempt;\n\t\treturn Math.min(rawDelay, this.retryMaxDelayMs);\n\t}\n\n\tprivate stopTransportForAuthFailure(\n\t\tstatus: number,\n\t\trejectedCount: number,\n\t): void {\n\t\tthis.isStopped = true;\n\t\tconst buffered = this.buffer.length;\n\t\tthis.buffer.splice(0, buffered);\n\t\tthis.logger.error(\n\t\t\t\"[Waniwani] Auth failure (HTTP %d). Stopping tracking transport and dropping %d queued event(s)\",\n\t\t\tstatus,\n\t\t\trejectedCount + buffered,\n\t\t);\n\t}\n}\n\nfunction isRetryableRejectedEvent(\n\trejectedEvent: V2BatchRejectedEvent,\n): boolean {\n\tif (rejectedEvent.retryable === true) {\n\t\treturn true;\n\t}\n\tconst code = rejectedEvent.code.toLowerCase();\n\treturn (\n\t\tcode.includes(\"timeout\") ||\n\t\tcode.includes(\"temporary\") ||\n\t\tcode.includes(\"unavailable\") ||\n\t\tcode.includes(\"rate_limit\") ||\n\t\tcode.includes(\"transient\") ||\n\t\tcode.includes(\"server\")\n\t);\n}\n\nasync function parseJsonResponse<T>(\n\tresponse: Response,\n): Promise<T | undefined> {\n\tconst body = await response.text();\n\tif (!body) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn JSON.parse(body) as T;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction joinUrl(apiUrl: string, endpointPath: string): string {\n\tconst normalizedBase = apiUrl.endsWith(\"/\") ? apiUrl : `${apiUrl}/`;\n\tconst normalizedPath = endpointPath.startsWith(\"/\")\n\t\t? endpointPath.slice(1)\n\t\t: endpointPath;\n\treturn `${normalizedBase}${normalizedPath}`;\n}\n\nfunction getErrorMessage(error: unknown): string {\n\tif (error instanceof Error) {\n\t\treturn error.message;\n\t}\n\treturn String(error);\n}\n","import type {\n\tTrackFn,\n\tTrackInput,\n\tTrackingShutdownOptions,\n\tTrackingShutdownResult,\n} from \"./@types.js\";\nimport { mapTrackEventToV2 } from \"./mapper.js\";\nimport { createRevenueApi } from \"./revenue.js\";\nimport { createV2BatchTransport } from \"./transport.js\";\n\n/**\n * Correlation identity read at emit time, so identifiers that only exist\n * later (a server-assigned chat session id, for example) are picked up on\n * every event without re-creating the client.\n */\nexport interface FrontendIdentity {\n\tsessionId?: string;\n\tvisitorId?: string;\n\texternalUserId?: string;\n\ttraceId?: string;\n}\n\nexport interface FrontendClientOptions {\n\t/**\n\t * Full URL of the events ingest endpoint\n\t * (e.g. `https://app.waniwani.ai/api/mcp/events/v2/batch`).\n\t */\n\tendpoint: string;\n\t/**\n\t * Browser-safe credential: an environment public token (`wwp_...`) or a\n\t * widget JWT injected by `withWaniwani` into tool response `_meta`. Omit\n\t * only when the endpoint is your own unauthenticated proxy route.\n\t */\n\ttoken?: string;\n\t/**\n\t * Channel attribution stamped on every event (e.g. `\"chatgpt\"`, `\"web\"`).\n\t * The ingest API rejects events it cannot attribute to a channel, so set\n\t * this (or `channelId`) unless every event carries its own. Accepts a\n\t * getter for values that resolve after client creation.\n\t */\n\tsource?: string | (() => string | undefined);\n\t/**\n\t * Exact channel attribution, stamped as `properties.channelId` on every\n\t * event that does not set its own. Accepts a getter for values that\n\t * resolve after client creation.\n\t */\n\tchannelId?: string | (() => string | undefined);\n\t/** Live identity, called on every emit. */\n\tidentity?: () => FrontendIdentity;\n\t/** Extra fields merged into every event's envelope metadata. */\n\tmetadata?: Record<string, unknown>;\n\t/** Periodic flush interval for buffered events. */\n\tflushIntervalMs?: number;\n}\n\n/**\n * The browser counterpart of the server tracking client: the exact same\n * `track` surface (callable plus the flat revenue helpers) over the same\n * batching transport, with identity stamped automatically.\n */\nexport interface FrontendTrackingClient {\n\ttrack: TrackFn;\n\t/**\n\t * Attach a stable external user id. Emits `user.identified` and stamps\n\t * `externalUserId` on every subsequent event from this client.\n\t */\n\tidentify(\n\t\tuserId: string,\n\t\tproperties?: Record<string, unknown>,\n\t): Promise<{ eventId: string }>;\n\tflush(): Promise<void>;\n\tshutdown(options?: TrackingShutdownOptions): Promise<TrackingShutdownResult>;\n}\n\n/**\n * Create a tracking client for browser surfaces (MCP-app widgets, the chat\n * embed, or any page holding a `wwp_` public token). Events flush in batches,\n * survive page navigation via keepalive teardown, and never throw into the\n * host page.\n */\nexport function createFrontendClient(\n\toptions: FrontendClientOptions,\n): FrontendTrackingClient {\n\tconst { apiUrl, endpointPath } = splitEndpoint(options.endpoint);\n\tconst transport = createV2BatchTransport({\n\t\tapiUrl,\n\t\tapiKey: options.token,\n\t\tendpointPath,\n\t\tflushIntervalMs: options.flushIntervalMs,\n\t});\n\n\tlet identifiedUserId: string | undefined;\n\n\tconst resolve = (\n\t\tvalue: string | (() => string | undefined) | undefined,\n\t): string | undefined => (typeof value === \"function\" ? value() : value);\n\n\tconst emit = async (event: TrackInput): Promise<{ eventId: string }> => {\n\t\tconst identity = options.identity?.() ?? {};\n\t\tconst channelId = resolve(options.channelId);\n\t\tconst properties =\n\t\t\tchannelId === undefined\n\t\t\t\t? event.properties\n\t\t\t\t: { channelId, ...event.properties };\n\t\tconst mapped = mapTrackEventToV2(\n\t\t\t{\n\t\t\t\tsessionId: identity.sessionId,\n\t\t\t\tvisitorId: identity.visitorId,\n\t\t\t\texternalUserId: identity.externalUserId ?? identifiedUserId,\n\t\t\t\ttraceId: identity.traceId,\n\t\t\t\t...event,\n\t\t\t\tproperties,\n\t\t\t\tmetadata: { ...options.metadata, ...event.metadata },\n\t\t\t} as TrackInput,\n\t\t\t{ source: resolve(options.source) },\n\t\t);\n\t\ttransport.enqueue(mapped);\n\t\treturn { eventId: mapped.id };\n\t};\n\n\treturn {\n\t\ttrack: Object.assign(emit, createRevenueApi(emit)),\n\t\tidentify(userId, properties) {\n\t\t\tidentifiedUserId = userId;\n\t\t\treturn emit({\n\t\t\t\tevent: \"user.identified\",\n\t\t\t\texternalUserId: userId,\n\t\t\t\tproperties,\n\t\t\t});\n\t\t},\n\t\tflush: () => transport.flush(),\n\t\tshutdown: (shutdownOptions) => transport.shutdown(shutdownOptions),\n\t};\n}\n\nfunction splitEndpoint(endpoint: string): {\n\tapiUrl: string;\n\tendpointPath: string;\n} {\n\tconst url = new URL(endpoint);\n\treturn { apiUrl: url.origin, endpointPath: `${url.pathname}${url.search}` };\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { KbSearchTrace } from \"./types.js\";\n\n// Per-request bucket the tool wrapper opens; kb.search records into it.\nexport interface RetrievalCollector {\n\tsearches: KbSearchTrace[];\n}\n\n// Global slot, not a module-level instance: core (kb.search) and mcp\n// (withWaniwani) ship as separate bundles, so only globalThis keeps a single\n// shared AsyncLocalStorage per process. Two SDK copies in one process share\n// this slot, so RetrievalCollector's shape must stay backward-compatible.\nconst globalWithStore = globalThis as typeof globalThis & {\n\t__waniwaniKbRetrievalStore?: AsyncLocalStorage<RetrievalCollector>;\n};\n\nlet store = globalWithStore.__waniwaniKbRetrievalStore;\nif (!store) {\n\tstore = new AsyncLocalStorage<RetrievalCollector>();\n\tglobalWithStore.__waniwaniKbRetrievalStore = store;\n}\nexport const retrievalCollectorStore = store;\n\n// Best-effort: recording runs inside kb.search(), so it must never throw back\n// into a user's search (e.g. a corrupted collector from a mixed SDK version).\nexport function recordKbSearch(trace: KbSearchTrace): void {\n\ttry {\n\t\tretrievalCollectorStore.getStore()?.searches.push(trace);\n\t} catch {}\n}\n","// KB Client — thin HTTP wrapper for knowledge base API\n\nimport { WaniWaniError } from \"../error.js\";\nimport type { InternalConfig } from \"../types.js\";\nimport { recordKbSearch } from \"./retrieval-context.js\";\nimport type {\n\tKbClient,\n\tKbIngestFile,\n\tKbIngestResult,\n\tKbSearchOptions,\n\tKbSource,\n\tSearchResult,\n} from \"./types.js\";\n\nconst SDK_NAME = \"@waniwani/sdk\";\n\nexport function createKbClient(config: InternalConfig): KbClient {\n\tconst { apiUrl, apiKey } = config;\n\n\tfunction requireApiKey(): string {\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(\"WANIWANI_API_KEY is not set\");\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\tasync function request<T>(\n\t\tmethod: \"GET\" | \"POST\",\n\t\tpath: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst key = requireApiKey();\n\t\tconst url = `${apiUrl.replace(/\\/$/, \"\")}${path}`;\n\n\t\tconst headers: Record<string, string> = {\n\t\t\tAuthorization: `Bearer ${key}`,\n\t\t\t\"X-WaniWani-SDK\": SDK_NAME,\n\t\t};\n\n\t\tconst init: RequestInit = { method, headers };\n\n\t\tif (body !== undefined) {\n\t\t\theaders[\"Content-Type\"] = \"application/json\";\n\t\t\tinit.body = JSON.stringify(body);\n\t\t}\n\n\t\tconst response = await fetch(url, init);\n\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tthrow new WaniWaniError(\n\t\t\t\ttext || `KB API error: HTTP ${response.status}`,\n\t\t\t\tresponse.status,\n\t\t\t);\n\t\t}\n\n\t\tconst json = (await response.json()) as { data: T };\n\t\treturn json.data;\n\t}\n\n\treturn {\n\t\tasync ingest(files: KbIngestFile[]): Promise<KbIngestResult> {\n\t\t\treturn request<KbIngestResult>(\"POST\", \"/api/mcp/kb/ingest\", {\n\t\t\t\tfiles,\n\t\t\t});\n\t\t},\n\n\t\tasync search(\n\t\t\tquery: string,\n\t\t\toptions?: KbSearchOptions,\n\t\t): Promise<SearchResult[]> {\n\t\t\tconst results = await request<SearchResult[]>(\n\t\t\t\t\"POST\",\n\t\t\t\t\"/api/mcp/kb/search\",\n\t\t\t\t{\n\t\t\t\t\tquery,\n\t\t\t\t\t...options,\n\t\t\t\t},\n\t\t\t);\n\t\t\trecordKbSearch({\n\t\t\t\tquery,\n\t\t\t\tresultCount: results.length,\n\t\t\t\tresults: results.map((r) => ({\n\t\t\t\t\tsource: r.source,\n\t\t\t\t\theading: r.heading,\n\t\t\t\t\tscore: r.score,\n\t\t\t\t})),\n\t\t\t});\n\t\t\treturn results;\n\t\t},\n\n\t\tasync sources(): Promise<KbSource[]> {\n\t\t\treturn request<KbSource[]>(\"GET\", \"/api/mcp/kb/sources\");\n\t\t},\n\t};\n}\n","// Tracking Module\n\nimport type { InternalConfig } from \"../types.js\";\nimport type {\n\tTrackFn,\n\tTrackInput,\n\tTrackingClient,\n\tTrackingShutdownOptions,\n} from \"./@types.js\";\nimport { mapTrackEventToV2 } from \"./mapper.js\";\nimport { createRevenueApi } from \"./revenue.js\";\nimport { createV2BatchTransport } from \"./transport.js\";\n\n// Re-export types\nexport type {\n\tCallableTrack,\n\tComparedPriceOption,\n\tConvertedProperties,\n\tEventType,\n\tLeadQualifiedProperties,\n\tLegacyTrackEvent,\n\tLinkClickedProperties,\n\tOptionSelectedProperties,\n\tPriceShownProperties,\n\tPricesComparedProperties,\n\tRevenueConvertedInput,\n\tRevenueLeadQualifiedInput,\n\tRevenueOptionSelectedInput,\n\tRevenuePriceShownInput,\n\tRevenuePricesComparedInput,\n\tRevenueTrackingApi,\n\tToolCalledProperties,\n\tTrackEvent,\n\tTrackFn,\n\tTrackInput,\n\tTrackingClient,\n\tTrackingConfig,\n\tTrackingShutdownOptions,\n\tTrackingShutdownResult,\n} from \"./@types.js\";\nexport { EVENT_TYPES } from \"./@types.js\";\nexport { createEventId, mapTrackEventToV2 } from \"./mapper.js\";\nexport type {\n\tV2BatchRejectedEvent,\n\tV2BatchRequest,\n\tV2BatchResponse,\n\tV2CorrelationIds,\n\tV2EnvelopeType,\n\tV2EventEnvelope,\n} from \"./v2-types.js\";\n\nexport function createTrackingClient(config: InternalConfig): TrackingClient {\n\tconst { apiUrl, apiKey, tracking } = config;\n\n\tfunction requireApiKey(): string {\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(\"WANIWANI_API_KEY is not set\");\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\tconst transport = apiKey\n\t\t? createV2BatchTransport({\n\t\t\t\tapiUrl,\n\t\t\t\tapiKey,\n\t\t\t\tendpointPath: tracking.endpointPath,\n\t\t\t\tflushIntervalMs: tracking.flushIntervalMs,\n\t\t\t\tmaxBatchSize: tracking.maxBatchSize,\n\t\t\t\tmaxBufferSize: tracking.maxBufferSize,\n\t\t\t\tmaxRetries: tracking.maxRetries,\n\t\t\t\tretryBaseDelayMs: tracking.retryBaseDelayMs,\n\t\t\t\tretryMaxDelayMs: tracking.retryMaxDelayMs,\n\t\t\t\tshutdownTimeoutMs: tracking.shutdownTimeoutMs,\n\t\t\t})\n\t\t: undefined;\n\n\t// Single enqueue path shared by track(), track.* revenue helpers, and identify().\n\tfunction emit(event: TrackInput): { eventId: string } {\n\t\trequireApiKey();\n\t\tconst mappedEvent = mapTrackEventToV2(event);\n\t\t// Identity is required server-side (sessionId, externalUserId, or\n\t\t// visitorId, possibly derived from meta). Warn early: without it the\n\t\t// ingest API rejects the event even though enqueue returns an id here.\n\t\tif (\n\t\t\t!mappedEvent.correlation.sessionId &&\n\t\t\t!mappedEvent.correlation.externalUserId &&\n\t\t\t!mappedEvent.correlation.visitorId\n\t\t) {\n\t\t\tconsole.warn(\n\t\t\t\t`[waniwani] event \"${mappedEvent.name}\" has no sessionId, externalUserId, or visitorId; ` +\n\t\t\t\t\t\"the ingest API requires one and will reject it.\",\n\t\t\t);\n\t\t}\n\t\ttransport?.enqueue(mappedEvent);\n\t\treturn { eventId: mappedEvent.id };\n\t}\n\n\tconst trackOnce = async (event: TrackInput): Promise<{ eventId: string }> =>\n\t\temit(event);\n\t// Revenue helpers attach flat onto `track` (track.priceShown(), …).\n\tconst track: TrackFn = Object.assign(trackOnce, createRevenueApi(trackOnce));\n\n\tconst client: TrackingClient = {\n\t\tasync identify(\n\t\t\tuserId: string,\n\t\t\tproperties?: Record<string, unknown>,\n\t\t\tmeta?: Record<string, unknown>,\n\t\t): Promise<{ eventId: string }> {\n\t\t\trequireApiKey();\n\t\t\tconst mappedEvent = mapTrackEventToV2({\n\t\t\t\tevent: \"user.identified\",\n\t\t\t\texternalUserId: userId,\n\t\t\t\tproperties,\n\t\t\t\tmeta,\n\t\t\t});\n\t\t\ttransport?.enqueue(mappedEvent);\n\t\t\treturn { eventId: mappedEvent.id };\n\t\t},\n\t\ttrack,\n\t\tasync flush(): Promise<void> {\n\t\t\trequireApiKey();\n\t\t\tawait transport?.flush();\n\t\t},\n\t\tasync shutdown(options?: TrackingShutdownOptions) {\n\t\t\trequireApiKey();\n\t\t\treturn (\n\t\t\t\t(await transport?.shutdown({\n\t\t\t\t\ttimeoutMs: options?.timeoutMs ?? tracking.shutdownTimeoutMs,\n\t\t\t\t})) ?? { timedOut: false, pendingEvents: 0 }\n\t\t\t);\n\t\t},\n\t};\n\n\tif (transport) {\n\t\tattachShutdownHooks(client, tracking.shutdownTimeoutMs);\n\t}\n\treturn client;\n}\n\nfunction attachShutdownHooks(\n\tclient: TrackingClient,\n\tdefaultTimeoutMs: number,\n): void {\n\tif (\n\t\ttypeof process === \"undefined\" ||\n\t\ttypeof process.once !== \"function\" ||\n\t\ttypeof process.on !== \"function\"\n\t) {\n\t\treturn;\n\t}\n\n\tconst shutdown = () => {\n\t\tvoid client.shutdown({ timeoutMs: defaultTimeoutMs });\n\t};\n\n\tprocess.once(\"beforeExit\", shutdown);\n\tprocess.once(\"SIGINT\", shutdown);\n\tprocess.once(\"SIGTERM\", shutdown);\n}\n","// Waniwani SDK - Main Entry\n\nimport { createKbClient } from \"./kb/client.js\";\nimport {\n\tgetGlobalConfig,\n\tloadProjectConfig,\n\ttype WaniWaniProjectConfig,\n} from \"./project-config.js\";\nimport { createTrackingClient } from \"./tracking/index.js\";\nimport type { WaniWaniClient, WaniWaniConfig } from \"./types.js\";\n\n/**\n * Create a Waniwani SDK client\n *\n * @param config - Configuration options. When omitted, reads `waniwani.json`\n * from the current working directory, then falls back to env vars.\n * @returns A fully typed Waniwani client\n *\n * @example\n * ```typescript\n * import { waniwani } from \"@waniwani/sdk\";\n * import { toNextJsHandler } from \"@waniwani/sdk/next-js\";\n *\n * const wani = waniwani({ apiKey: \"...\" });\n *\n * // Next.js route handler\n * export const { GET, POST } = toNextJsHandler(wani, {\n * chat: { systemPrompt: \"You are a helpful assistant.\" },\n * });\n * ```\n */\nexport function waniwani(\n\tconfig?: WaniWaniConfig | WaniWaniProjectConfig,\n): WaniWaniClient {\n\tconst projectConfig = config ?? loadProjectConfig() ?? getGlobalConfig();\n\tconst effective = projectConfig as\n\t\t| (WaniWaniConfig & WaniWaniProjectConfig)\n\t\t| undefined;\n\n\tconst apiUrl =\n\t\teffective?.apiUrl ??\n\t\tprocess.env.WANIWANI_API_URL ??\n\t\t\"https://app.waniwani.ai\";\n\tconst apiKey = effective?.apiKey ?? process.env.WANIWANI_API_KEY;\n\tconst trackingConfig = {\n\t\tendpointPath:\n\t\t\teffective?.tracking?.endpointPath ?? \"/api/mcp/events/v2/batch\",\n\t\tflushIntervalMs: effective?.tracking?.flushIntervalMs ?? 1_000,\n\t\tmaxBatchSize: effective?.tracking?.maxBatchSize ?? 20,\n\t\tmaxBufferSize: effective?.tracking?.maxBufferSize ?? 1_000,\n\t\tmaxRetries: effective?.tracking?.maxRetries ?? 3,\n\t\tretryBaseDelayMs: effective?.tracking?.retryBaseDelayMs ?? 200,\n\t\tretryMaxDelayMs: effective?.tracking?.retryMaxDelayMs ?? 2_000,\n\t\tshutdownTimeoutMs: effective?.tracking?.shutdownTimeoutMs ?? 2_000,\n\t};\n\n\tconst internalConfig = { apiUrl, apiKey, tracking: trackingConfig };\n\n\t// Compose client from modules\n\tconst trackingClient = createTrackingClient(internalConfig);\n\tconst kbClient = createKbClient(internalConfig);\n\n\treturn {\n\t\t...trackingClient,\n\t\tkb: kbClient,\n\t\t_config: internalConfig,\n\t};\n}\n"],"mappings":"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","names":["WaniWaniError","message","status","existsSync","readFileSync","resolve","CONFIG_FILENAME","_cached","loadProjectConfig","filePath","raw","GLOBAL_KEY","defineConfig","config","getGlobalConfig","EVENT_TYPES","pickFirst","meta","keys","key","value","SESSION_ID_KEYS","REQUEST_ID_KEYS","TRACE_ID_KEYS","EXTERNAL_USER_ID_KEYS","VISITOR_ID_KEYS","CORRELATION_ID_KEYS","extractSessionId","meta","pickFirst","SESSION_ID_KEYS","extractRequestId","REQUEST_ID_KEYS","extractTraceId","TRACE_ID_KEYS","extractExternalUserId","EXTERNAL_USER_ID_KEYS","extractVisitorId","VISITOR_ID_KEYS","extractCorrelationId","CORRELATION_ID_KEYS","SOURCE_SESSION_KEYS","CLIENT_INFO_NAME_SOURCES","extractSource","meta","clientInfo","explicit","key","source","value","name","lower","needle","DEFAULT_SOURCE","mapTrackEventToV2","input","options","now","generateId","createEventId","eventName","resolveEventName","meta","toRecord","metadata","correlation","resolveCorrelationIds","eventId","takeNonEmptyString","timestamp","normalizeTimestamp","source","extractSource","rawLegacy","isLegacyTrackEvent","mappedMetadata","mapProperties","legacyProperties","mapLegacyProperties","explicitProperties","properties","requestId","extractRequestId","sessionId","extractSessionId","traceId","extractTraceId","externalUserId","extractExternalUserId","visitorId","extractVisitorId","correlationId","extractCorrelationId","date","value","createRevenueApi","emit","amount","currency","itemId","label","context","options","id","input","externalId","email","name","occurredAt","DEFAULT_ENDPOINT_PATH","SDK_NAME","AUTH_FAILURE_STATUS","RETRYABLE_STATUS","createV2BatchTransport","options","BatchingV2Transport","joinUrl","DEFAULT_ENDPOINT_PATH","delayMs","resolve","event","dropCount","timeoutMs","flushPromise","timeoutSignal","teardownFlush","events","body","mid","headers","SDK_NAME","batch","attempt","pendingBatch","result","response","error","getErrorMessage","data","parseJsonResponse","partial","rejected","byId","retryable","permanent","rejectedEvent","isRetryableRejectedEvent","rawDelay","status","rejectedCount","buffered","code","apiUrl","endpointPath","normalizedBase","normalizedPath","createFrontendClient","options","apiUrl","endpointPath","splitEndpoint","transport","createV2BatchTransport","identifiedUserId","resolve","value","emit","event","identity","channelId","properties","mapped","mapTrackEventToV2","createRevenueApi","userId","shutdownOptions","endpoint","url","AsyncLocalStorage","globalWithStore","store","retrievalCollectorStore","recordKbSearch","trace","SDK_NAME","createKbClient","config","apiUrl","apiKey","requireApiKey","request","method","path","body","key","url","headers","init","response","text","WaniWaniError","files","query","options","results","recordKbSearch","r","createTrackingClient","config","apiUrl","apiKey","tracking","requireApiKey","transport","createV2BatchTransport","emit","event","mappedEvent","mapTrackEventToV2","trackOnce","track","createRevenueApi","client","userId","properties","meta","options","attachShutdownHooks","defaultTimeoutMs","shutdown","waniwani","config","effective","loadProjectConfig","getGlobalConfig","apiUrl","apiKey","trackingConfig","internalConfig","trackingClient","createTrackingClient","kbClient","createKbClient"]}
|
package/dist/legacy/index.d.ts
CHANGED
|
@@ -1539,6 +1539,15 @@ interface WelcomeConfig {
|
|
|
1539
1539
|
/** Suggestion cards shown in the welcome screen. Disappear after the first message. */
|
|
1540
1540
|
suggestions?: string[];
|
|
1541
1541
|
}
|
|
1542
|
+
/** Where a suggestion pill came from. */
|
|
1543
|
+
/**
|
|
1544
|
+
* Every place a suggestion pill can come from. The single source of truth:
|
|
1545
|
+
* {@link SuggestionOrigin} derives from it, and runtime validation reads it,
|
|
1546
|
+
* so adding an origin is a one-line change.
|
|
1547
|
+
*/
|
|
1548
|
+
declare const SUGGESTION_ORIGINS: readonly ["channel", "page", "flow", "followup"];
|
|
1549
|
+
/** Where a suggestion pill came from. */
|
|
1550
|
+
type SuggestionOrigin = (typeof SUGGESTION_ORIGINS)[number];
|
|
1542
1551
|
interface SuggestionsConfig {
|
|
1543
1552
|
/**
|
|
1544
1553
|
* Initial suggestions to show before the user sends their first message.
|
|
@@ -1546,8 +1555,14 @@ interface SuggestionsConfig {
|
|
|
1546
1555
|
*/
|
|
1547
1556
|
initial?: string[];
|
|
1548
1557
|
/**
|
|
1549
|
-
*
|
|
1550
|
-
*
|
|
1558
|
+
* Which providers may fill the pill row. Omitted, this defaults to
|
|
1559
|
+
* `["channel", "page", "followup"]`: starter prompts and generated
|
|
1560
|
+
* follow-ups render, flow-driven pills stay opt-in.
|
|
1561
|
+
*/
|
|
1562
|
+
origins?: SuggestionOrigin[];
|
|
1563
|
+
/**
|
|
1564
|
+
* @deprecated Use `origins`. `true` maps to every origin, `false` to none.
|
|
1565
|
+
* Will be removed in a future minor release.
|
|
1551
1566
|
*/
|
|
1552
1567
|
dynamic?: boolean;
|
|
1553
1568
|
}
|
|
@@ -1629,8 +1644,11 @@ interface ChatBaseProps {
|
|
|
1629
1644
|
/** Callback fired when a response is received */
|
|
1630
1645
|
onResponseReceived?: () => void;
|
|
1631
1646
|
/**
|
|
1632
|
-
*
|
|
1633
|
-
* `
|
|
1647
|
+
* Suggestion pill configuration. Unset: no suggestions. An object sets
|
|
1648
|
+
* starter prompts (`initial`) and which origins may fill the per-turn
|
|
1649
|
+
* pill row (`origins`); `origins: ["flow"]` (or any list including
|
|
1650
|
+
* `"flow"`) opts into flow-driven pills. `true` enables every origin with
|
|
1651
|
+
* defaults; `false` hides the pill row entirely.
|
|
1634
1652
|
*/
|
|
1635
1653
|
suggestions?: boolean | SuggestionsConfig;
|
|
1636
1654
|
/**
|