playingpack 0.3.2 → 0.5.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
@@ -26,27 +26,28 @@ PlayingPack solves these problems:
26
26
 
27
27
  | Problem | Solution |
28
28
  |---------|----------|
29
- | Expensive iteration | **VCR Mode** — Record once, replay forever. Zero API costs after first run. |
30
- | Non-deterministic tests | **Tape playback** — Same request always returns same response. Deterministic by design. |
31
- | Blind debugging | **Interceptor** — Pause on tool calls, inspect payloads, see exactly what the LLM decided. |
29
+ | Expensive iteration | **Cache Mode** — Record once, replay forever. Zero API costs after first run. |
30
+ | Non-deterministic tests | **Cache playback** — Same request always returns same response. Deterministic by design. |
31
+ | Blind debugging | **Intervene Mode** — Pause before and after LLM calls. Inspect, edit, or mock at any point. |
32
32
  | Slow feedback | **Instant replay** — Cached responses return in milliseconds, not seconds. |
33
- | CI/CD reliability | **Replay-only mode** — Run tests against recorded tapes. Fast, free, deterministic. |
33
+ | CI/CD reliability | **Read-only cache** — Run tests against cached responses. Fast, free, deterministic. |
34
34
 
35
35
  ---
36
36
 
37
37
  ## Features
38
38
 
39
- ### VCR Mode
40
- Record API responses and replay them deterministically. First request hits the real API and saves a "tape." Subsequent identical requests replay from cache with original timing preserved.
39
+ ### Cache Mode
40
+ Record API responses and replay them deterministically. First request hits the real API and saves the response to cache. Subsequent identical requests replay from cache with original timing preserved.
41
41
 
42
42
  ### Real-time Dashboard
43
43
  Browser-based UI showing live request streaming, status updates, request/response inspection with syntax highlighting, and full history.
44
44
 
45
- ### Interceptor
46
- Pause requests when tool calls are detected. Inspect the exact function name and arguments. Decide whether to allow the request to continue or inject a mock response.
45
+ ### Intervene Mode
46
+ Pause requests at two points in the lifecycle:
47
+ - **Before LLM call** — Inspect the request, edit it, use a cached response, or mock without calling the LLM
48
+ - **After LLM response** — Inspect the response before it reaches your agent, modify or mock as needed
47
49
 
48
- ### Mock Editor
49
- Monaco-powered JSON editor for crafting custom responses. Test error scenarios, edge cases, or specific tool call results without touching the real API.
50
+ Full control over request/response flow with the ability to inject mock responses at any point.
50
51
 
51
52
  ### SSE Streaming
52
53
  Full OpenAI-compatible streaming with proper chunk handling. Parses tool calls in real-time. Works exactly like the real API.
@@ -150,12 +151,12 @@ Navigate to [http://localhost:4747](http://localhost:4747) in your browser.
150
151
  Record your agent's API interactions once, then replay them in tests forever:
151
152
 
152
153
  ```bash
153
- # First run: records responses to .playingpack/tapes/
154
+ # First run: records responses to .playingpack/cache/
154
155
  npx playingpack start &
155
156
  pytest tests/
156
157
 
157
158
  # Subsequent runs: replays from cache (instant, free)
158
- npx playingpack start --record replay-only &
159
+ npx playingpack start --cache read &
159
160
  pytest tests/
160
161
  ```
161
162
 
@@ -167,33 +168,36 @@ Your tests become:
167
168
 
168
169
  ### Debugging Agent Behavior
169
170
 
170
- Enable pause mode to stop on tool calls and inspect what your agent is doing:
171
+ Enable intervene mode to pause requests and inspect what your agent is doing:
171
172
 
172
- 1. Start PlayingPack and open the dashboard
173
- 2. Toggle "Pause on tool calls" in the UI
174
- 3. Run your agent
175
- 4. When a tool call is detected, the request pauses
176
- 5. Inspect the function name, arguments, and full context
177
- 6. Click "Allow" to continue or "Mock" to inject a custom response
173
+ 1. Start PlayingPack with `--intervene` flag or enable in the dashboard
174
+ 2. Run your agent
175
+ 3. At **Point 1** (before LLM call), choose:
176
+ - **Allow** Send the original request to the LLM
177
+ - **Use Cache** Replay from cache if available (saves API costs)
178
+ - **Mock** Return a mock response without calling the LLM
179
+ 4. At **Point 2** (after LLM response), choose:
180
+ - **Return** — Send the response to your agent as-is
181
+ - **Modify** — Edit the response before sending to your agent
178
182
 
179
183
  ### CI/CD Integration
180
184
 
181
- Run your test suite against recorded tapes in CI:
185
+ Run your test suite against cached responses in CI:
182
186
 
183
187
  ```bash
184
188
  # In your CI pipeline
185
- npx playingpack start --no-ui --record replay-only &
189
+ npx playingpack start --no-ui --cache read &
186
190
  sleep 2 # Wait for server
187
191
  npm test
188
192
  ```
189
193
 
190
- If a tape is missing, the request fails immediately — no surprise API calls in CI.
194
+ If a cached response is missing, the request fails immediately — no surprise API calls in CI.
191
195
 
192
196
  ```yaml
193
197
  # Example GitHub Actions step
194
198
  - name: Run tests with PlayingPack
195
199
  run: |
196
- npx playingpack start --no-ui --record replay-only &
200
+ npx playingpack start --no-ui --cache read &
197
201
  sleep 2
198
202
  npm test
199
203
  ```
@@ -235,17 +239,20 @@ export default defineConfig({
235
239
  // Upstream API endpoint (default: https://api.openai.com)
236
240
  upstream: process.env.LLM_API_URL ?? 'https://api.openai.com',
237
241
 
238
- // Directory for tape storage (default: .playingpack/tapes)
239
- tapesDir: '.playingpack/tapes',
242
+ // Cache mode: 'off' | 'read' | 'read-write' (default: read-write)
243
+ // - off: Always hit upstream, never cache
244
+ // - read: Only read from cache, fail if missing
245
+ // - read-write: Read from cache if available, write new responses
246
+ cache: process.env.CI ? 'read' : 'read-write',
240
247
 
241
- // Directory for logs (default: .playingpack/logs)
242
- logsDir: '.playingpack/logs',
248
+ // Intervene mode: pause for human inspection (default: true)
249
+ intervene: true,
250
+
251
+ // Directory for cache storage (default: .playingpack/cache)
252
+ cachePath: '.playingpack/cache',
243
253
 
244
- // Recording mode: 'auto' | 'off' | 'replay-only' (default: auto)
245
- // - auto: Record if no tape exists, replay if it does
246
- // - off: Always hit upstream, never record
247
- // - replay-only: Only replay from cache, fail if tape missing
248
- record: process.env.CI ? 'replay-only' : 'auto',
254
+ // Directory for logs (default: .playingpack/logs)
255
+ logPath: '.playingpack/logs',
249
256
 
250
257
  // Server port (default: 4747)
251
258
  port: 4747,
@@ -255,12 +262,6 @@ export default defineConfig({
255
262
 
256
263
  // Run without UI in CI environments (default: false)
257
264
  headless: !!process.env.CI,
258
-
259
- // Pause mode: 'off' | 'tool-calls' | 'all' (default: off)
260
- // - off: No interception, requests flow through normally
261
- // - tool-calls: Pause when LLM makes a tool/function call
262
- // - all: Pause on every request
263
- pause: 'off',
264
265
  });
265
266
  ```
266
267
 
@@ -277,8 +278,8 @@ export default defineConfig({
277
278
  ? 'https://api.openai.com'
278
279
  : 'http://localhost:11434/v1',
279
280
 
280
- // CI: replay-only (fast, deterministic), Local: auto (record on miss)
281
- record: process.env.CI ? 'replay-only' : 'auto',
281
+ // CI: read-only (fast, deterministic), Local: read-write (record on miss)
282
+ cache: process.env.CI ? 'read' : 'read-write',
282
283
 
283
284
  // No UI needed in CI
284
285
  headless: !!process.env.CI,
@@ -312,8 +313,9 @@ npx playingpack start [options]
312
313
  | `-h, --host <host>` | Host to bind to | `0.0.0.0` |
313
314
  | `--no-ui` | Run without UI (headless mode) | `false` |
314
315
  | `--upstream <url>` | Upstream API URL | `https://api.openai.com` |
315
- | `--tapes-dir <path>` | Directory for tape storage | `.playingpack/tapes` |
316
- | `--record <mode>` | Recording mode (`auto`, `off`, `replay-only`) | `auto` |
316
+ | `--cache-path <path>` | Directory for cache storage | `.playingpack/cache` |
317
+ | `--cache <mode>` | Cache mode (`off`, `read`, `read-write`) | `read-write` |
318
+ | `--intervene` | Enable human intervention mode | `true` |
317
319
 
318
320
  ### Examples
319
321
 
@@ -321,14 +323,14 @@ npx playingpack start [options]
321
323
  # Proxy to a local LLM (Ollama)
322
324
  npx playingpack start --upstream http://localhost:11434/v1
323
325
 
324
- # CI mode: replay only, no UI, fail if tape missing
325
- npx playingpack start --no-ui --record replay-only
326
+ # CI mode: read-only cache, no UI, fail if cache missing
327
+ npx playingpack start --no-ui --cache read
326
328
 
327
- # Custom port and tapes directory
328
- npx playingpack start --port 8080 --tapes-dir ./test/fixtures/tapes
329
+ # Custom port and cache directory
330
+ npx playingpack start --port 8080 --cache-path ./test/fixtures/cache
329
331
 
330
- # Production OpenAI with custom tapes location
331
- npx playingpack start --tapes-dir ./recordings
332
+ # Enable intervention mode for debugging
333
+ npx playingpack start --intervene
332
334
  ```
333
335
 
334
336
  ---
@@ -351,29 +353,28 @@ Your Agent → PlayingPack (localhost:4747) → Upstream API
351
353
 
352
354
  1. **Request arrives** at `POST /v1/chat/completions`
353
355
  2. **Cache lookup** — Request body is normalized and hashed (SHA-256)
354
- 3. **Cache hit?** → Replay tape with original timing
355
- 4. **Cache miss?**Forward to upstream, stream response, record tape
356
- 5. **Tool call detected?** → Optionally pause for inspection
357
- 6. **Response complete** → Save tape, notify dashboard
356
+ 3. **Intervention Point 1?** → If intervene enabled, wait for user action (allow/cache/mock)
357
+ 4. **Get response**From cache (if available) or upstream LLM
358
+ 5. **Intervention Point 2?** → If intervene enabled, wait for user action (return/modify)
359
+ 6. **Response complete** → Save to cache (if enabled), notify dashboard
358
360
 
359
- ### State Machine
361
+ ### Simple Mental Model
360
362
 
361
363
  ```
362
- LOOKUP → (cache hit) → REPLAY → COMPLETE
363
- ↘ (cache miss) → CONNECT → STREAMING → COMPLETE
364
- ↓ (tool call + pause enabled)
365
- INTERCEPT
366
-
367
- ┌────────────┴────────────┐
368
- ↓ ↓
369
- FLUSH (allow) INJECT (mock)
370
- ↓ ↓
371
- COMPLETE COMPLETE
364
+ ┌─────────────────────────────────────────────────────────────────────┐
365
+ │ PlayingPack │
366
+ │ │
367
+ │ Cache: System remembers responses (read/write/off) │
368
+ │ Intervene: Human can inspect/modify at two points │
369
+ │ │
370
+ │ Request → [Point 1: Before LLM] → Response → [Point 2: After] → │
371
+ │ │
372
+ └─────────────────────────────────────────────────────────────────────┘
372
373
  ```
373
374
 
374
- ### Tape Format
375
+ ### Cache Format
375
376
 
376
- Tapes are stored as JSON files named by request hash:
377
+ Cached responses are stored as JSON files named by request hash:
377
378
 
378
379
  ```json
379
380
  {
@@ -407,7 +408,7 @@ Requests are normalized before hashing to ensure deterministic matching:
407
408
  - Keys are sorted alphabetically
408
409
  - `stream` parameter is ignored (streaming and non-streaming match)
409
410
  - Timestamps and request IDs are removed
410
- - Result: SHA-256 hash used as tape filename
411
+ - Result: SHA-256 hash used as cache filename
411
412
 
412
413
  ---
413
414
 
@@ -459,8 +460,8 @@ playingpack/
459
460
  │ ├── shared/ # TypeScript types & Zod schemas
460
461
  │ ├── cli/ # Fastify proxy server + CLI
461
462
  │ │ ├── proxy/ # HTTP routing, upstream client, SSE parsing
462
- │ │ ├── tape/ # Recording & playback
463
- │ │ ├── interceptor/ # Session state machine
463
+ │ │ ├── cache/ # Response caching & playback
464
+ │ │ ├── session/ # Session state management
464
465
  │ │ ├── mock/ # Synthetic response generation
465
466
  │ │ ├── trpc/ # API procedures
466
467
  │ │ └── websocket/ # Real-time events
@@ -480,14 +481,14 @@ A: No. Requests are forwarded to upstream unchanged. The only modification is ad
480
481
  **Q: Can I use this in production?**
481
482
  A: PlayingPack is designed for development and testing. For production, point your agents directly at your LLM provider.
482
483
 
483
- **Q: How do I update recordings when my prompts change?**
484
- A: Delete the relevant tapes from `.playingpack/tapes/` and run your tests again. New tapes will be recorded automatically.
484
+ **Q: How do I update cached responses when my prompts change?**
485
+ A: Delete the relevant files from `.playingpack/cache/` and run your tests again. New responses will be cached automatically.
485
486
 
486
487
  **Q: Does it work with function calling / tool use?**
487
- A: Yes. PlayingPack fully supports OpenAI's function calling and tool use. The interceptor can pause specifically on tool calls for inspection.
488
+ A: Yes. PlayingPack fully supports OpenAI's function calling and tool use.
488
489
 
489
- **Q: Can I share tapes with my team?**
490
- A: Yes. Commit your `.playingpack/tapes/` directory to version control. Everyone on the team gets the same deterministic behavior.
490
+ **Q: Can I share cached responses with my team?**
491
+ A: Yes. Commit your `.playingpack/cache/` directory to version control. Everyone on the team gets the same deterministic behavior.
491
492
 
492
493
  ---
493
494
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
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,"TOOL_CALL"),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,"PAUSED");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 L(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(),F=a.object({requestId:a.string(),type:a.enum(["text","error","tool_result"]),content:a.string()}),$=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($).mutation(({ctx:n,input:e})=>({success:n.sessionManager.allowRequest(e.requestId)})),mockRequest:m.procedure.input(F).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}
2
+ import{program as C}from"commander";import mt from"open";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";import{createHash as ve}from"crypto";function M(t){if(t==null)return null;if(Array.isArray(t))return t.map(M);if(typeof t=="object"){let e=t,o={},n=Object.keys(e).filter(s=>!["stream","request_id","timestamp"].includes(s)).sort();for(let s of n)o[s]=M(e[s]);return o}return t}function v(t){let e=M(t),o=JSON.stringify(e);return ve("sha256").update(o).digest("hex")}var R=class{sessions=new Map;settings;listeners=new Set;point1Resolvers=new Map;point2Resolvers=new Map;constructor(e){this.settings={cache:"read-write",intervene:!0,...e}}createSession(e,o){let n=o,s=typeof n?.model=="string"?n.model:"unknown",a=Array.isArray(n?.messages)?n.messages:[],i=n?.stream!==!1,c={id:e,state:this.settings.intervene?"pending":"processing",timestamp:new Date().toISOString(),request:{model:s,messages:a,stream:i},cacheKey:v(o),cacheHit:!1};return this.sessions.set(e,c),this.emit({type:"request_update",session:c}),c}getSession(e){return this.sessions.get(e)}getAllSessions(){return Array.from(this.sessions.values())}updateState(e,o){let n=this.sessions.get(e);n&&(n.state=o,this.emit({type:"request_update",session:n}))}setProcessing(e){this.updateState(e,"processing")}setReviewing(e){this.updateState(e,"reviewing")}setCacheHit(e,o){let n=this.sessions.get(e);n&&(n.cacheHit=o,this.emit({type:"request_update",session:n}))}setResponse(e,o,n,s=[]){let a=this.sessions.get(e);a&&(a.response={status:o,content:n,toolCalls:s},this.emit({type:"request_update",session:a}))}addToolCall(e,o){let n=this.sessions.get(e);n&&(n.response||(n.response={status:200,content:"",toolCalls:[]}),n.response.toolCalls.push(o),this.emit({type:"request_update",session:n}))}appendContent(e,o){let n=this.sessions.get(e);n&&(n.response||(n.response={status:200,content:"",toolCalls:[]}),n.response.content+=o)}complete(e){let o=this.sessions.get(e);o&&(o.state="complete",o.completedAt=new Date().toISOString(),this.emit({type:"request_update",session:o}))}error(e,o){let n=this.sessions.get(e);n&&(n.state="complete",n.error=o,n.completedAt=new Date().toISOString(),this.emit({type:"request_update",session:n}))}shouldIntervene(){return this.settings.intervene}async waitForPoint1(e){if(!this.sessions.get(e))throw new Error(`Session ${e} not found`);return new Promise(n=>{this.point1Resolvers.set(e,{resolve:n})})}async waitForPoint2(e){if(!this.sessions.get(e))throw new Error(`Session ${e} not found`);return this.updateState(e,"reviewing"),new Promise(n=>{this.point2Resolvers.set(e,{resolve:n})})}resolvePoint1(e,o){let n=this.point1Resolvers.get(e);return n?(this.point1Resolvers.delete(e),this.updateState(e,"processing"),n.resolve(o),!0):!1}resolvePoint2(e,o){let n=this.point2Resolvers.get(e);return n?(this.point2Resolvers.delete(e),n.resolve(o),!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 o of this.listeners)try{o(e)}catch{}}cleanup(){let e=Array.from(this.sessions.entries());if(e.length>100){let o=e.filter(([n,s])=>s.state==="complete").slice(0,e.length-100);for(let[n]of o)this.sessions.delete(n)}}removeSession(e){this.sessions.delete(e),this.point1Resolvers.delete(e),this.point2Resolvers.delete(e)}},S=null;function J(t){return S=new R(t),S}function f(){return S||(S=new R),S}function W(t){return{sessionManager:f(),req:t?.req,res:t?.res}}import{initTRPC as Re}from"@trpc/server";import{z as w}from"zod";import{createRequire as xe}from"module";import{z as r}from"zod";var G=r.enum(["off","read","read-write"]),A=r.object({cache:G.optional(),intervene:r.boolean().optional(),upstream:r.string().url().optional(),port:r.number().int().min(1).max(65535).optional(),host:r.string().optional(),cachePath:r.string().optional(),logPath:r.string().optional(),headless:r.boolean().optional()}),we=r.object({cache:G,intervene:r.boolean()}),z=r.object({settings:we.partial()}),Pt=r.object({model:r.string(),messages:r.array(r.object({role:r.enum(["system","user","assistant","tool"]),content:r.union([r.string(),r.null(),r.array(r.any())]).optional(),name:r.string().optional(),tool_calls:r.array(r.any()).optional(),tool_call_id:r.string().optional()})),temperature:r.number().optional(),top_p:r.number().optional(),n:r.number().optional(),stream:r.boolean().optional(),stop:r.union([r.string(),r.array(r.string())]).optional(),max_tokens:r.number().optional(),presence_penalty:r.number().optional(),frequency_penalty:r.number().optional(),logit_bias:r.record(r.number()).optional(),user:r.string().optional(),tools:r.array(r.object({type:r.literal("function"),function:r.object({name:r.string(),description:r.string().optional(),parameters:r.any().optional()})})).optional(),tool_choice:r.union([r.literal("none"),r.literal("auto"),r.literal("required"),r.object({type:r.literal("function"),function:r.object({name:r.string()})})]).optional(),response_format:r.object({type:r.enum(["text","json_object"])}).optional(),seed:r.number().optional()}).passthrough(),Se=r.object({data:r.string(),delay:r.number()}),Rt=r.object({hash:r.string(),timestamp:r.string(),request:r.object({model:r.string(),messages:r.array(r.unknown())}),response:r.object({status:r.number(),body:r.unknown().optional(),chunks:r.array(Se).optional()})}),j=r.discriminatedUnion("action",[r.object({action:r.literal("llm")}),r.object({action:r.literal("cache")}),r.object({action:r.literal("mock"),content:r.string()})]),T=r.discriminatedUnion("action",[r.object({action:r.literal("return")}),r.object({action:r.literal("modify"),content:r.string()})]),Ce=r.object({type:r.literal("point1_action"),requestId:r.string(),action:j}),Pe=r.object({type:r.literal("point2_action"),requestId:r.string(),action:T}),B=r.discriminatedUnion("type",[Ce,Pe]);var _e=xe(import.meta.url),Ee=_e("../../package.json"),b=Re.context().create(),K=b.router({getSessions:b.procedure.query(({ctx:t})=>({sessions:t.sessionManager.getAllSessions()})),getSession:b.procedure.input(w.object({id:w.string()})).query(({ctx:t,input:e})=>({session:t.sessionManager.getSession(e.id)||null})),getSettings:b.procedure.query(({ctx:t})=>({settings:t.sessionManager.getSettings(),version:Ee.version})),updateSettings:b.procedure.input(z).mutation(({ctx:t,input:e})=>(t.sessionManager.updateSettings(e.settings),{settings:t.sessionManager.getSettings()})),point1Action:b.procedure.input(w.object({requestId:w.string(),action:j})).mutation(({ctx:t,input:e})=>({success:t.sessionManager.resolvePoint1(e.requestId,e.action)})),point2Action:b.procedure.input(w.object({requestId:w.string(),action:T})).mutation(({ctx:t,input:e})=>({success:t.sessionManager.resolvePoint2(e.requestId,e.action)})),health:b.procedure.query(()=>({status:"ok",timestamp:new Date().toISOString()}))});import{createParser as Me}from"eventsource-parser";var I=class{callbacks;toolCalls=new Map;accumulatedContent="";parser;constructor(e={}){this.callbacks=e,this.parser=Me(o=>{o.type==="event"&&this.handleEvent(o.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(o=>({id:o.id,type:"function",function:{name:o.name,arguments:o.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 o=JSON.parse(e);for(let n of o.choices){let s=n.delta;if(s.content&&(this.accumulatedContent+=s.content,this.callbacks.onContent?.(s.content)),s.tool_calls)for(let a of s.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(o){this.callbacks.onError?.(o instanceof Error?o:new Error(String(o)))}}};function O(t){return new I(t)}var Ae="https://api.openai.com";function V(t){return!t||t.length<8?"****":`${t.substring(0,4)}...${t.substring(t.length-4)}`}function je(t,e=!0){let o=new Headers,n=["authorization","content-type","accept","openai-organization","openai-project","user-agent"];for(let s of n){let a=t[s];if(a){let i=Array.isArray(a)?a[0]:a;i&&o.set(s,i)}}return e?o.set("accept","text/event-stream"):o.set("accept","application/json"),o}async function q(t){let o=`${t.upstreamUrl||Ae}${t.path}`,n=typeof t.body=="object"&&t.body!==null&&"stream"in t.body&&typeof t.body.stream=="boolean"?t.body.stream:!0,s=je(t.headers,n),a=await fetch(o,{method:t.method,headers:s,body:JSON.stringify(t.body)});return{status:a.status,headers:a.headers,body:a.body,ok:a.ok}}async function*Z(t){let e=t.getReader(),o=new TextDecoder;try{for(;;){let{done:s,value:a}=await e.read();if(s)break;yield o.decode(a,{stream:!0})}let n=o.decode();n&&(yield n)}finally{e.releaseLock()}}import{mkdir as Te,writeFile as Ie,readFile as Oe,access as qe}from"fs/promises";import{dirname as Fe,join as N}from"path";var U=".playingpack/cache";async function Q(t,e=U){let o=v(t),n=N(e,`${o}.json`);try{return await qe(n),!0}catch{return!1}}async function Ne(t,e=U){let o=v(t),n=N(e,`${o}.json`);try{let s=await Oe(n,"utf-8");return JSON.parse(s)}catch{return null}}var x=class{cachePath;chunks=[];lastChunkTime=0;requestBody;model="unknown";hash="";constructor(e=U){this.cachePath=e}start(e){if(this.requestBody=e,this.chunks=[],this.lastChunkTime=Date.now(),this.hash=v(e),typeof e=="object"&&e!==null){let o=e;typeof o.model=="string"&&(this.model=o.model)}}recordChunk(e){let o=Date.now(),n=this.chunks.length===0?0:o-this.lastChunkTime;this.chunks.push({data:e,delay:n}),this.lastChunkTime=o}async save(e=200){let o={hash:this.hash,timestamp:new Date().toISOString(),request:{model:this.model,messages:this.requestBody?.messages??[]},response:{status:e,chunks:this.chunks}},n=this.getCachePath();return await Te(Fe(n),{recursive:!0}),await Ie(n,JSON.stringify(o,null,2),"utf-8"),n}getCachePath(){return N(this.cachePath,`${this.hash}.json`)}getHash(){return this.hash}getChunkCount(){return this.chunks.length}},F=class{cached;aborted=!1;constructor(e){this.cached=e}getStatus(){return this.cached.response.status}getHash(){return this.cached.hash}abort(){this.aborted=!0}async*replay(){this.aborted=!1;let e=this.cached.response.chunks??[];for(let o of e){if(this.aborted||(o.delay>0&&await this.delay(o.delay),this.aborted))break;yield o.data}}async*replayFast(){let e=this.cached.response.chunks??[];for(let o of e){if(this.aborted)break;yield o.data}}delay(e){return new Promise(o=>setTimeout(o,e))}};async function X(t,e){let o=await Ne(t,e);return o?new F(o):null}function $(){return`chatcmpl-mock-${Date.now()}`}function te(t,e=4){let o=[];for(let n=0;n<t.length;n+=e)o.push(t.slice(n,n+e));return o}function y(t){return`data: ${t}
3
3
 
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 Le,mkdir as Fe}from"fs/promises";import{join as $e}from"path";var j=null,O=null;async function pe(n){j=n,await Fe(j,{recursive:!0});let e=new Date().toISOString().split("T")[0];O=$e(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)}
4
+ `}function Y(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 ee(t,e,o,n,s,a=!1,i=null){let c={index:0};a?(c.id=o,c.type="function",c.function={name:n,arguments:s}):c.function={arguments:s};let u={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(u)}async function*ne(t,e={}){let{model:o="gpt-4",delayMs:n=20}=e,s=$(),a=te(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 c of a)n>0&&await ie(n),yield y(Y(s,o,c));yield y(Y(s,o,null,"stop")),yield y("[DONE]")}async function*oe(t,e,o={}){let{model:n="gpt-4",delayMs:s=10}=o,a=$(),i=`call_mock_${Date.now()}`,c=te(e,10),u={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(u)),yield y(ee(a,n,i,t,c[0]||"",!0));for(let g=1;g<c.length;g++)s>0&&await ie(s),yield y(ee(a,n,i,t,c[g]||""));let p={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(p)),yield y("[DONE]")}function se(t,e="invalid_request_error",o=null){return JSON.stringify({error:{message:t,type:e,param:null,code:o}})}function re(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 ae(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 ie(t){return new Promise(e=>setTimeout(e,t))}import{appendFile as Ue,mkdir as $e}from"fs/promises";import{join as Le}from"path";var L=null,D=null;async function ce(t){L=t,await $e(L,{recursive:!0});let e=new Date().toISOString().split("T")[0];D=Le(L,`server-${e}.log`)}function De(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 R(n,e,t){if(!O)return;let o={timestamp:new Date().toISOString(),level:n,message:e,data:t};try{await Le(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:L}}),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();
6
+ `}async function _(t,e,o){if(!D)return;let n={timestamp:new Date().toISOString(),level:t,message:e,data:o};try{await Ue(D,De(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 h={upstream:"https://api.openai.com",cachePath:".playingpack/cache",cache:"read-write"};function ue(t,e){e&&(h={...h,...e}),t.post("/v1/chat/completions",async(o,n)=>{await He(o,n)}),t.get("/health",async(o,n)=>n.send({status:"ok"})),t.all("/v1/*",async(o,n)=>{await ze(o,n)})}async function He(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: ${V(i.replace("Bearer ",""))}`),k.info("Request received",{requestId:n,path:"/v1/chat/completions",model:s.model,stream:a}),o.createSession(n,s);let c=h.cache!=="off",u=h.cache==="read-write",p=h.cache==="read",g=c&&await Q(s,h.cachePath);o.setCacheHit(n,g);let d=g?"cache":"llm",P;if(o.shouldIntervene()){console.log(` [POINT 1] Waiting for human action (cache: ${g?"yes":"no"})`);let l=await o.waitForPoint1(n);switch(l.action){case"cache":d="cache",console.log(" [ACTION] Use cache");break;case"llm":d="llm",console.log(" [ACTION] Call LLM");break;case"mock":d="mock",P=l.content,console.log(" [ACTION] Mock response");break}}else g?(console.log(" [AUTO] Using cached response"),d="cache"):(console.log(" [AUTO] Calling LLM"),d="llm");try{let l;switch(d){case"cache":{if(!g)throw new Error("Cache requested but no cached response available");l={...await Je(t,e,n,s,a),cached:!0};break}case"llm":if(p){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}l=await We(t,e,n,s,a,u);break;case"mock":l=await le(e,n,P||"",a);break}if(o.shouldIntervene()&&l){console.log(" [POINT 2] Waiting for human action");let m=await o.waitForPoint2(n);switch(m.action){case"return":console.log(" [ACTION] Return as-is");break;case"modify":console.log(" [ACTION] Modify response"),m.content&&(l=await le(e,n,m.content,a));break}}l&&Ge(e,l,a),o.complete(n),k.info("Request completed",{requestId:n,source:d})}catch(l){let m=l instanceof Error?l.message:"Unknown error";console.error(` [ERROR] ${m}`),k.error("Request failed",{requestId:n,error:m}),o.error(n,m),e.sent||e.code(500).send({error:{message:m,type:"proxy_error"}})}}async function Je(t,e,o,n,s){let a=f(),i=await X(n,h.cachePath);if(!i)throw new Error("Failed to load cached response");let c=O({onToolCall:p=>{a.addToolCall(o,p),console.log(` [TOOL CALL] ${p.name}`)},onContent:p=>{a.appendContent(o,p)}}),u="";for await(let p of i.replay())c.feed(p),u+=p;return{content:u,status:i.getStatus(),cached:!0}}async function We(t,e,o,n,s,a){let i=f(),c=n,u=await q({method:"POST",path:"/v1/chat/completions",headers:t.headers,body:c,upstreamUrl:h.upstream});if(!u.ok||!u.body)return{content:u.body?await pe(u.body):"",status:u.status};let p=a?new x(h.cachePath):null;p?.start(n);let g=O({onToolCall:l=>{i.addToolCall(o,l),console.log(` [TOOL CALL] ${l.name}`)},onContent:l=>{i.appendContent(o,l)}}),d="",P=Z(u.body);for await(let l of P)g.feed(l),p?.recordChunk(l),d+=l;if(p)try{let l=await p.save(u.status);console.log(` [CACHE] Saved to ${l}`)}catch(l){console.error(" [CACHE ERROR] Failed to save:",l)}return{content:d,status:u.status}}async function le(t,e,o,n=!0){let s=ae(o);if(s.type==="error")return{content:se(s.content),status:400,mocked:!0};if(n){let a=s.type==="tool_call"?oe(s.functionName||"mock_function",s.content):ne(s.content),i="";for await(let c of a)i+=c;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,c=re(i,a);return{content:JSON.stringify(c),status:200,mocked:!0}}}function Ge(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 ze(t,e){try{let o=await q({method:t.method,path:t.url,headers:t.headers,body:t.body,upstreamUrl:h.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 pe(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 pe(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 ge,dirname as Be}from"path";import{fileURLToPath as Ke}from"url";import{access as Ve}from"fs/promises";import Ze from"@fastify/static";var Qe=Be(Ke(import.meta.url));function de(){return ge(Qe,"../public")}async function Xe(){try{return await Ve(ge(de(),"index.html")),!0}catch{return!1}}async function he(t){let e=de();return await Xe()?(await t.register(Ze,{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 me(t){E.add(t);let e=f(),o=e.getAllSessions();for(let s of o)H(t,{type:"request_update",session:s});let n=e.subscribe(s=>{H(t,s)});t.on("message",s=>{try{let a=JSON.parse(s.toString());Ye(t,a)}catch{}}),t.on("close",()=>{E.delete(t),n()}),t.on("error",()=>{E.delete(t),n()})}function Ye(t,e){let o=B.safeParse(e);if(!o.success){if(typeof e=="object"&&e!==null&&e.type==="ping"){H(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 H(t,e){if(t.readyState===t.OPEN)try{t.send(JSON.stringify(e))}catch{E.delete(t)}}async function fe(t){let e=t.port,o=t.host;J({cache:t.cache,intervene:t.intervene}),await ce(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 st({port:e});n!==e&&console.log(` Port ${e} in use, using ${n}`);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},a=>{me(a)}),await s.register(ot,{prefix:"/api/trpc",trpcOptions:{router:K,createContext:W}}),ue(s,{upstream:t.upstream,cachePath:t.cachePath,cache:t.cache}),t.headless||await he(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 rt}from"fs/promises";import{existsSync as at}from"fs";import{join as ye}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"],ut={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 pt(t){let e=t.replace(/\/\*[\s\S]*?\*\//g,"");return e=e.replace(/(?<!["'])\/\/.*$/gm,""),e}async function gt(t){for(let e of ct){let o=ye(t,e);if(at(o))try{let s=await it(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:A.parse(a),filename:e}}catch(n){console.warn(` Warning: Error loading ${e}:`,n.message)}}return null}async function dt(t){for(let e of lt){let o=ye(t,e);try{let n=await rt(o,"utf-8"),s=JSON.parse(pt(n));return{config:A.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 ht(t){let e=await gt(t);if(e)return console.log(` Config loaded from ${e.filename}`),e.config;let o=await dt(t);return o?(console.log(` Config loaded from ${o.filename}`),o.config):{}}async function be(t={}){let e=process.cwd(),o=await ht(e),n={...ut,...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 ke="1.0.0";C.name("playingpack").description("Chrome DevTools for AI Agents - Debug and test your AI agent LLM calls").version(ke);C.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 be({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 fe(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 mt(s)),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)}});C.command("version").description("Show version").action(()=>{console.log(`playingpack v${ke}`)});process.argv.length===2?C.parse(["node","playingpack","start"]):C.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playingpack",
3
- "version": "0.3.2",
3
+ "version": "0.5.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": {
@@ -30,7 +30,6 @@
30
30
  },
31
31
  "author": "Geoptly Intelligence Inc.",
32
32
  "dependencies": {
33
- "jiti": "^2.4.2",
34
33
  "@fastify/cors": "^9.0.1",
35
34
  "@fastify/static": "^7.0.1",
36
35
  "@fastify/websocket": "^10.0.1",
@@ -39,17 +38,21 @@
39
38
  "eventsource-parser": "^1.1.2",
40
39
  "fastify": "^4.26.0",
41
40
  "get-port": "^7.0.0",
41
+ "jiti": "^2.4.2",
42
+ "open": "^11.0.0",
42
43
  "ws": "^8.16.0",
43
44
  "zod": "^3.22.4"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@types/node": "^20.11.0",
47
48
  "@types/ws": "^8.5.10",
49
+ "@vitest/coverage-v8": "^1.2.0",
48
50
  "tsup": "^8.0.1",
49
51
  "tsx": "^4.7.0",
50
52
  "typescript": "^5.3.3",
51
- "@vitest/coverage-v8": "^1.2.0",
52
- "vitest": "^1.2.0"
53
+ "vite-node": "^5.2.0",
54
+ "vitest": "^1.2.0",
55
+ "@playingpack/shared": "1.0.0"
53
56
  },
54
57
  "engines": {
55
58
  "node": ">=20.0.0"
@@ -69,7 +72,7 @@
69
72
  "registry": "https://registry.npmjs.org/"
70
73
  },
71
74
  "scripts": {
72
- "dev": "tsx watch src/index.ts",
75
+ "dev": "vite-node --watch src/index.ts",
73
76
  "build": "tsup",
74
77
  "start": "node dist/index.js",
75
78
  "test": "vitest",
@@ -0,0 +1 @@
1
+ @import"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap";*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,SF Mono,Monaco,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.mt-1{margin-top:.25rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.flex{display:flex}.inline-flex{display:inline-flex}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3\.5{height:.875rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-48{max-height:12rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-3\.5{width:.875rem}.w-72{width:18rem}.w-8{width:2rem}.w-full{width:100%}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-6{gap:1.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-amber-500\/30{border-color:#f59e0b4d}.border-amber-900\/50{border-color:#78350f80}.border-blue-500\/30{border-color:#3b82f64d}.border-blue-900\/50{border-color:#1e3a8a80}.border-gray-500\/30{border-color:#6b72804d}.border-green-500\/30{border-color:#22c55e4d}.border-orange-500\/30{border-color:#f973164d}.border-orange-900\/50{border-color:#7c2d1280}.border-pp-light{--tw-border-opacity: 1;border-color:rgb(42 42 42 / var(--tw-border-opacity, 1))}.border-purple-500\/20{border-color:#a855f733}.border-teal-500\/30{border-color:#14b8a64d}.border-l-amber-500{--tw-border-opacity: 1;border-left-color:rgb(245 158 11 / var(--tw-border-opacity, 1))}.border-l-blue-500{--tw-border-opacity: 1;border-left-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-l-orange-500{--tw-border-opacity: 1;border-left-color:rgb(249 115 22 / var(--tw-border-opacity, 1))}.border-l-transparent{border-left-color:transparent}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-900\/50{background-color:#78350f80}.bg-amber-950\/30{background-color:#451a034d}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-900\/50{background-color:#1e3a8a80}.bg-blue-950\/30{background-color:#1725544d}.bg-gray-500\/10{background-color:#6b72801a}.bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-green-900\/50{background-color:#14532d80}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-orange-500\/10{background-color:#f973161a}.bg-orange-600{--tw-bg-opacity: 1;background-color:rgb(234 88 12 / var(--tw-bg-opacity, 1))}.bg-orange-900\/50{background-color:#7c2d1280}.bg-orange-950\/30{background-color:#4314074d}.bg-pp-dark{--tw-bg-opacity: 1;background-color:rgb(15 15 15 / var(--tw-bg-opacity, 1))}.bg-pp-darker{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-pp-gray{--tw-bg-opacity: 1;background-color:rgb(26 26 26 / var(--tw-bg-opacity, 1))}.bg-purple-500\/10{background-color:#a855f71a}.bg-purple-600{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-900\/50{background-color:#7f1d1d80}.bg-teal-500\/10{background-color:#14b8a61a}.bg-teal-600{--tw-bg-opacity: 1;background-color:rgb(13 148 136 / var(--tw-bg-opacity, 1))}.bg-teal-900\/50{background-color:#134e4a80}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-center{text-align:center}.font-mono{font-family:JetBrains Mono,SF Mono,Monaco,monospace}.font-sans{font-family:Inter,system-ui,sans-serif}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-wide{letter-spacing:.025em}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-orange-400{--tw-text-opacity: 1;color:rgb(251 146 60 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-teal-400{--tw-text-opacity: 1;color:rgb(45 212 191 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-50{opacity:.5}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{font-size:14px;line-height:1.5;font-weight:400;color:#e0e0e0;background-color:#0f0f0f;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;min-height:100vh}#root{min-height:100vh}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#1a1a1a}::-webkit-scrollbar-thumb{background:#3a3a3a;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#4a4a4a}.json-key{color:#7dd3fc}.json-string{color:#86efac}.json-number{color:#fbbf24}.json-boolean{color:#c084fc}.json-null{color:#94a3b8}.status-pending{color:#fbbf24}.status-success{color:#22c55e}.status-error{color:#ef4444}.status-paused{color:#3b82f6}@keyframes pulse-border{0%,to{border-color:#3b82f680}50%{border-color:#3b82f6}}.paused-border{animation:pulse-border 2s ease-in-out infinite}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:bg-pp-gray\/50:hover{background-color:#1a1a1a80}.hover\:bg-purple-700:hover{--tw-bg-opacity: 1;background-color:rgb(126 34 206 / var(--tw-bg-opacity, 1))}.hover\:bg-teal-700:hover{--tw-bg-opacity: 1;background-color:rgb(15 118 110 / var(--tw-bg-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.focus\:border-purple-500:focus{--tw-border-opacity: 1;border-color:rgb(168 85 247 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}