playingpack 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +4 -4
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import{program as w}from"commander";import Ye from"fastify";import Be from"@fastify/cors";import Ve from"@fastify/websocket";import{fastifyTRPCPlugin as Ze}from"@trpc/server/adapters/fastify";import Qe from"get-port";var v=class{sessions=new Map;settings;listeners=new Set;pendingResolvers=new Map;constructor(e){this.settings={pause:"off",...e}}createSession(e,n,o,r){let a={id:e,state:"LOOKUP",timestamp:new Date().toISOString(),method:n,path:o,body:r,toolCalls:[],responseContent:""};if(typeof r=="object"&&r!==null){let i=r;typeof i.model=="string"&&(a.model=i.model)}return this.sessions.set(e,a),this.emit({type:"request_update",session:a}),a}getSession(e){return this.sessions.get(e)}getAllSessions(){return Array.from(this.sessions.values())}updateState(e,n){let o=this.sessions.get(e);o&&(o.state=n,this.emit({type:"request_update",session:o}))}addToolCall(e,n){let o=this.sessions.get(e);o&&(o.toolCalls.push(n),this.emit({type:"request_update",session:o}))}updateContent(e,n){let o=this.sessions.get(e);o&&(o.responseContent+=n)}setRawResponse(e,n){let o=this.sessions.get(e);o&&(o.responseContent=n)}complete(e,n,o){let r=this.sessions.get(e);r&&(r.state="COMPLETE",r.statusCode=n,this.emit({type:"request_update",session:r}),this.emit({type:"request_complete",requestId:e,statusCode:n,cached:o}))}error(e,n){let o=this.sessions.get(e);o&&(o.state="ERROR",o.error=n,this.emit({type:"request_update",session:o}))}shouldIntercept(e){if(this.settings.pause==="off")return!1;let n=this.sessions.get(e);return n?this.settings.pause==="tool-calls"?n.toolCalls.length>0:!0:!1}async intercept(e){let n=this.sessions.get(e);if(!n)throw new Error(`Session ${e} not found`);if(this.updateState(e,"INTERCEPT"),n.toolCalls.length>0){let o=n.toolCalls[0];this.emit({type:"intercept",requestId:e,toolCall:{name:o?.name||"unknown",arguments:o?.arguments||"{}"},status:"paused"})}return new Promise(o=>{this.pendingResolvers.set(e,{resolve:r=>{let a=this.pendingResolvers.get(e);this.pendingResolvers.delete(e),o({action:r,mockContent:a?.mockContent})}})})}allowRequest(e){let n=this.pendingResolvers.get(e);return n?(this.updateState(e,"FLUSH"),n.resolve("allow"),!0):!1}mockRequest(e,n){let o=this.pendingResolvers.get(e);return o?(o.mockContent=n,this.updateState(e,"INJECT"),o.resolve("mock"),!0):!1}getSettings(){return{...this.settings}}updateSettings(e){this.settings={...this.settings,...e}}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}emit(e){for(let n of this.listeners)try{n(e)}catch{}}cleanup(){let e=Array.from(this.sessions.entries());if(e.length>100){let n=e.filter(([o,r])=>r.state==="COMPLETE"||r.state==="ERROR").slice(0,e.length-100);for(let[o]of n)this.sessions.delete(o)}}removeSession(e){this.sessions.delete(e),this.pendingResolvers.delete(e)}},S=null;function A(t){return S=new v(t),S}function d(){return S||(S=new v),S}function N(t){return{sessionManager:d(),req:t?.req,res:t?.res}}import{initTRPC as Se}from"@trpc/server";import{z as L}from"zod";import{z as s}from"zod";var dt=s.object({model:s.string(),messages:s.array(s.object({role:s.enum(["system","user","assistant","tool"]),content:s.union([s.string(),s.null(),s.array(s.any())]).optional(),name:s.string().optional(),tool_calls:s.array(s.any()).optional(),tool_call_id:s.string().optional()})),temperature:s.number().optional(),top_p:s.number().optional(),n:s.number().optional(),stream:s.boolean().optional(),stop:s.union([s.string(),s.array(s.string())]).optional(),max_tokens:s.number().optional(),presence_penalty:s.number().optional(),frequency_penalty:s.number().optional(),logit_bias:s.record(s.number()).optional(),user:s.string().optional(),tools:s.array(s.object({type:s.literal("function"),function:s.object({name:s.string(),description:s.string().optional(),parameters:s.any().optional()})})).optional(),tool_choice:s.union([s.literal("none"),s.literal("auto"),s.literal("required"),s.object({type:s.literal("function"),function:s.object({name:s.string()})})]).optional(),response_format:s.object({type:s.enum(["text","json_object"])}).optional(),seed:s.number().optional()}).passthrough(),U=s.object({requestId:s.string(),type:s.enum(["text","error","tool_result"]),content:s.string()}),F=s.object({requestId:s.string()}),fe=s.object({pause:s.enum(["off","tool-calls","all"])}),q=s.object({settings:fe.partial()}),me=s.object({c:s.string(),d:s.number()}),he=s.object({id:s.string(),hash:s.string(),timestamp:s.string(),model:s.string(),endpoint:s.string()}),ft=s.object({meta:he,request:s.object({body:s.unknown()}),response:s.object({status:s.number(),chunks:s.array(me)})}),ye=s.enum(["auto","off","replay-only"]),ke=s.enum(["off","tool-calls","all"]),P=s.object({upstream:s.string().url().optional(),tapesDir:s.string().optional(),logsDir:s.string().optional(),record:ye.optional(),headless:s.boolean().optional(),port:s.number().int().min(1).max(65535).optional(),host:s.string().optional(),pause:ke.optional()});var y=Se.context().create(),$=y.router({getSessions:y.procedure.query(({ctx:t})=>({sessions:t.sessionManager.getAllSessions()})),getSession:y.procedure.input(L.object({id:L.string()})).query(({ctx:t,input:e})=>({session:t.sessionManager.getSession(e.id)||null})),getSettings:y.procedure.query(({ctx:t})=>({settings:t.sessionManager.getSettings()})),updateSettings:y.procedure.input(q).mutation(({ctx:t,input:e})=>(t.sessionManager.updateSettings(e.settings),{settings:t.sessionManager.getSettings()})),allowRequest:y.procedure.input(F).mutation(({ctx:t,input:e})=>({success:t.sessionManager.allowRequest(e.requestId)})),mockRequest:y.procedure.input(U).mutation(({ctx:t,input:e})=>({success:t.sessionManager.mockRequest(e.requestId,e.content)})),health:y.procedure.query(()=>({status:"ok",timestamp:new Date().toISOString()}))});import{createParser as be}from"eventsource-parser";var T=class{callbacks;toolCalls=new Map;accumulatedContent="";parser;constructor(e={}){this.callbacks=e,this.parser=be(n=>{n.type==="event"&&this.handleEvent(n.data)})}feed(e){this.parser.feed(e)}getContent(){return this.accumulatedContent}getToolCalls(){return Array.from(this.toolCalls.values())}hasToolCalls(){return this.toolCalls.size>0}getAssembledMessage(){let e=this.getToolCalls();return e.length>0?{role:"assistant",content:null,tool_calls:e.map(n=>({id:n.id,type:"function",function:{name:n.name,arguments:n.arguments}}))}:{role:"assistant",content:this.accumulatedContent||null}}reset(){this.toolCalls.clear(),this.accumulatedContent="",this.parser.reset()}handleEvent(e){if(e==="[DONE]"){this.callbacks.onDone?.();return}try{let n=JSON.parse(e);for(let o of n.choices){let r=o.delta;if(r.content&&(this.accumulatedContent+=r.content,this.callbacks.onContent?.(r.content)),r.tool_calls)for(let a of r.tool_calls){let i=this.toolCalls.get(a.index);if(i)a.function?.arguments&&(i.arguments+=a.function.arguments,this.callbacks.onToolCallUpdate?.(a.index,a.function.arguments));else{let c={index:a.index,id:a.id||"",name:a.function?.name||"",arguments:a.function?.arguments||""};this.toolCalls.set(a.index,c),this.callbacks.onToolCall?.(c)}}}}catch(n){this.callbacks.onError?.(n instanceof Error?n:new Error(String(n)))}}};function E(t){return new T(t)}var we="https://api.openai.com";function J(t){return!t||t.length<8?"****":`${t.substring(0,4)}...${t.substring(t.length-4)}`}function ve(t){let e=new Headers,n=["authorization","content-type","accept","openai-organization","openai-project","user-agent"];for(let o of n){let r=t[o];if(r){let a=Array.isArray(r)?r[0]:r;a&&e.set(o,a)}}return e.set("accept","text/event-stream"),e}async function M(t){let n=`${t.upstreamUrl||we}${t.path}`,o=ve(t.headers),r=typeof t.body=="object"&&t.body!==null?{...t.body,stream:!0}:t.body,a=await fetch(n,{method:t.method,headers:o,body:JSON.stringify(r)});return{status:a.status,headers:a.headers,body:a.body,ok:a.ok}}async function*H(t){let e=t.getReader(),n=new TextDecoder;try{for(;;){let{done:r,value:a}=await e.read();if(r)break;yield n.decode(a,{stream:!0})}let o=n.decode();o&&(yield o)}finally{e.releaseLock()}}import{mkdir as Re,writeFile as xe}from"fs/promises";import{dirname as Pe,join as Te}from"path";import{createHash as Ce}from"crypto";function I(t){if(t==null)return null;if(Array.isArray(t))return t.map(I);if(typeof t=="object"){let e=t,n={},o=Object.keys(e).filter(r=>!["stream","request_id","timestamp"].includes(r)).sort();for(let r of o)n[r]=I(e[r]);return n}return t}function b(t){let e=I(t),n=JSON.stringify(e);return Ce("sha256").update(n).digest("hex")}var Ee=".playingpack/tapes",C=class{tapesDir;chunks=[];lastChunkTime=0;requestBody;model="unknown";endpoint;hash="";constructor(e=Ee){this.tapesDir=e,this.endpoint="/v1/chat/completions"}start(e){if(this.requestBody=e,this.chunks=[],this.lastChunkTime=Date.now(),this.hash=b(e),typeof e=="object"&&e!==null){let n=e;typeof n.model=="string"&&(this.model=n.model)}}recordChunk(e){let n=Date.now(),o=this.chunks.length===0?0:n-this.lastChunkTime;this.chunks.push({c:e,d:o}),this.lastChunkTime=n}async save(e=200){let n={meta:{id:crypto.randomUUID(),hash:this.hash,timestamp:new Date().toISOString(),model:this.model,endpoint:this.endpoint},request:{body:this.requestBody},response:{status:e,chunks:this.chunks}},o=this.getTapePath();return await Re(Pe(o),{recursive:!0}),await xe(o,JSON.stringify(n,null,2),"utf-8"),o}getTapePath(){return Te(this.tapesDir,`${this.hash}.json`)}getHash(){return this.hash}getChunkCount(){return this.chunks.length}};import{readFile as Me,access as Ie}from"fs/promises";import{join as W}from"path";var G=".playingpack/tapes";async function z(t,e=G){let n=b(t),o=W(e,`${n}.json`);try{return await Ie(o),!0}catch{return!1}}async function _e(t,e=G){let n=b(t),o=W(e,`${n}.json`);try{let r=await Me(o,"utf-8");return JSON.parse(r)}catch{return null}}var _=class{tape;currentIndex=0;aborted=!1;constructor(e){this.tape=e}getMeta(){return this.tape.meta}getStatus(){return this.tape.response.status}abort(){this.aborted=!0}async*replay(){this.currentIndex=0,this.aborted=!1;for(let e of this.tape.response.chunks){if(this.aborted||(e.d>0&&await this.delay(e.d),this.aborted))break;yield e.c,this.currentIndex++}}async*replayFast(){for(let e of this.tape.response.chunks){if(this.aborted)break;yield e.c}}delay(e){return new Promise(n=>setTimeout(n,e))}};async function K(t,e){let n=await _e(t,e);return n?new _(n):null}function V(){return`chatcmpl-mock-${Date.now()}`}function Z(t,e=4){let n=[];for(let o=0;o<t.length;o+=e)n.push(t.slice(o,o+e));return n}function m(t){return`data: ${t}
2
+ import{program as v}from"commander";import et from"fastify";import tt from"@fastify/cors";import nt from"@fastify/websocket";import{fastifyTRPCPlugin as ot}from"@trpc/server/adapters/fastify";import st from"get-port";var C=class{sessions=new Map;settings;listeners=new Set;pendingResolvers=new Map;preInterceptResolvers=new Map;constructor(e){this.settings={pause:"off",...e}}createSession(e,t,o,s){let r={id:e,state:"LOOKUP",timestamp:new Date().toISOString(),method:t,path:o,body:s,toolCalls:[],responseContent:""};if(typeof s=="object"&&s!==null){let i=s;typeof i.model=="string"&&(r.model=i.model)}return this.sessions.set(e,r),this.emit({type:"request_update",session:r}),r}getSession(e){return this.sessions.get(e)}getAllSessions(){return Array.from(this.sessions.values())}updateState(e,t){let o=this.sessions.get(e);o&&(o.state=t,this.emit({type:"request_update",session:o}))}addToolCall(e,t){let o=this.sessions.get(e);o&&(o.toolCalls.push(t),this.emit({type:"request_update",session:o}))}updateContent(e,t){let o=this.sessions.get(e);o&&(o.responseContent+=t)}setRawResponse(e,t){let o=this.sessions.get(e);o&&(o.responseContent=t)}complete(e,t,o){let s=this.sessions.get(e);s&&(s.state="COMPLETE",s.statusCode=t,this.emit({type:"request_update",session:s}),this.emit({type:"request_complete",requestId:e,statusCode:t,cached:o}))}error(e,t){let o=this.sessions.get(e);o&&(o.state="ERROR",o.error=t,this.emit({type:"request_update",session:o}))}shouldIntercept(e){if(this.settings.pause==="off")return!1;let t=this.sessions.get(e);return t?this.settings.pause==="tool-calls"?t.toolCalls.length>0:!0:!1}async intercept(e){let t=this.sessions.get(e);if(!t)throw new Error(`Session ${e} not found`);if(this.updateState(e,"INTERCEPT"),t.toolCalls.length>0){let o=t.toolCalls[0];this.emit({type:"intercept",requestId:e,toolCall:{name:o?.name||"unknown",arguments:o?.arguments||"{}"},status:"paused"})}return new Promise(o=>{this.pendingResolvers.set(e,{resolve:s=>{let r=this.pendingResolvers.get(e);this.pendingResolvers.delete(e),o({action:s,mockContent:r?.mockContent})}})})}allowRequest(e){let t=this.pendingResolvers.get(e);return t?(this.updateState(e,"FLUSH"),t.resolve("allow"),!0):!1}mockRequest(e,t){let o=this.pendingResolvers.get(e);return o?(o.mockContent=t,this.updateState(e,"INJECT"),o.resolve("mock"),!0):!1}shouldPreIntercept(e){return this.settings.pause!=="off"}async preIntercept(e,t){let o=this.sessions.get(e);if(!o)throw new Error(`Session ${e} not found`);this.updateState(e,"PRE_INTERCEPT");let s=o.body,r=Array.isArray(s?.messages)?s.messages:[];return this.emit({type:"pre_intercept",requestId:e,request:{model:o.model||"unknown",messages:r},hasCachedResponse:t,status:"paused"}),new Promise(i=>{this.preInterceptResolvers.set(e,{resolve:i})})}preInterceptAllow(e){let t=this.preInterceptResolvers.get(e);return t?(this.preInterceptResolvers.delete(e),t.resolve({action:"allow"}),!0):!1}preInterceptEdit(e,t){let o=this.preInterceptResolvers.get(e);if(o){this.preInterceptResolvers.delete(e);let s=this.sessions.get(e);return s&&(s.body=t,this.emit({type:"request_update",session:s})),o.resolve({action:"edit",editedBody:t}),!0}return!1}preInterceptUseCache(e){let t=this.preInterceptResolvers.get(e);return t?(this.preInterceptResolvers.delete(e),t.resolve({action:"use_cache"}),!0):!1}preInterceptMock(e,t){let o=this.preInterceptResolvers.get(e);return o?(this.preInterceptResolvers.delete(e),this.updateState(e,"INJECT"),o.resolve({action:"mock",mockContent:t}),!0):!1}getSettings(){return{...this.settings}}updateSettings(e){this.settings={...this.settings,...e}}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}emit(e){for(let t of this.listeners)try{t(e)}catch{}}cleanup(){let e=Array.from(this.sessions.entries());if(e.length>100){let t=e.filter(([o,s])=>s.state==="COMPLETE"||s.state==="ERROR").slice(0,e.length-100);for(let[o]of t)this.sessions.delete(o)}}removeSession(e){this.sessions.delete(e),this.pendingResolvers.delete(e),this.preInterceptResolvers.delete(e)}},b=null;function q(n){return b=new C(n),b}function h(){return b||(b=new C),b}function F(n){return{sessionManager:h(),req:n?.req,res:n?.res}}import{initTRPC as xe}from"@trpc/server";import{z as B}from"zod";import{z as a}from"zod";var bt=a.object({model:a.string(),messages:a.array(a.object({role:a.enum(["system","user","assistant","tool"]),content:a.union([a.string(),a.null(),a.array(a.any())]).optional(),name:a.string().optional(),tool_calls:a.array(a.any()).optional(),tool_call_id:a.string().optional()})),temperature:a.number().optional(),top_p:a.number().optional(),n:a.number().optional(),stream:a.boolean().optional(),stop:a.union([a.string(),a.array(a.string())]).optional(),max_tokens:a.number().optional(),presence_penalty:a.number().optional(),frequency_penalty:a.number().optional(),logit_bias:a.record(a.number()).optional(),user:a.string().optional(),tools:a.array(a.object({type:a.literal("function"),function:a.object({name:a.string(),description:a.string().optional(),parameters:a.any().optional()})})).optional(),tool_choice:a.union([a.literal("none"),a.literal("auto"),a.literal("required"),a.object({type:a.literal("function"),function:a.object({name:a.string()})})]).optional(),response_format:a.object({type:a.enum(["text","json_object"])}).optional(),seed:a.number().optional()}).passthrough(),$=a.object({requestId:a.string(),type:a.enum(["text","error","tool_result"]),content:a.string()}),L=a.object({requestId:a.string()}),J=a.object({requestId:a.string()}),H=a.object({requestId:a.string(),editedBody:a.record(a.unknown())}),W=a.object({requestId:a.string()}),G=a.object({requestId:a.string(),mockContent:a.string()}),we=a.object({pause:a.enum(["off","tool-calls","all"])}),z=a.object({settings:we.partial()}),ve=a.object({c:a.string(),d:a.number()}),Ce=a.object({id:a.string(),hash:a.string(),timestamp:a.string(),model:a.string(),endpoint:a.string()}),St=a.object({meta:Ce,request:a.object({body:a.unknown()}),response:a.object({status:a.number(),chunks:a.array(ve)})}),Re=a.enum(["auto","off","replay-only"]),Pe=a.enum(["off","tool-calls","all"]),x=a.object({upstream:a.string().url().optional(),tapesDir:a.string().optional(),logsDir:a.string().optional(),record:Re.optional(),headless:a.boolean().optional(),port:a.number().int().min(1).max(65535).optional(),host:a.string().optional(),pause:Pe.optional()});var m=xe.context().create(),K=m.router({getSessions:m.procedure.query(({ctx:n})=>({sessions:n.sessionManager.getAllSessions()})),getSession:m.procedure.input(B.object({id:B.string()})).query(({ctx:n,input:e})=>({session:n.sessionManager.getSession(e.id)||null})),getSettings:m.procedure.query(({ctx:n})=>({settings:n.sessionManager.getSettings()})),updateSettings:m.procedure.input(z).mutation(({ctx:n,input:e})=>(n.sessionManager.updateSettings(e.settings),{settings:n.sessionManager.getSettings()})),allowRequest:m.procedure.input(L).mutation(({ctx:n,input:e})=>({success:n.sessionManager.allowRequest(e.requestId)})),mockRequest:m.procedure.input($).mutation(({ctx:n,input:e})=>({success:n.sessionManager.mockRequest(e.requestId,e.content)})),preInterceptAllow:m.procedure.input(J).mutation(({ctx:n,input:e})=>({success:n.sessionManager.preInterceptAllow(e.requestId)})),preInterceptEdit:m.procedure.input(H).mutation(({ctx:n,input:e})=>({success:n.sessionManager.preInterceptEdit(e.requestId,e.editedBody)})),preInterceptUseCache:m.procedure.input(W).mutation(({ctx:n,input:e})=>({success:n.sessionManager.preInterceptUseCache(e.requestId)})),preInterceptMock:m.procedure.input(G).mutation(({ctx:n,input:e})=>({success:n.sessionManager.preInterceptMock(e.requestId,e.mockContent)})),health:m.procedure.query(()=>({status:"ok",timestamp:new Date().toISOString()}))});import{createParser as Ie}from"eventsource-parser";var I=class{callbacks;toolCalls=new Map;accumulatedContent="";parser;constructor(e={}){this.callbacks=e,this.parser=Ie(t=>{t.type==="event"&&this.handleEvent(t.data)})}feed(e){this.parser.feed(e)}getContent(){return this.accumulatedContent}getToolCalls(){return Array.from(this.toolCalls.values())}hasToolCalls(){return this.toolCalls.size>0}getAssembledMessage(){let e=this.getToolCalls();return e.length>0?{role:"assistant",content:null,tool_calls:e.map(t=>({id:t.id,type:"function",function:{name:t.name,arguments:t.arguments}}))}:{role:"assistant",content:this.accumulatedContent||null}}reset(){this.toolCalls.clear(),this.accumulatedContent="",this.parser.reset()}handleEvent(e){if(e==="[DONE]"){this.callbacks.onDone?.();return}try{let t=JSON.parse(e);for(let o of t.choices){let s=o.delta;if(s.content&&(this.accumulatedContent+=s.content,this.callbacks.onContent?.(s.content)),s.tool_calls)for(let r of s.tool_calls){let i=this.toolCalls.get(r.index);if(i)r.function?.arguments&&(i.arguments+=r.function.arguments,this.callbacks.onToolCallUpdate?.(r.index,r.function.arguments));else{let c={index:r.index,id:r.id||"",name:r.function?.name||"",arguments:r.function?.arguments||""};this.toolCalls.set(r.index,c),this.callbacks.onToolCall?.(c)}}}}catch(t){this.callbacks.onError?.(t instanceof Error?t:new Error(String(t)))}}};function T(n){return new I(n)}var Te="https://api.openai.com";function Y(n){return!n||n.length<8?"****":`${n.substring(0,4)}...${n.substring(n.length-4)}`}function Ee(n,e=!0){let t=new Headers,o=["authorization","content-type","accept","openai-organization","openai-project","user-agent"];for(let s of o){let r=n[s];if(r){let i=Array.isArray(r)?r[0]:r;i&&t.set(s,i)}}return e?t.set("accept","text/event-stream"):t.set("accept","application/json"),t}async function E(n){let t=`${n.upstreamUrl||Te}${n.path}`,o=typeof n.body=="object"&&n.body!==null&&"stream"in n.body&&typeof n.body.stream=="boolean"?n.body.stream:!0,s=Ee(n.headers,o),r=await fetch(t,{method:n.method,headers:s,body:JSON.stringify(n.body)});return{status:r.status,headers:r.headers,body:r.body,ok:r.ok}}async function*V(n){let e=n.getReader(),t=new TextDecoder;try{for(;;){let{done:s,value:r}=await e.read();if(s)break;yield t.decode(r,{stream:!0})}let o=t.decode();o&&(yield o)}finally{e.releaseLock()}}import{mkdir as Me,writeFile as Ae}from"fs/promises";import{dirname as je,join as Oe}from"path";import{createHash as _e}from"crypto";function _(n){if(n==null)return null;if(Array.isArray(n))return n.map(_);if(typeof n=="object"){let e=n,t={},o=Object.keys(e).filter(s=>!["stream","request_id","timestamp"].includes(s)).sort();for(let s of o)t[s]=_(e[s]);return t}return n}function S(n){let e=_(n),t=JSON.stringify(e);return _e("sha256").update(t).digest("hex")}var Ne=".playingpack/tapes",w=class{tapesDir;chunks=[];lastChunkTime=0;requestBody;model="unknown";endpoint;hash="";constructor(e=Ne){this.tapesDir=e,this.endpoint="/v1/chat/completions"}start(e){if(this.requestBody=e,this.chunks=[],this.lastChunkTime=Date.now(),this.hash=S(e),typeof e=="object"&&e!==null){let t=e;typeof t.model=="string"&&(this.model=t.model)}}recordChunk(e){let t=Date.now(),o=this.chunks.length===0?0:t-this.lastChunkTime;this.chunks.push({c:e,d:o}),this.lastChunkTime=t}async save(e=200){let t={meta:{id:crypto.randomUUID(),hash:this.hash,timestamp:new Date().toISOString(),model:this.model,endpoint:this.endpoint},request:{body:this.requestBody},response:{status:e,chunks:this.chunks}},o=this.getTapePath();return await Me(je(o),{recursive:!0}),await Ae(o,JSON.stringify(t,null,2),"utf-8"),o}getTapePath(){return Oe(this.tapesDir,`${this.hash}.json`)}getHash(){return this.hash}getChunkCount(){return this.chunks.length}};import{readFile as De,access as Ue}from"fs/promises";import{join as Z}from"path";var Q=".playingpack/tapes";async function X(n,e=Q){let t=S(n),o=Z(e,`${t}.json`);try{return await Ue(o),!0}catch{return!1}}async function qe(n,e=Q){let t=S(n),o=Z(e,`${t}.json`);try{let s=await De(o,"utf-8");return JSON.parse(s)}catch{return null}}var M=class{tape;currentIndex=0;aborted=!1;constructor(e){this.tape=e}getMeta(){return this.tape.meta}getStatus(){return this.tape.response.status}abort(){this.aborted=!0}async*replay(){this.currentIndex=0,this.aborted=!1;for(let e of this.tape.response.chunks){if(this.aborted||(e.d>0&&await this.delay(e.d),this.aborted))break;yield e.c,this.currentIndex++}}async*replayFast(){for(let e of this.tape.response.chunks){if(this.aborted)break;yield e.c}}delay(e){return new Promise(t=>setTimeout(t,e))}};async function ee(n,e){let t=await qe(n,e);return t?new M(t):null}function A(){return`chatcmpl-mock-${Date.now()}`}function oe(n,e=4){let t=[];for(let o=0;o<n.length;o+=e)t.push(n.slice(o,o+e));return t}function k(n){return`data: ${n}
3
3
 
4
- `}function Y(t,e,n,o=null){let r={id:t,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:e,choices:[{index:0,delta:n!==null?{content:n}:{},finish_reason:o}]};return JSON.stringify(r)}function B(t,e,n,o,r,a=!1,i=null){let c={index:0};a?(c.id=n,c.type="function",c.function={name:o,arguments:r}):c.function={arguments:r};let g={id:t,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:e,choices:[{index:0,delta:{tool_calls:[c]},finish_reason:i}]};return JSON.stringify(g)}async function*Q(t,e={}){let{model:n="gpt-4",delayMs:o=20}=e,r=V(),a=Z(t),i={id:r,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:n,choices:[{index:0,delta:{role:"assistant",content:""},finish_reason:null}]};yield m(JSON.stringify(i));for(let c of a)o>0&&await ne(o),yield m(Y(r,n,c));yield m(Y(r,n,null,"stop")),yield m("[DONE]")}async function*X(t,e,n={}){let{model:o="gpt-4",delayMs:r=10}=n,a=V(),i=`call_mock_${Date.now()}`,c=Z(e,10),g={id:a,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:o,choices:[{index:0,delta:{role:"assistant",content:null},finish_reason:null}]};yield m(JSON.stringify(g)),yield m(B(a,o,i,t,c[0]||"",!0));for(let u=1;u<c.length;u++)r>0&&await ne(r),yield m(B(a,o,i,t,c[u]||""));let p={id:a,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:o,choices:[{index:0,delta:{},finish_reason:"tool_calls"}]};yield m(JSON.stringify(p)),yield m("[DONE]")}function ee(t,e="invalid_request_error",n=null){return JSON.stringify({error:{message:t,type:e,param:null,code:n}})}function te(t){let e=t.trim();if(e.startsWith("ERROR:"))return{type:"error",content:e.slice(6).trim()};try{let n=JSON.parse(e);if(n&&typeof n=="object"&&"function"in n)return{type:"tool_call",functionName:n.function,content:JSON.stringify(n.arguments||{})}}catch{}return{type:"text",content:e}}function ne(t){return new Promise(e=>setTimeout(e,t))}import{appendFile as je,mkdir as De}from"fs/promises";import{join as Oe}from"path";var j=null,D=null;async function oe(t){j=t,await De(j,{recursive:!0});let e=new Date().toISOString().split("T")[0];D=Oe(j,`server-${e}.log`)}function Ae(t){let e=`[${t.timestamp}] [${t.level.toUpperCase()}] ${t.message}`;return t.data!==void 0?`${e} ${JSON.stringify(t.data)}
4
+ `}function te(n,e,t,o=null){let s={id:n,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:e,choices:[{index:0,delta:t!==null?{content:t}:{},finish_reason:o}]};return JSON.stringify(s)}function ne(n,e,t,o,s,r=!1,i=null){let c={index:0};r?(c.id=t,c.type="function",c.function={name:o,arguments:s}):c.function={arguments:s};let p={id:n,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:e,choices:[{index:0,delta:{tool_calls:[c]},finish_reason:i}]};return JSON.stringify(p)}async function*se(n,e={}){let{model:t="gpt-4",delayMs:o=20}=e,s=A(),r=oe(n),i={id:s,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:t,choices:[{index:0,delta:{role:"assistant",content:""},finish_reason:null}]};yield k(JSON.stringify(i));for(let c of r)o>0&&await le(o),yield k(te(s,t,c));yield k(te(s,t,null,"stop")),yield k("[DONE]")}async function*re(n,e,t={}){let{model:o="gpt-4",delayMs:s=10}=t,r=A(),i=`call_mock_${Date.now()}`,c=oe(e,10),p={id:r,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:o,choices:[{index:0,delta:{role:"assistant",content:null},finish_reason:null}]};yield k(JSON.stringify(p)),yield k(ne(r,o,i,n,c[0]||"",!0));for(let d=1;d<c.length;d++)s>0&&await le(s),yield k(ne(r,o,i,n,c[d]||""));let l={id:r,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:o,choices:[{index:0,delta:{},finish_reason:"tool_calls"}]};yield k(JSON.stringify(l)),yield k("[DONE]")}function ae(n,e="invalid_request_error",t=null){return JSON.stringify({error:{message:n,type:e,param:null,code:t}})}function ie(n,e,t="gpt-4"){let o=A(),s=Math.floor(Date.now()/1e3),r={role:"assistant",content:e?null:n};e&&e.length>0&&(r.tool_calls=e);let i=e&&e.length>0?"tool_calls":"stop";return{id:o,object:"chat.completion",created:s,model:t,choices:[{index:0,message:r,finish_reason:i}],usage:{prompt_tokens:0,completion_tokens:0,total_tokens:0}}}function ce(n){let e=n.trim();if(e.startsWith("ERROR:"))return{type:"error",content:e.slice(6).trim()};try{let t=JSON.parse(e);if(t&&typeof t=="object"&&"function"in t)return{type:"tool_call",functionName:t.function,content:JSON.stringify(t.arguments||{})}}catch{}return{type:"text",content:e}}function le(n){return new Promise(e=>setTimeout(e,n))}import{appendFile as Fe,mkdir as $e}from"fs/promises";import{join as Le}from"path";var j=null,O=null;async function pe(n){j=n,await $e(j,{recursive:!0});let e=new Date().toISOString().split("T")[0];O=Le(j,`server-${e}.log`)}function Je(n){let e=`[${n.timestamp}] [${n.level.toUpperCase()}] ${n.message}`;return n.data!==void 0?`${e} ${JSON.stringify(n.data)}
5
5
  `:`${e}
6
- `}async function R(t,e,n){if(!D)return;let o={timestamp:new Date().toISOString(),level:t,message:e,data:n};try{await je(D,Ae(o))}catch{}}var f={info:(t,e)=>R("info",t,e),warn:(t,e)=>R("warn",t,e),error:(t,e)=>R("error",t,e),debug:(t,e)=>R("debug",t,e)};var h={upstream:"https://api.openai.com",tapesDir:".playingpack/tapes",record:"auto"};function re(t,e){e&&(h=e),t.post("/v1/chat/completions",async(n,o)=>{await Ne(n,o)}),t.get("/health",async(n,o)=>o.send({status:"ok"})),t.all("/v1/*",async(n,o)=>{await Le(n,o)})}async function Ne(t,e){let n=d(),o=crypto.randomUUID(),r=t.body,a=t.headers.authorization||"";console.log(`[${o.slice(0,8)}] POST /v1/chat/completions`),console.log(` Model: ${r.model||"unknown"}`),console.log(` Auth: ${J(a.replace("Bearer ",""))}`),f.info("Request received",{requestId:o,path:"/v1/chat/completions",model:r.model}),n.createSession(o,"POST","/v1/chat/completions",r),n.updateState(o,"LOOKUP");let i=h.record!=="off",c=h.record==="auto",g=h.record==="replay-only";if(i&&await z(r,h.tapesDir)){console.log(" [CACHE HIT] Replaying from tape"),f.info("Cache hit",{requestId:o,model:r.model}),await Fe(t,e,o,r);return}if(g){console.log(" [REPLAY-ONLY] No tape found, rejecting request"),n.error(o,"No tape found (replay-only mode)"),e.code(404).send({error:{message:"No recorded tape found for this request (replay-only mode)",type:"tape_not_found"}});return}console.log(" [CACHE MISS] Forwarding to upstream"),f.info("Cache miss",{requestId:o,model:r.model}),n.updateState(o,"CONNECT");try{let u=await M({method:"POST",path:"/v1/chat/completions",headers:t.headers,body:r,upstreamUrl:h.upstream});if(!u.ok||!u.body){n.error(o,`Upstream error: ${u.status}`),e.code(u.status).header("content-type","application/json").send(await se(u.body));return}await Ue(t,e,o,u,r,c)}catch(u){let k=u instanceof Error?u.message:"Unknown error";console.error(` [ERROR] ${k}`),f.error("Request failed",{requestId:o,error:k}),n.error(o,k),e.code(500).send({error:{message:k,type:"proxy_error"}})}}async function Ue(t,e,n,o,r,a){let i=d(),c=a?new C(h.tapesDir):null;c?.start(r),i.updateState(n,"STREAMING");let g=E({onToolCall:l=>{i.addToolCall(n,l),console.log(` [TOOL CALL] ${l.name}`)},onContent:l=>{i.updateContent(n,l)}});if(!o.body){e.raw.end();return}let p=[],u=H(o.body);for await(let l of u)g.feed(l),c?.recordChunk(l),p.push(l);let k=g.getAssembledMessage();if(i.setRawResponse(n,JSON.stringify(k,null,2)),c)try{let l=await c.save(o.status);console.log(` [TAPE] Saved to ${l}`)}catch(l){console.error(" [TAPE ERROR] Failed to save tape:",l)}if(i.shouldIntercept(n)){console.log(" [INTERCEPT] Pausing for user action");let l=await i.intercept(n);if(console.log(` [ACTION] ${l.action}`),l.action==="mock"){await qe(e,n,l.mockContent||"");return}}e.code(o.status).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive");for(let l of p)e.raw.write(l);i.complete(n,o.status,!1),f.info("Request completed",{requestId:n,status:o.status,cached:!1}),e.raw.end()}async function Fe(t,e,n,o){let r=d();r.updateState(n,"REPLAY");let a=await K(o,h.tapesDir);if(!a){r.error(n,"Failed to load tape"),e.code(500).send({error:{message:"Tape not found",type:"proxy_error"}});return}e.code(a.getStatus()).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive").header("x-playingpack-cached","true");let i=E({onToolCall:p=>{r.addToolCall(n,p),console.log(` [TOOL CALL] ${p.name}`)},onContent:p=>{r.updateContent(n,p)}}),c=[];for await(let p of a.replay())i.feed(p),c.push(p),e.raw.write(p);let g=i.getAssembledMessage();r.setRawResponse(n,JSON.stringify(g,null,2)),r.complete(n,a.getStatus(),!0),f.info("Request completed",{requestId:n,status:a.getStatus(),cached:!0}),e.raw.end()}async function qe(t,e,n){let o=d(),r=te(n);if(r.type==="error"){t.code(400).header("content-type","application/json").send(ee(r.content)),o.complete(e,400,!1);return}t.code(200).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive").header("x-playingpack-mocked","true");let a=r.type==="tool_call"?X(r.functionName||"mock_function",r.content):Q(r.content);for await(let i of a)t.raw.write(i);o.complete(e,200,!1),t.raw.end()}async function Le(t,e){try{let n=await M({method:t.method,path:t.url,headers:t.headers,body:t.body,upstreamUrl:h.upstream});if(e.code(n.status),n.headers.forEach((o,r)=>{["content-encoding","transfer-encoding"].includes(r.toLowerCase())||e.header(r,o)}),n.body){let o=await se(n.body);e.send(o)}else e.send()}catch(n){let o=n instanceof Error?n.message:"Unknown error";e.code(500).send({error:{message:o,type:"proxy_error"}})}}async function se(t){if(!t)return"";let e=t.getReader(),n=new TextDecoder,o="";for(;;){let{done:r,value:a}=await e.read();if(r)break;o+=n.decode(a,{stream:!0})}return o}import{join as ae,dirname as $e}from"path";import{fileURLToPath as Je}from"url";import{access as He}from"fs/promises";import We from"@fastify/static";var Ge=$e(Je(import.meta.url));function ie(){return ae(Ge,"../public")}async function ze(){try{return await He(ae(ie(),"index.html")),!0}catch{return!1}}async function ce(t){let e=ie();return await ze()?(await t.register(We,{root:e,prefix:"/",wildcard:!1}),t.setNotFoundHandler((o,r)=>o.url.startsWith("/v1")||o.url.startsWith("/api")||o.url.startsWith("/ws")?r.code(404).send({error:"Not Found"}):r.sendFile("index.html")),console.log(" UI available at root path"),!0):(console.log(" UI not available (development mode)"),console.log(" Run the UI separately: cd packages/web && pnpm dev"),!1)}var x=new Set;function le(t){x.add(t);let e=d(),n=e.getAllSessions();for(let r of n)O(t,{type:"request_update",session:r});let o=e.subscribe(r=>{O(t,r)});t.on("message",r=>{try{let a=JSON.parse(r.toString());Ke(t,a)}catch{}}),t.on("close",()=>{x.delete(t),o()}),t.on("error",()=>{x.delete(t),o()})}function Ke(t,e){if(typeof e!="object"||e===null)return;let n=e,o=d();switch(n.type){case"allow":typeof n.requestId=="string"&&o.allowRequest(n.requestId);break;case"mock":typeof n.requestId=="string"&&typeof n.content=="string"&&o.mockRequest(n.requestId,n.content);break;case"ping":O(t,{type:"pong"});break}}function O(t,e){if(t.readyState===t.OPEN)try{t.send(JSON.stringify(e))}catch{x.delete(t)}}async function ue(t){let e=t.port,n=t.host;A({pause:t.pause}),await oe(t.logsDir),await f.info("Server starting",{upstream:t.upstream,tapesDir:t.tapesDir,record:t.record,headless:t.headless});let o=await Qe({port:e});o!==e&&console.log(` Port ${e} in use, using ${o}`);let r=Ye({logger:!1,bodyLimit:50*1024*1024});return await r.register(Be,{origin:!0,credentials:!0}),await r.register(Ve),r.get("/ws",{websocket:!0},a=>{le(a)}),await r.register(Ze,{prefix:"/api/trpc",trpcOptions:{router:$,createContext:N}}),re(r,{upstream:t.upstream,tapesDir:t.tapesDir,record:t.record}),t.headless?console.log(" Running in headless mode (no UI)"):await ce(r),await r.listen({port:o,host:n}),await f.info("Server listening",{port:o,host:n}),{server:r,port:o,host:n}}import{readFile as Xe}from"fs/promises";import{existsSync as et}from"fs";import{join as pe}from"path";import{createJiti as tt}from"jiti";var nt=["playingpack.config.ts","playingpack.config.mts","playingpack.config.js","playingpack.config.mjs"],ot=["playingpack.config.jsonc","playingpack.config.json",".playingpackrc.json",".playingpackrc"],rt={upstream:"https://api.openai.com",tapesDir:".playingpack/tapes",logsDir:".playingpack/logs",record:"auto",headless:!1,port:4747,host:"0.0.0.0",pause:"off"};function st(t){let e=t.replace(/\/\*[\s\S]*?\*\//g,"");return e=e.replace(/(?<!["'])\/\/.*$/gm,""),e}async function at(t){for(let e of nt){let n=pe(t,e);if(et(n))try{let r=await tt(import.meta.url,{interopDefault:!0}).import(n),a=r&&typeof r=="object"&&"default"in r?r.default:r;if(!a||typeof a!="object"){console.warn(` Warning: ${e} must export a config object`);continue}return{config:P.parse(a),filename:e}}catch(o){console.warn(` Warning: Error loading ${e}:`,o.message)}}return null}async function it(t){for(let e of ot){let n=pe(t,e);try{let o=await Xe(n,"utf-8"),r=JSON.parse(st(o));return{config:P.parse(r),filename:e}}catch(o){o.code!=="ENOENT"&&(o instanceof SyntaxError||o.name==="ZodError")&&console.warn(` Warning: Invalid config in ${e}:`,o.message)}}return null}async function ct(t){let e=await at(t);if(e)return console.log(` Config loaded from ${e.filename}`),e.config;let n=await it(t);return n?(console.log(` Config loaded from ${n.filename}`),n.config):{}}async function ge(t={}){let e=process.cwd(),n=await ct(e),o={...rt,...n};return t.port!==void 0&&(o.port=t.port),t.host!==void 0&&(o.host=t.host),t.ui!==void 0&&(o.headless=!t.ui),t.upstream!==void 0&&(o.upstream=t.upstream),t.tapesDir!==void 0&&(o.tapesDir=t.tapesDir),t.record!==void 0&&(o.record=t.record),o}var de="1.0.0";w.name("playingpack").description("Chrome DevTools for AI Agents - Local reverse proxy and debugger").version(de);w.command("start").description("Start the PlayingPack proxy server").option("-p, --port <port>","Port to listen on").option("-h, --host <host>","Host to bind to").option("--no-ui","Run without UI server (headless mode for CI/CD)").option("--upstream <url>","Upstream API URL (default: https://api.openai.com)").option("--tapes-dir <path>","Directory for tape storage (default: .playingpack/tapes)").option("--record <mode>","Recording mode: auto, off, replay-only (default: auto)").action(async t=>{console.log(),console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),console.log(" \u2551 \u2551"),console.log(" \u2551 \u2593\u2593\u2593\u2593 PlayingPack - The Flight Simulator \u2593\u2593\u2593\u2593 \u2551"),console.log(" \u2551 Chrome DevTools for AI Agents \u2551"),console.log(" \u2551 \u2551"),console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),console.log();try{let e=await ge({port:t.port?parseInt(t.port,10):void 0,host:t.host,ui:t.ui,upstream:t.upstream,tapesDir:t.tapesDir,record:t.record}),{port:n,host:o}=await ue(e),r=`http://localhost:${n}`,a=o==="0.0.0.0"?`http://<your-ip>:${n}`:`http://${o}:${n}`;console.log(),console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),console.log(" \u2502 Server running! \u2502"),console.log(" \u2502 \u2502"),console.log(` \u2502 Local: ${r.padEnd(44)}\u2502`),console.log(` \u2502 Network: ${a.padEnd(44)}\u2502`),console.log(" \u2502 \u2502"),console.log(" \u2502 To use with your AI agent, set: \u2502"),console.log(` \u2502 baseURL = "${r}/v1"`.padEnd(60)+"\u2502"),e.headless||(console.log(" \u2502 \u2502"),console.log(" \u2502 Dashboard: Open the local URL in your browser \u2502")),console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"),console.log(),console.log(" Waiting for requests..."),console.log();let i=async()=>{console.log(`
7
- Shutting down...`),process.exit(0)};process.on("SIGINT",i),process.on("SIGTERM",i)}catch(e){console.error("Failed to start server:",e),process.exit(1)}});w.command("version").description("Show version").action(()=>{console.log(`playingpack v${de}`)});process.argv.length===2?w.parse(["node","playingpack","start"]):w.parse();
6
+ `}async function R(n,e,t){if(!O)return;let o={timestamp:new Date().toISOString(),level:n,message:e,data:t};try{await Fe(O,Je(o))}catch{}}var f={info:(n,e)=>R("info",n,e),warn:(n,e)=>R("warn",n,e),error:(n,e)=>R("error",n,e),debug:(n,e)=>R("debug",n,e)};var y={upstream:"https://api.openai.com",tapesDir:".playingpack/tapes",record:"auto"};function ge(n,e){e&&(y=e),n.post("/v1/chat/completions",async(t,o)=>{await He(t,o)}),n.get("/health",async(t,o)=>o.send({status:"ok"})),n.all("/v1/*",async(t,o)=>{await ze(t,o)})}async function He(n,e){let t=h(),o=crypto.randomUUID(),s=n.body,r=s.stream!==!1,i=n.headers.authorization||"";console.log(`[${o.slice(0,8)}] POST /v1/chat/completions`),console.log(` Model: ${s.model||"unknown"}`),console.log(` Stream: ${r}`),console.log(` Auth: ${Y(i.replace("Bearer ",""))}`),f.info("Request received",{requestId:o,path:"/v1/chat/completions",model:s.model,stream:r}),t.createSession(o,"POST","/v1/chat/completions",s),t.updateState(o,"LOOKUP");let c=y.record!=="off",p=y.record==="auto",l=y.record==="replay-only",d=c&&await X(s,y.tapesDir);if(t.shouldPreIntercept(o)){console.log(` [PRE-INTERCEPT] Pausing before LLM call (cache ${d?"available":"not available"})`),f.info("Pre-intercept",{requestId:o,model:s.model,hasTape:d});let u=await t.preIntercept(o,d);switch(console.log(` [PRE-ACTION] ${u.action}`),u.action){case"use_cache":if(d){console.log(" [CACHE] Using cached response"),await ue(n,e,o,s,r);return}console.log(" [WARN] Cache requested but no tape available, proceeding to upstream");break;case"mock":console.log(" [MOCK] Sending mock response"),await N(e,o,u.mockContent||"",r);return;case"edit":u.editedBody&&(Object.assign(s,u.editedBody),console.log(" [EDIT] Request body modified"));break;case"allow":break}}else if(d){console.log(" [CACHE HIT] Replaying from tape"),f.info("Cache hit",{requestId:o,model:s.model}),await ue(n,e,o,s,r);return}if(l&&!d){console.log(" [REPLAY-ONLY] No tape found, rejecting request"),t.error(o,"No tape found (replay-only mode)"),e.code(404).send({error:{message:"No recorded tape found for this request (replay-only mode)",type:"tape_not_found"}});return}console.log(" [CACHE MISS] Forwarding to upstream"),f.info("Cache miss",{requestId:o,model:s.model}),t.updateState(o,"CONNECT");try{let u=await E({method:"POST",path:"/v1/chat/completions",headers:n.headers,body:s,upstreamUrl:y.upstream});if(!u.ok||!u.body){t.error(o,`Upstream error: ${u.status}`),e.code(u.status).header("content-type","application/json").send(await D(u.body));return}r?await We(n,e,o,u,s,p):await Ge(n,e,o,u,s,p)}catch(u){let g=u instanceof Error?u.message:"Unknown error";console.error(` [ERROR] ${g}`),f.error("Request failed",{requestId:o,error:g}),t.error(o,g),e.code(500).send({error:{message:g,type:"proxy_error"}})}}async function We(n,e,t,o,s,r){let i=h(),c=r?new w(y.tapesDir):null;c?.start(s),i.updateState(t,"STREAMING");let p=T({onToolCall:g=>{i.addToolCall(t,g),console.log(` [TOOL CALL] ${g.name}`)},onContent:g=>{i.updateContent(t,g)}});if(!o.body){e.raw.end();return}let l=[],d=V(o.body);for await(let g of d)p.feed(g),c?.recordChunk(g),l.push(g);let u=p.getAssembledMessage();if(i.setRawResponse(t,JSON.stringify(u,null,2)),c)try{let g=await c.save(o.status);console.log(` [TAPE] Saved to ${g}`)}catch(g){console.error(" [TAPE ERROR] Failed to save tape:",g)}if(i.shouldIntercept(t)){console.log(" [INTERCEPT] Pausing for user action");let g=await i.intercept(t);if(console.log(` [ACTION] ${g.action}`),g.action==="mock"){await N(e,t,g.mockContent||"",!0);return}}e.code(o.status).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive");for(let g of l)e.raw.write(g);i.complete(t,o.status,!1),f.info("Request completed",{requestId:t,status:o.status,cached:!1}),e.raw.end()}async function Ge(n,e,t,o,s,r){let i=h(),c=r?new w(y.tapesDir):null;if(c?.start(s),i.updateState(t,"STREAMING"),!o.body){e.code(o.status).header("content-type","application/json").send("");return}let p=await D(o.body);c?.recordChunk(p);try{let l=JSON.parse(p);if(l.choices?.[0]?.message){let d=l.choices[0].message;if(d.content&&i.updateContent(t,d.content),d.tool_calls)for(let u of d.tool_calls)i.addToolCall(t,{index:0,id:u.id,name:u.function.name,arguments:u.function.arguments}),console.log(` [TOOL CALL] ${u.function.name}`)}i.setRawResponse(t,JSON.stringify(l,null,2))}catch{i.setRawResponse(t,p)}if(c)try{let l=await c.save(o.status);console.log(` [TAPE] Saved to ${l}`)}catch(l){console.error(" [TAPE ERROR] Failed to save tape:",l)}if(i.shouldIntercept(t)){console.log(" [INTERCEPT] Pausing for user action");let l=await i.intercept(t);if(console.log(` [ACTION] ${l.action}`),l.action==="mock"){await N(e,t,l.mockContent||"",!1);return}}e.code(o.status).header("content-type","application/json").send(p),i.complete(t,o.status,!1),f.info("Request completed",{requestId:t,status:o.status,cached:!1})}async function ue(n,e,t,o,s){let r=h();r.updateState(t,"REPLAY");let i=await ee(o,y.tapesDir);if(!i){r.error(t,"Failed to load tape"),e.code(500).send({error:{message:"Tape not found",type:"proxy_error"}});return}if(s){e.code(i.getStatus()).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive").header("x-playingpack-cached","true");let c=T({onToolCall:l=>{r.addToolCall(t,l),console.log(` [TOOL CALL] ${l.name}`)},onContent:l=>{r.updateContent(t,l)}});for await(let l of i.replay())c.feed(l),e.raw.write(l);let p=c.getAssembledMessage();r.setRawResponse(t,JSON.stringify(p,null,2)),r.complete(t,i.getStatus(),!0),f.info("Request completed",{requestId:t,status:i.getStatus(),cached:!0}),e.raw.end()}else{let c="";for await(let p of i.replay())c+=p;try{let p=JSON.parse(c);if(p.choices?.[0]?.message){let l=p.choices[0].message;if(l.content&&r.updateContent(t,l.content),l.tool_calls)for(let d of l.tool_calls)r.addToolCall(t,{index:0,id:d.id,name:d.function.name,arguments:d.function.arguments}),console.log(` [TOOL CALL] ${d.function.name}`)}r.setRawResponse(t,JSON.stringify(p,null,2))}catch{r.setRawResponse(t,c)}e.code(i.getStatus()).header("content-type","application/json").header("x-playingpack-cached","true").send(c),r.complete(t,i.getStatus(),!0),f.info("Request completed",{requestId:t,status:i.getStatus(),cached:!0})}}async function N(n,e,t,o=!0){let s=h(),r=ce(t);if(r.type==="error"){n.code(400).header("content-type","application/json").send(ae(r.content)),s.complete(e,400,!1);return}if(o){n.code(200).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive").header("x-playingpack-mocked","true");let i=r.type==="tool_call"?re(r.functionName||"mock_function",r.content):se(r.content);for await(let c of i)n.raw.write(c);s.complete(e,200,!1),n.raw.end()}else{let i=r.type==="tool_call"?[{id:`call_mock_${Date.now()}`,type:"function",function:{name:r.functionName||"mock_function",arguments:r.content}}]:void 0,c=r.type==="text"?r.content:null,p=ie(c,i);n.code(200).header("content-type","application/json").header("x-playingpack-mocked","true").send(JSON.stringify(p)),s.complete(e,200,!1)}}async function ze(n,e){try{let t=await E({method:n.method,path:n.url,headers:n.headers,body:n.body,upstreamUrl:y.upstream});if(e.code(t.status),t.headers.forEach((o,s)=>{["content-encoding","transfer-encoding"].includes(s.toLowerCase())||e.header(s,o)}),t.body){let o=await D(t.body);e.send(o)}else e.send()}catch(t){let o=t instanceof Error?t.message:"Unknown error";e.code(500).send({error:{message:o,type:"proxy_error"}})}}async function D(n){if(!n)return"";let e=n.getReader(),t=new TextDecoder,o="";for(;;){let{done:s,value:r}=await e.read();if(s)break;o+=t.decode(r,{stream:!0})}return o}import{join as de,dirname as Be}from"path";import{fileURLToPath as Ke}from"url";import{access as Ye}from"fs/promises";import Ve from"@fastify/static";var Ze=Be(Ke(import.meta.url));function fe(){return de(Ze,"../public")}async function Qe(){try{return await Ye(de(fe(),"index.html")),!0}catch{return!1}}async function me(n){let e=fe();return await Qe()?(await n.register(Ve,{root:e,prefix:"/",wildcard:!1}),n.setNotFoundHandler((o,s)=>o.url.startsWith("/v1")||o.url.startsWith("/api")||o.url.startsWith("/ws")?s.code(404).send({error:"Not Found"}):s.sendFile("index.html")),console.log(" UI available at root path"),!0):(console.log(" UI not available (development mode)"),console.log(" Run the UI separately: cd packages/web && pnpm dev"),!1)}var P=new Set;function he(n){P.add(n);let e=h(),t=e.getAllSessions();for(let s of t)U(n,{type:"request_update",session:s});let o=e.subscribe(s=>{U(n,s)});n.on("message",s=>{try{let r=JSON.parse(s.toString());Xe(n,r)}catch{}}),n.on("close",()=>{P.delete(n),o()}),n.on("error",()=>{P.delete(n),o()})}function Xe(n,e){if(typeof e!="object"||e===null)return;let t=e,o=h();switch(t.type){case"allow":typeof t.requestId=="string"&&o.allowRequest(t.requestId);break;case"mock":typeof t.requestId=="string"&&typeof t.content=="string"&&o.mockRequest(t.requestId,t.content);break;case"pre_allow":typeof t.requestId=="string"&&o.preInterceptAllow(t.requestId);break;case"pre_edit":typeof t.requestId=="string"&&typeof t.editedBody=="object"&&o.preInterceptEdit(t.requestId,t.editedBody);break;case"pre_use_cache":typeof t.requestId=="string"&&o.preInterceptUseCache(t.requestId);break;case"pre_mock":typeof t.requestId=="string"&&typeof t.mockContent=="string"&&o.preInterceptMock(t.requestId,t.mockContent);break;case"ping":U(n,{type:"pong"});break}}function U(n,e){if(n.readyState===n.OPEN)try{n.send(JSON.stringify(e))}catch{P.delete(n)}}async function ye(n){let e=n.port,t=n.host;q({pause:n.pause}),await pe(n.logsDir),await f.info("Server starting",{upstream:n.upstream,tapesDir:n.tapesDir,record:n.record,headless:n.headless});let o=await st({port:e});o!==e&&console.log(` Port ${e} in use, using ${o}`);let s=et({logger:!1,bodyLimit:50*1024*1024});return await s.register(tt,{origin:!0,credentials:!0}),await s.register(nt),s.get("/ws",{websocket:!0},r=>{he(r)}),await s.register(ot,{prefix:"/api/trpc",trpcOptions:{router:K,createContext:F}}),ge(s,{upstream:n.upstream,tapesDir:n.tapesDir,record:n.record}),n.headless?console.log(" Running in headless mode (no UI)"):await me(s),await s.listen({port:o,host:t}),await f.info("Server listening",{port:o,host:t}),{server:s,port:o,host:t}}import{readFile as rt}from"fs/promises";import{existsSync as at}from"fs";import{join as ke}from"path";import{createJiti as it}from"jiti";var ct=["playingpack.config.ts","playingpack.config.mts","playingpack.config.js","playingpack.config.mjs"],lt=["playingpack.config.jsonc","playingpack.config.json",".playingpackrc.json",".playingpackrc"],pt={upstream:"https://api.openai.com",tapesDir:".playingpack/tapes",logsDir:".playingpack/logs",record:"auto",headless:!1,port:4747,host:"0.0.0.0",pause:"off"};function ut(n){let e=n.replace(/\/\*[\s\S]*?\*\//g,"");return e=e.replace(/(?<!["'])\/\/.*$/gm,""),e}async function gt(n){for(let e of ct){let t=ke(n,e);if(at(t))try{let s=await it(import.meta.url,{interopDefault:!0}).import(t),r=s&&typeof s=="object"&&"default"in s?s.default:s;if(!r||typeof r!="object"){console.warn(` Warning: ${e} must export a config object`);continue}return{config:x.parse(r),filename:e}}catch(o){console.warn(` Warning: Error loading ${e}:`,o.message)}}return null}async function dt(n){for(let e of lt){let t=ke(n,e);try{let o=await rt(t,"utf-8"),s=JSON.parse(ut(o));return{config:x.parse(s),filename:e}}catch(o){o.code!=="ENOENT"&&(o instanceof SyntaxError||o.name==="ZodError")&&console.warn(` Warning: Invalid config in ${e}:`,o.message)}}return null}async function ft(n){let e=await gt(n);if(e)return console.log(` Config loaded from ${e.filename}`),e.config;let t=await dt(n);return t?(console.log(` Config loaded from ${t.filename}`),t.config):{}}async function be(n={}){let e=process.cwd(),t=await ft(e),o={...pt,...t};return n.port!==void 0&&(o.port=n.port),n.host!==void 0&&(o.host=n.host),n.ui!==void 0&&(o.headless=!n.ui),n.upstream!==void 0&&(o.upstream=n.upstream),n.tapesDir!==void 0&&(o.tapesDir=n.tapesDir),n.record!==void 0&&(o.record=n.record),o}var Se="1.0.0";v.name("playingpack").description("Chrome DevTools for AI Agents - Local reverse proxy and debugger").version(Se);v.command("start").description("Start the PlayingPack proxy server").option("-p, --port <port>","Port to listen on").option("-h, --host <host>","Host to bind to").option("--no-ui","Run without UI server (headless mode for CI/CD)").option("--upstream <url>","Upstream API URL (default: https://api.openai.com)").option("--tapes-dir <path>","Directory for tape storage (default: .playingpack/tapes)").option("--record <mode>","Recording mode: auto, off, replay-only (default: auto)").action(async n=>{console.log(),console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),console.log(" \u2551 \u2551"),console.log(" \u2551 \u2593\u2593\u2593\u2593 PlayingPack - The Flight Simulator \u2593\u2593\u2593\u2593 \u2551"),console.log(" \u2551 Chrome DevTools for AI Agents \u2551"),console.log(" \u2551 \u2551"),console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),console.log();try{let e=await be({port:n.port?parseInt(n.port,10):void 0,host:n.host,ui:n.ui,upstream:n.upstream,tapesDir:n.tapesDir,record:n.record}),{port:t,host:o}=await ye(e),s=`http://localhost:${t}`,r=o==="0.0.0.0"?`http://<your-ip>:${t}`:`http://${o}:${t}`;console.log(),console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),console.log(" \u2502 Server running! \u2502"),console.log(" \u2502 \u2502"),console.log(` \u2502 Local: ${s.padEnd(44)}\u2502`),console.log(` \u2502 Network: ${r.padEnd(44)}\u2502`),console.log(" \u2502 \u2502"),console.log(" \u2502 To use with your AI agent, set: \u2502"),console.log(` \u2502 baseURL = "${s}/v1"`.padEnd(60)+"\u2502"),e.headless||(console.log(" \u2502 \u2502"),console.log(" \u2502 Dashboard: Open the local URL in your browser \u2502")),console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"),console.log(),console.log(" Waiting for requests..."),console.log();let i=async()=>{console.log(`
7
+ Shutting down...`),process.exit(0)};process.on("SIGINT",i),process.on("SIGTERM",i)}catch(e){console.error("Failed to start server:",e),process.exit(1)}});v.command("version").description("Show version").action(()=>{console.log(`playingpack v${Se}`)});process.argv.length===2?v.parse(["node","playingpack","start"]):v.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playingpack",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Chrome DevTools for AI Agents - Local reverse proxy and debugger for LLM API calls",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "https://github.com/playingpack/playingpack.git"
25
+ "url": "git+https://github.com/playingpack/playingpack.git"
26
26
  },
27
27
  "homepage": "https://github.com/playingpack/playingpack#readme",
28
28
  "bugs": {