playingpack 0.6.0 → 0.7.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.
package/README.md CHANGED
@@ -170,7 +170,7 @@ Your tests become:
170
170
 
171
171
  Enable intervene mode to pause requests and inspect what your agent is doing:
172
172
 
173
- 1. Start PlayingPack with `--intervene` flag or enable in the dashboard
173
+ 1. Start PlayingPack (intervention mode is enabled by default)
174
174
  2. Run your agent
175
175
  3. At **Point 1** (before LLM call), choose:
176
176
  - **Allow** — Send the original request to the LLM
@@ -186,7 +186,7 @@ Run your test suite against cached responses in CI:
186
186
 
187
187
  ```bash
188
188
  # In your CI pipeline
189
- npx playingpack start --no-ui --cache read &
189
+ npx playingpack start --no-ui --no-intervene --cache read &
190
190
  sleep 2 # Wait for server
191
191
  npm test
192
192
  ```
@@ -197,7 +197,7 @@ If a cached response is missing, the request fails immediately — no surprise A
197
197
  # Example GitHub Actions step
198
198
  - name: Run tests with PlayingPack
199
199
  run: |
200
- npx playingpack start --no-ui --cache read &
200
+ npx playingpack start --no-ui --no-intervene --cache read &
201
201
  sleep 2
202
202
  npm test
203
203
  ```
@@ -315,7 +315,7 @@ npx playingpack start [options]
315
315
  | `--upstream <url>` | Upstream API URL | `https://api.openai.com` |
316
316
  | `--cache-path <path>` | Directory for cache storage | `.playingpack/cache` |
317
317
  | `--cache <mode>` | Cache mode (`off`, `read`, `read-write`) | `read-write` |
318
- | `--intervene` | Enable human intervention mode | `true` |
318
+ | `--no-intervene` | Disable human intervention mode | `false` |
319
319
 
320
320
  ### Examples
321
321
 
@@ -323,14 +323,14 @@ npx playingpack start [options]
323
323
  # Proxy to a local LLM (Ollama)
324
324
  npx playingpack start --upstream http://localhost:11434/v1
325
325
 
326
- # CI mode: read-only cache, no UI, fail if cache missing
327
- npx playingpack start --no-ui --cache read
326
+ # CI mode: read-only cache, no UI, no intervention
327
+ npx playingpack start --no-ui --no-intervene --cache read
328
328
 
329
329
  # Custom port and cache directory
330
330
  npx playingpack start --port 8080 --cache-path ./test/fixtures/cache
331
331
 
332
- # Enable intervention mode for debugging
333
- npx playingpack start --intervene
332
+ # Disable intervention mode for CI/CD
333
+ npx playingpack start --no-intervene
334
334
  ```
335
335
 
336
336
  ---
package/dist/index.js CHANGED
@@ -3,5 +3,5 @@ import{program as P}from"commander";import vt from"open";import rt from"fastify"
3
3
 
4
4
  `}function ne(t,e,o,n=null){let s={id:t,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:e,choices:[{index:0,delta:o!==null?{content:o}:{},finish_reason:n}]};return JSON.stringify(s)}function oe(t,e,o,n,s,a=!1,i=null){let l={index:0};a?(l.id=o,l.type="function",l.function={name:n,arguments:s}):l.function={arguments:s};let d={id:t,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:e,choices:[{index:0,delta:{tool_calls:[l]},finish_reason:i}]};return JSON.stringify(d)}async function*re(t,e={}){let{model:o="gpt-4",delayMs:n=20}=e,s=$(),a=se(t),i={id:s,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:o,choices:[{index:0,delta:{role:"assistant",content:""},finish_reason:null}]};yield y(JSON.stringify(i));for(let l of a)n>0&&await ue(n),yield y(ne(s,o,l));yield y(ne(s,o,null,"stop")),yield y("[DONE]")}async function*ae(t,e,o={}){let{model:n="gpt-4",delayMs:s=10}=o,a=$(),i=`call_mock_${Date.now()}`,l=se(e,10),d={id:a,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:n,choices:[{index:0,delta:{role:"assistant",content:null},finish_reason:null}]};yield y(JSON.stringify(d)),yield y(oe(a,n,i,t,l[0]||"",!0));for(let g=1;g<l.length;g++)s>0&&await ue(s),yield y(oe(a,n,i,t,l[g]||""));let u={id:a,object:"chat.completion.chunk",created:Math.floor(Date.now()/1e3),model:n,choices:[{index:0,delta:{},finish_reason:"tool_calls"}]};yield y(JSON.stringify(u)),yield y("[DONE]")}function ie(t,e="invalid_request_error",o=null){return JSON.stringify({error:{message:t,type:e,param:null,code:o}})}function ce(t,e,o="gpt-4"){let n=$(),s=Math.floor(Date.now()/1e3),a={role:"assistant",content:e?null:t};e&&e.length>0&&(a.tool_calls=e);let i=e&&e.length>0?"tool_calls":"stop";return{id:n,object:"chat.completion",created:s,model:o,choices:[{index:0,message:a,finish_reason:i}],usage:{prompt_tokens:0,completion_tokens:0,total_tokens:0}}}function le(t){let e=t.trim();if(e.startsWith("ERROR:"))return{type:"error",content:e.slice(6).trim()};try{let o=JSON.parse(e);if(o&&typeof o=="object"&&"function"in o)return{type:"tool_call",functionName:o.function,content:JSON.stringify(o.arguments||{})}}catch{}return{type:"text",content:e}}function ue(t){return new Promise(e=>setTimeout(e,t))}import{appendFile as We,mkdir as He}from"fs/promises";import{join as Ge}from"path";var L=null,D=null;async function pe(t){L=t,await He(L,{recursive:!0});let e=new Date().toISOString().split("T")[0];D=Ge(L,`server-${e}.log`)}function ze(t){let e=`[${t.timestamp}] [${t.level.toUpperCase()}] ${t.message}`;return t.data!==void 0?`${e} ${JSON.stringify(t.data)}
5
5
  `:`${e}
6
- `}async function _(t,e,o){if(!D)return;let n={timestamp:new Date().toISOString(),level:t,message:e,data:o};try{await We(D,ze(n))}catch{}}var k={info:(t,e)=>_("info",t,e),warn:(t,e)=>_("warn",t,e),error:(t,e)=>_("error",t,e),debug:(t,e)=>_("debug",t,e)};var m={upstream:"https://api.openai.com",cachePath:".playingpack/cache",cache:"read-write"};function de(t,e){e&&(m={...m,...e}),t.post("/v1/chat/completions",async(o,n)=>{await Be(o,n)}),t.get("/health",async(o,n)=>n.send({status:"ok"})),t.all("/v1/*",async(o,n)=>{await Qe(o,n)})}async function Be(t,e){let o=f(),n=crypto.randomUUID(),s=t.body,a=s.stream!==!1,i=t.headers.authorization||"";console.log(`[${n.slice(0,8)}] POST /v1/chat/completions`),console.log(` Model: ${s.model||"unknown"}`),console.log(` Stream: ${a}`),console.log(` Auth: ${X(i.replace("Bearer ",""))}`),k.info("Request received",{requestId:n,path:"/v1/chat/completions",model:s.model,stream:a}),o.createSession(n,s);let l=m.cache!=="off",d=m.cache==="read-write",u=m.cache==="read",g=l&&await ee(s,m.cachePath);o.setCacheAvailable(n,g);let h=g?"cache":"llm",w;if(o.shouldIntervene()){console.log(` [POINT 1] Waiting for human action (cache: ${g?"yes":"no"})`);let p=await o.waitForPoint1(n);switch(p.action){case"cache":h="cache",console.log(" [ACTION] Use cache");break;case"llm":h="llm",console.log(" [ACTION] Call LLM");break;case"mock":h="mock",w=p.content,console.log(" [ACTION] Mock response");break}}else g?(console.log(" [AUTO] Using cached response"),h="cache"):(console.log(" [AUTO] Calling LLM"),h="llm");try{let p;switch(h){case"cache":{if(!g)throw new Error("Cache requested but no cached response available");p={...await Ke(t,e,n,s,a),cached:!0};break}case"llm":if(u){console.log(" [ERROR] Cache-only mode but no cache available"),o.error(n,"No cached response (cache: read mode)"),e.code(404).send({error:{message:"No cached response found (cache mode: read)",type:"cache_not_found"}});return}p=await Ve(t,e,n,s,a,d);break;case"mock":p=await ge(e,n,w||"",a);break}if(o.shouldIntervene()&&p){console.log(" [POINT 2] Waiting for human action");let c=await o.waitForPoint2(n);switch(c.action){case"return":console.log(" [ACTION] Return as-is");break;case"modify":console.log(" [ACTION] Modify response"),c.content&&(p=await ge(e,n,c.content,a));break}}p&&Ze(e,p,a),o.setResponseSource(n,h),o.complete(n),k.info("Request completed",{requestId:n,source:h})}catch(p){let c=p instanceof Error?p.message:"Unknown error";console.error(` [ERROR] ${c}`),k.error("Request failed",{requestId:n,error:c}),o.error(n,c),e.sent||e.code(500).send({error:{message:c,type:"proxy_error"}})}}async function Ke(t,e,o,n,s){let a=f(),i=await te(n,m.cachePath);if(!i)throw new Error("Failed to load cached response");let l=U({onToolCall:u=>{a.addToolCall(o,u),console.log(` [TOOL CALL] ${u.name}`)},onContent:u=>{a.appendContent(o,u)}}),d="";for await(let u of i.replay())l.feed(u),d+=u;return{content:d,status:i.getStatus(),cached:!0}}async function Ve(t,e,o,n,s,a){let i=f(),d={...n,stream_options:{include_usage:!0}},u=await F({method:"POST",path:"/v1/chat/completions",headers:t.headers,body:d,upstreamUrl:m.upstream});if(!u.ok||!u.body)return{content:u.body?await he(u.body):"",status:u.status};let g=a?new x(m.cachePath):null;g?.start(n);let h=U({onToolCall:c=>{i.addToolCall(o,c),console.log(` [TOOL CALL] ${c.name}`)},onContent:c=>{i.appendContent(o,c)},onFinishReason:c=>{i.setFinishReason(o,c)},onUsage:c=>{i.setUsage(o,c),console.log(` [USAGE] ${c.prompt_tokens} prompt, ${c.completion_tokens} completion`)}}),w="",p=Y(u.body);for await(let c of p)h.feed(c),g?.recordChunk(c),w+=c;if(g)try{let c=await g.save(u.status);console.log(` [CACHE] Saved to ${c}`)}catch(c){console.error(" [CACHE ERROR] Failed to save:",c)}return{content:w,status:u.status}}async function ge(t,e,o,n=!0){let s=le(o);if(s.type==="error")return{content:ie(s.content),status:400,mocked:!0};if(n){let a=s.type==="tool_call"?ae(s.functionName||"mock_function",s.content):re(s.content),i="";for await(let l of a)i+=l;return{content:i,status:200,mocked:!0}}else{let a=s.type==="tool_call"?[{id:`call_mock_${Date.now()}`,type:"function",function:{name:s.functionName||"mock_function",arguments:s.content}}]:void 0,i=s.type==="text"?s.content:null,l=ce(i,a);return{content:JSON.stringify(l),status:200,mocked:!0}}}function Ze(t,e,o){let n={};if(e.cached&&(n["x-playingpack-cached"]="true"),e.mocked&&(n["x-playingpack-mocked"]="true"),o&&e.content.includes("data: ")){t.code(e.status).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive");for(let[s,a]of Object.entries(n))t.header(s,a);t.raw.write(e.content),t.raw.end()}else{t.code(e.status).header("content-type","application/json");for(let[s,a]of Object.entries(n))t.header(s,a);t.send(e.content)}}async function Qe(t,e){try{let o=await F({method:t.method,path:t.url,headers:t.headers,body:t.body,upstreamUrl:m.upstream});if(e.code(o.status),o.headers.forEach((n,s)=>{["content-encoding","transfer-encoding"].includes(s.toLowerCase())||e.header(s,n)}),o.body){let n=await he(o.body);e.send(n)}else e.send()}catch(o){let n=o instanceof Error?o.message:"Unknown error";e.code(500).send({error:{message:n,type:"proxy_error"}})}}async function he(t){if(!t)return"";let e=t.getReader(),o=new TextDecoder,n="";for(;;){let{done:s,value:a}=await e.read();if(s)break;n+=o.decode(a,{stream:!0})}return n}import{join as me,dirname as Xe}from"path";import{fileURLToPath as Ye}from"url";import{access as et}from"fs/promises";import tt from"@fastify/static";var nt=Xe(Ye(import.meta.url));function fe(){return me(nt,"../public")}async function ot(){try{return await et(me(fe(),"index.html")),!0}catch{return!1}}async function ye(t){let e=fe();return await ot()?(await t.register(tt,{root:e,prefix:"/",wildcard:!1}),t.setNotFoundHandler((n,s)=>n.url.startsWith("/v1")||n.url.startsWith("/api")||n.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 E=new Set;function be(t){E.add(t);let e=f(),o=e.getAllSessions();for(let s of o)J(t,{type:"request_update",session:s});let n=e.subscribe(s=>{J(t,s)});t.on("message",s=>{try{let a=JSON.parse(s.toString());st(t,a)}catch{}}),t.on("close",()=>{E.delete(t),n()}),t.on("error",()=>{E.delete(t),n()})}function st(t,e){let o=B.safeParse(e);if(!o.success){if(typeof e=="object"&&e!==null&&e.type==="ping"){J(t,{type:"request_update",session:null});return}return}let n=o.data,s=f();switch(n.type){case"point1_action":s.resolvePoint1(n.requestId,n.action);break;case"point2_action":s.resolvePoint2(n.requestId,n.action);break}}function J(t,e){if(t.readyState===t.OPEN)try{t.send(JSON.stringify(e))}catch{E.delete(t)}}async function ke(t){let e=t.port,o=t.host;W({cache:t.cache,intervene:t.intervene}),await pe(t.logPath),await k.info("Server starting",{upstream:t.upstream,cachePath:t.cachePath,cache:t.cache,intervene:t.intervene,headless:t.headless});let n=await lt({port:e});n!==e&&console.log(` Port ${e} in use, using ${n}`);let s=rt({logger:!1,bodyLimit:50*1024*1024});return await s.register(at,{origin:!0,credentials:!0}),await s.register(it),s.get("/ws",{websocket:!0},a=>{be(a)}),await s.register(ct,{prefix:"/api/trpc",trpcOptions:{router:Q,createContext:H}}),de(s,{upstream:t.upstream,cachePath:t.cachePath,cache:t.cache}),t.headless||await ye(s),await s.listen({port:n,host:o}),await k.info("Server listening",{port:n,host:o}),{server:s,port:n,host:o}}import{readFile as ut}from"fs/promises";import{existsSync as pt}from"fs";import{join as ve}from"path";import{createJiti as gt}from"jiti";var dt=["playingpack.config.ts","playingpack.config.mts","playingpack.config.js","playingpack.config.mjs"],ht=["playingpack.config.jsonc","playingpack.config.json",".playingpackrc.json",".playingpackrc"],mt={cache:"read-write",intervene:!0,upstream:"https://api.openai.com",port:4747,host:"0.0.0.0",cachePath:".playingpack/cache",logPath:".playingpack/logs",headless:!1};function ft(t){let e=t.replace(/\/\*[\s\S]*?\*\//g,"");return e=e.replace(/(?<!["'])\/\/.*$/gm,""),e}async function yt(t){for(let e of dt){let o=ve(t,e);if(pt(o))try{let s=await gt(import.meta.url,{interopDefault:!0}).import(o),a=s&&typeof s=="object"&&"default"in s?s.default:s;if(!a||typeof a!="object"){console.warn(` Warning: ${e} must export a config object`);continue}return{config:T.parse(a),filename:e}}catch(n){console.warn(` Warning: Error loading ${e}:`,n.message)}}return null}async function bt(t){for(let e of ht){let o=ve(t,e);try{let n=await ut(o,"utf-8"),s=JSON.parse(ft(n));return{config:T.parse(s),filename:e}}catch(n){n.code!=="ENOENT"&&(n instanceof SyntaxError||n.name==="ZodError")&&console.warn(` Warning: Invalid config in ${e}:`,n.message)}}return null}async function kt(t){let e=await yt(t);if(e)return console.log(` Config loaded from ${e.filename}`),e.config;let o=await bt(t);return o?(console.log(` Config loaded from ${o.filename}`),o.config):{}}async function Se(t={}){let e=process.cwd(),o=await kt(e),n={...mt,...o};return t.port!==void 0&&(n.port=t.port),t.host!==void 0&&(n.host=t.host),t.ui!==void 0&&(n.headless=!t.ui),t.upstream!==void 0&&(n.upstream=t.upstream),t.cachePath!==void 0&&(n.cachePath=t.cachePath),t.cache!==void 0&&(n.cache=t.cache),t.intervene!==void 0&&(n.intervene=t.intervene),n}var we="1.0.0";P.name("playingpack").description("Chrome DevTools for AI Agents - Debug and test your AI agent LLM calls").version(we);P.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 (headless mode for CI/CD)").option("--upstream <url>","Upstream API URL (default: https://api.openai.com)").option("--cache-path <path>","Directory for cached responses (default: .playingpack/cache)").option("--cache <mode>","Cache mode: off, read, read-write (default: read-write)").option("--intervene","Enable intervention mode (pause for human actions)").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 PlayingPack - Debug your 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 Se({port:t.port?parseInt(t.port,10):void 0,host:t.host,ui:t.ui,upstream:t.upstream,cachePath:t.cachePath,cache:t.cache,intervene:t.intervene}),{port:o,host:n}=await ke(e),s=`http://localhost:${o}`,a=n==="0.0.0.0"?`http://<your-ip>:${o}`:`http://${n}:${o}`;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: ${a.padEnd(44)}\u2502`),console.log(" \u2502 \u2502"),console.log(" \u2502 Settings: \u2502"),console.log(` \u2502 Cache: ${e.cache.padEnd(41)}\u2502`),console.log(` \u2502 Intervene: ${String(e.intervene).padEnd(41)}\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"),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(),e.headless?console.log(" Running in headless mode (no UI)"):(console.log(" Opening dashboard in browser..."),await vt(s)),console.log(),console.log(" Waiting for requests..."),console.log();let i=async()=>{console.log(`
6
+ `}async function _(t,e,o){if(!D)return;let n={timestamp:new Date().toISOString(),level:t,message:e,data:o};try{await We(D,ze(n))}catch{}}var k={info:(t,e)=>_("info",t,e),warn:(t,e)=>_("warn",t,e),error:(t,e)=>_("error",t,e),debug:(t,e)=>_("debug",t,e)};var m={upstream:"https://api.openai.com",cachePath:".playingpack/cache",cache:"read-write"};function de(t,e){e&&(m={...m,...e}),t.post("/v1/chat/completions",async(o,n)=>{await Be(o,n)}),t.get("/health",async(o,n)=>n.send({status:"ok"})),t.all("/v1/*",async(o,n)=>{await Qe(o,n)})}async function Be(t,e){let o=f(),n=crypto.randomUUID(),s=t.body,a=s.stream!==!1,i=t.headers.authorization||"";console.log(`[${n.slice(0,8)}] POST /v1/chat/completions`),console.log(` Model: ${s.model||"unknown"}`),console.log(` Stream: ${a}`),console.log(` Auth: ${X(i.replace("Bearer ",""))}`),k.info("Request received",{requestId:n,path:"/v1/chat/completions",model:s.model,stream:a}),o.createSession(n,s);let l=m.cache!=="off",d=m.cache==="read-write",u=m.cache==="read",g=l&&await ee(s,m.cachePath);o.setCacheAvailable(n,g);let h=g?"cache":"llm",w;if(o.shouldIntervene()){console.log(` [POINT 1] Waiting for human action (cache: ${g?"yes":"no"})`);let p=await o.waitForPoint1(n);switch(p.action){case"cache":h="cache",console.log(" [ACTION] Use cache");break;case"llm":h="llm",console.log(" [ACTION] Call LLM");break;case"mock":h="mock",w=p.content,console.log(" [ACTION] Mock response");break}}else g?(console.log(" [AUTO] Using cached response"),h="cache"):(console.log(" [AUTO] Calling LLM"),h="llm");try{let p;switch(h){case"cache":{if(!g)throw new Error("Cache requested but no cached response available");p={...await Ke(t,e,n,s,a),cached:!0};break}case"llm":if(u){console.log(" [ERROR] Cache-only mode but no cache available"),o.error(n,"No cached response (cache: read mode)"),e.code(404).send({error:{message:"No cached response found (cache mode: read)",type:"cache_not_found"}});return}p=await Ve(t,e,n,s,a,d);break;case"mock":p=await ge(e,n,w||"",a);break}if(o.shouldIntervene()&&p){console.log(" [POINT 2] Waiting for human action");let c=await o.waitForPoint2(n);switch(c.action){case"return":console.log(" [ACTION] Return as-is");break;case"modify":console.log(" [ACTION] Modify response"),c.content&&(p=await ge(e,n,c.content,a));break}}p&&Ze(e,p,a),o.setResponseSource(n,h),o.complete(n),k.info("Request completed",{requestId:n,source:h})}catch(p){let c=p instanceof Error?p.message:"Unknown error";console.error(` [ERROR] ${c}`),k.error("Request failed",{requestId:n,error:c}),o.error(n,c),e.sent||e.code(500).send({error:{message:c,type:"proxy_error"}})}}async function Ke(t,e,o,n,s){let a=f(),i=await te(n,m.cachePath);if(!i)throw new Error("Failed to load cached response");let l=U({onToolCall:u=>{a.addToolCall(o,u),console.log(` [TOOL CALL] ${u.name}`)},onContent:u=>{a.appendContent(o,u)}}),d="";for await(let u of i.replay())l.feed(u),d+=u;return{content:d,status:i.getStatus(),cached:!0}}async function Ve(t,e,o,n,s,a){let i=f(),d={...n,stream_options:{include_usage:!0}},u=await F({method:"POST",path:"/v1/chat/completions",headers:t.headers,body:d,upstreamUrl:m.upstream});if(!u.ok||!u.body)return{content:u.body?await he(u.body):"",status:u.status};let g=a?new x(m.cachePath):null;g?.start(n);let h=U({onToolCall:c=>{i.addToolCall(o,c),console.log(` [TOOL CALL] ${c.name}`)},onContent:c=>{i.appendContent(o,c)},onFinishReason:c=>{i.setFinishReason(o,c)},onUsage:c=>{i.setUsage(o,c),console.log(` [USAGE] ${c.prompt_tokens} prompt, ${c.completion_tokens} completion`)}}),w="",p=Y(u.body);for await(let c of p)h.feed(c),g?.recordChunk(c),w+=c;if(g)try{let c=await g.save(u.status);console.log(` [CACHE] Saved to ${c}`)}catch(c){console.error(" [CACHE ERROR] Failed to save:",c)}return{content:w,status:u.status}}async function ge(t,e,o,n=!0){let s=le(o);if(s.type==="error")return{content:ie(s.content),status:400,mocked:!0};if(n){let a=s.type==="tool_call"?ae(s.functionName||"mock_function",s.content):re(s.content),i="";for await(let l of a)i+=l;return{content:i,status:200,mocked:!0}}else{let a=s.type==="tool_call"?[{id:`call_mock_${Date.now()}`,type:"function",function:{name:s.functionName||"mock_function",arguments:s.content}}]:void 0,i=s.type==="text"?s.content:null,l=ce(i,a);return{content:JSON.stringify(l),status:200,mocked:!0}}}function Ze(t,e,o){let n={};if(e.cached&&(n["x-playingpack-cached"]="true"),e.mocked&&(n["x-playingpack-mocked"]="true"),o&&e.content.includes("data: ")){t.code(e.status).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive");for(let[s,a]of Object.entries(n))t.header(s,a);t.raw.write(e.content),t.raw.end()}else{t.code(e.status).header("content-type","application/json");for(let[s,a]of Object.entries(n))t.header(s,a);t.send(e.content)}}async function Qe(t,e){try{let o=await F({method:t.method,path:t.url,headers:t.headers,body:t.body,upstreamUrl:m.upstream});if(e.code(o.status),o.headers.forEach((n,s)=>{["content-encoding","transfer-encoding"].includes(s.toLowerCase())||e.header(s,n)}),o.body){let n=await he(o.body);e.send(n)}else e.send()}catch(o){let n=o instanceof Error?o.message:"Unknown error";e.code(500).send({error:{message:n,type:"proxy_error"}})}}async function he(t){if(!t)return"";let e=t.getReader(),o=new TextDecoder,n="";for(;;){let{done:s,value:a}=await e.read();if(s)break;n+=o.decode(a,{stream:!0})}return n}import{join as me,dirname as Xe}from"path";import{fileURLToPath as Ye}from"url";import{access as et}from"fs/promises";import tt from"@fastify/static";var nt=Xe(Ye(import.meta.url));function fe(){return me(nt,"../public")}async function ot(){try{return await et(me(fe(),"index.html")),!0}catch{return!1}}async function ye(t){let e=fe();return await ot()?(await t.register(tt,{root:e,prefix:"/",wildcard:!1}),t.setNotFoundHandler((n,s)=>n.url.startsWith("/v1")||n.url.startsWith("/api")||n.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 E=new Set;function be(t){E.add(t);let e=f(),o=e.getAllSessions();for(let s of o)J(t,{type:"request_update",session:s});let n=e.subscribe(s=>{J(t,s)});t.on("message",s=>{try{let a=JSON.parse(s.toString());st(t,a)}catch{}}),t.on("close",()=>{E.delete(t),n()}),t.on("error",()=>{E.delete(t),n()})}function st(t,e){let o=B.safeParse(e);if(!o.success){if(typeof e=="object"&&e!==null&&e.type==="ping"){J(t,{type:"request_update",session:null});return}return}let n=o.data,s=f();switch(n.type){case"point1_action":s.resolvePoint1(n.requestId,n.action);break;case"point2_action":s.resolvePoint2(n.requestId,n.action);break}}function J(t,e){if(t.readyState===t.OPEN)try{t.send(JSON.stringify(e))}catch{E.delete(t)}}async function ke(t){let e=t.port,o=t.host;W({cache:t.cache,intervene:t.intervene}),await pe(t.logPath),await k.info("Server starting",{upstream:t.upstream,cachePath:t.cachePath,cache:t.cache,intervene:t.intervene,headless:t.headless});let n=await lt({port:e});n!==e&&console.log(` Port ${e} in use, using ${n}`);let s=rt({logger:!1,bodyLimit:50*1024*1024});return await s.register(at,{origin:!0,credentials:!0}),await s.register(it),s.get("/ws",{websocket:!0},a=>{be(a)}),await s.register(ct,{prefix:"/api/trpc",trpcOptions:{router:Q,createContext:H}}),de(s,{upstream:t.upstream,cachePath:t.cachePath,cache:t.cache}),t.headless||await ye(s),await s.listen({port:n,host:o}),await k.info("Server listening",{port:n,host:o}),{server:s,port:n,host:o}}import{readFile as ut}from"fs/promises";import{existsSync as pt}from"fs";import{join as ve}from"path";import{createJiti as gt}from"jiti";var dt=["playingpack.config.ts","playingpack.config.mts","playingpack.config.js","playingpack.config.mjs"],ht=["playingpack.config.jsonc","playingpack.config.json",".playingpackrc.json",".playingpackrc"],mt={cache:"read-write",intervene:!0,upstream:"https://api.openai.com",port:4747,host:"0.0.0.0",cachePath:".playingpack/cache",logPath:".playingpack/logs",headless:!1};function ft(t){let e=t.replace(/\/\*[\s\S]*?\*\//g,"");return e=e.replace(/(?<!["'])\/\/.*$/gm,""),e}async function yt(t){for(let e of dt){let o=ve(t,e);if(pt(o))try{let s=await gt(import.meta.url,{interopDefault:!0}).import(o),a=s&&typeof s=="object"&&"default"in s?s.default:s;if(!a||typeof a!="object"){console.warn(` Warning: ${e} must export a config object`);continue}return{config:T.parse(a),filename:e}}catch(n){console.warn(` Warning: Error loading ${e}:`,n.message)}}return null}async function bt(t){for(let e of ht){let o=ve(t,e);try{let n=await ut(o,"utf-8"),s=JSON.parse(ft(n));return{config:T.parse(s),filename:e}}catch(n){n.code!=="ENOENT"&&(n instanceof SyntaxError||n.name==="ZodError")&&console.warn(` Warning: Invalid config in ${e}:`,n.message)}}return null}async function kt(t){let e=await yt(t);if(e)return console.log(` Config loaded from ${e.filename}`),e.config;let o=await bt(t);return o?(console.log(` Config loaded from ${o.filename}`),o.config):{}}async function Se(t={}){let e=process.cwd(),o=await kt(e),n={...mt,...o};return t.port!==void 0&&(n.port=t.port),t.host!==void 0&&(n.host=t.host),t.ui!==void 0&&(n.headless=!t.ui),t.upstream!==void 0&&(n.upstream=t.upstream),t.cachePath!==void 0&&(n.cachePath=t.cachePath),t.cache!==void 0&&(n.cache=t.cache),t.intervene!==void 0&&(n.intervene=t.intervene),n}var we="1.0.0";P.name("playingpack").description("Chrome DevTools for AI Agents - Debug and test your AI agent LLM calls").version(we);P.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 (headless mode for CI/CD)").option("--upstream <url>","Upstream API URL (default: https://api.openai.com)").option("--cache-path <path>","Directory for cached responses (default: .playingpack/cache)").option("--cache <mode>","Cache mode: off, read, read-write (default: read-write)").option("--no-intervene","Disable intervention mode (no pausing for human actions)").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 PlayingPack - Debug your 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 Se({port:t.port?parseInt(t.port,10):void 0,host:t.host,ui:t.ui,upstream:t.upstream,cachePath:t.cachePath,cache:t.cache,intervene:t.intervene}),{port:o,host:n}=await ke(e),s=`http://localhost:${o}`,a=n==="0.0.0.0"?`http://<your-ip>:${o}`:`http://${n}:${o}`;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: ${a.padEnd(44)}\u2502`),console.log(" \u2502 \u2502"),console.log(" \u2502 Settings: \u2502"),console.log(` \u2502 Cache: ${e.cache.padEnd(41)}\u2502`),console.log(` \u2502 Intervene: ${String(e.intervene).padEnd(41)}\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"),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(),e.headless?console.log(" Running in headless mode (no UI)"):(console.log(" Opening dashboard in browser..."),await vt(s)),console.log(),console.log(" Waiting for requests..."),console.log();let i=async()=>{console.log(`
7
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)}});P.command("version").description("Show version").action(()=>{console.log(`playingpack v${we}`)});process.argv.length===2?P.parse(["node","playingpack","start"]):P.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playingpack",
3
- "version": "0.6.0",
3
+ "version": "0.7.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": {