playingpack 0.3.1 → 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 +74 -73
- package/dist/index.js +4 -4
- package/package.json +17 -15
- package/public/assets/index-B5GdntfX.css +1 -0
- package/public/assets/index-DB6kGqLF.js +66 -0
- package/public/index.html +2 -2
- package/public/assets/index-C93ohr4T.css +0 -1
- package/public/assets/index-CXP13x6_.js +0 -89
package/README.md
CHANGED
|
@@ -26,27 +26,28 @@ PlayingPack solves these problems:
|
|
|
26
26
|
|
|
27
27
|
| Problem | Solution |
|
|
28
28
|
|---------|----------|
|
|
29
|
-
| Expensive iteration | **
|
|
30
|
-
| Non-deterministic tests | **
|
|
31
|
-
| Blind debugging | **
|
|
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 | **
|
|
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
|
-
###
|
|
40
|
-
Record API responses and replay them deterministically. First request hits the real API and saves
|
|
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
|
-
###
|
|
46
|
-
Pause requests
|
|
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
|
-
|
|
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/
|
|
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 --
|
|
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
|
|
171
|
+
Enable intervene mode to pause requests and inspect what your agent is doing:
|
|
171
172
|
|
|
172
|
-
1. Start PlayingPack
|
|
173
|
-
2.
|
|
174
|
-
3.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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 --
|
|
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
|
|
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 --
|
|
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
|
-
//
|
|
239
|
-
|
|
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
|
-
//
|
|
242
|
-
|
|
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
|
-
//
|
|
245
|
-
|
|
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:
|
|
281
|
-
|
|
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
|
-
| `--
|
|
316
|
-
| `--
|
|
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:
|
|
325
|
-
npx playingpack start --no-ui --
|
|
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
|
|
328
|
-
npx playingpack start --port 8080 --
|
|
329
|
+
# Custom port and cache directory
|
|
330
|
+
npx playingpack start --port 8080 --cache-path ./test/fixtures/cache
|
|
329
331
|
|
|
330
|
-
#
|
|
331
|
-
npx playingpack start --
|
|
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. **
|
|
355
|
-
4. **
|
|
356
|
-
5. **
|
|
357
|
-
6. **Response complete** → Save
|
|
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
|
-
###
|
|
361
|
+
### Simple Mental Model
|
|
360
362
|
|
|
361
363
|
```
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
###
|
|
375
|
+
### Cache Format
|
|
375
376
|
|
|
376
|
-
|
|
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
|
|
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
|
-
│ │ ├──
|
|
463
|
-
│ │ ├──
|
|
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
|
|
484
|
-
A: Delete the relevant
|
|
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.
|
|
488
|
+
A: Yes. PlayingPack fully supports OpenAI's function calling and tool use.
|
|
488
489
|
|
|
489
|
-
**Q: Can I share
|
|
490
|
-
A: Yes. Commit your `.playingpack/
|
|
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,"INTERCEPT"),t.toolCalls.length>0){let o=t.toolCalls[0];this.emit({type:"intercept",requestId:e,toolCall:{name:o?.name||"unknown",arguments:o?.arguments||"{}"},status:"paused"})}return new Promise(o=>{this.pendingResolvers.set(e,{resolve:s=>{let r=this.pendingResolvers.get(e);this.pendingResolvers.delete(e),o({action:s,mockContent:r?.mockContent})}})})}allowRequest(e){let t=this.pendingResolvers.get(e);return t?(this.updateState(e,"FLUSH"),t.resolve("allow"),!0):!1}mockRequest(e,t){let o=this.pendingResolvers.get(e);return o?(o.mockContent=t,this.updateState(e,"INJECT"),o.resolve("mock"),!0):!1}shouldPreIntercept(e){return this.settings.pause!=="off"}async preIntercept(e,t){let o=this.sessions.get(e);if(!o)throw new Error(`Session ${e} not found`);this.updateState(e,"PRE_INTERCEPT");let s=o.body,r=Array.isArray(s?.messages)?s.messages:[];return this.emit({type:"pre_intercept",requestId:e,request:{model:o.model||"unknown",messages:r},hasCachedResponse:t,status:"paused"}),new Promise(i=>{this.preInterceptResolvers.set(e,{resolve:i})})}preInterceptAllow(e){let t=this.preInterceptResolvers.get(e);return t?(this.preInterceptResolvers.delete(e),t.resolve({action:"allow"}),!0):!1}preInterceptEdit(e,t){let o=this.preInterceptResolvers.get(e);if(o){this.preInterceptResolvers.delete(e);let s=this.sessions.get(e);return s&&(s.body=t,this.emit({type:"request_update",session:s})),o.resolve({action:"edit",editedBody:t}),!0}return!1}preInterceptUseCache(e){let t=this.preInterceptResolvers.get(e);return t?(this.preInterceptResolvers.delete(e),t.resolve({action:"use_cache"}),!0):!1}preInterceptMock(e,t){let o=this.preInterceptResolvers.get(e);return o?(this.preInterceptResolvers.delete(e),this.updateState(e,"INJECT"),o.resolve({action:"mock",mockContent:t}),!0):!1}getSettings(){return{...this.settings}}updateSettings(e){this.settings={...this.settings,...e}}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}emit(e){for(let t of this.listeners)try{t(e)}catch{}}cleanup(){let e=Array.from(this.sessions.entries());if(e.length>100){let t=e.filter(([o,s])=>s.state==="COMPLETE"||s.state==="ERROR").slice(0,e.length-100);for(let[o]of t)this.sessions.delete(o)}}removeSession(e){this.sessions.delete(e),this.pendingResolvers.delete(e),this.preInterceptResolvers.delete(e)}},b=null;function q(n){return b=new C(n),b}function h(){return b||(b=new C),b}function F(n){return{sessionManager:h(),req:n?.req,res:n?.res}}import{initTRPC as xe}from"@trpc/server";import{z as B}from"zod";import{z as a}from"zod";var bt=a.object({model:a.string(),messages:a.array(a.object({role:a.enum(["system","user","assistant","tool"]),content:a.union([a.string(),a.null(),a.array(a.any())]).optional(),name:a.string().optional(),tool_calls:a.array(a.any()).optional(),tool_call_id:a.string().optional()})),temperature:a.number().optional(),top_p:a.number().optional(),n:a.number().optional(),stream:a.boolean().optional(),stop:a.union([a.string(),a.array(a.string())]).optional(),max_tokens:a.number().optional(),presence_penalty:a.number().optional(),frequency_penalty:a.number().optional(),logit_bias:a.record(a.number()).optional(),user:a.string().optional(),tools:a.array(a.object({type:a.literal("function"),function:a.object({name:a.string(),description:a.string().optional(),parameters:a.any().optional()})})).optional(),tool_choice:a.union([a.literal("none"),a.literal("auto"),a.literal("required"),a.object({type:a.literal("function"),function:a.object({name:a.string()})})]).optional(),response_format:a.object({type:a.enum(["text","json_object"])}).optional(),seed:a.number().optional()}).passthrough(),$=a.object({requestId:a.string(),type:a.enum(["text","error","tool_result"]),content:a.string()}),L=a.object({requestId:a.string()}),J=a.object({requestId:a.string()}),H=a.object({requestId:a.string(),editedBody:a.record(a.unknown())}),W=a.object({requestId:a.string()}),G=a.object({requestId:a.string(),mockContent:a.string()}),we=a.object({pause:a.enum(["off","tool-calls","all"])}),z=a.object({settings:we.partial()}),ve=a.object({c:a.string(),d:a.number()}),Ce=a.object({id:a.string(),hash:a.string(),timestamp:a.string(),model:a.string(),endpoint:a.string()}),St=a.object({meta:Ce,request:a.object({body:a.unknown()}),response:a.object({status:a.number(),chunks:a.array(ve)})}),Re=a.enum(["auto","off","replay-only"]),Pe=a.enum(["off","tool-calls","all"]),x=a.object({upstream:a.string().url().optional(),tapesDir:a.string().optional(),logsDir:a.string().optional(),record:Re.optional(),headless:a.boolean().optional(),port:a.number().int().min(1).max(65535).optional(),host:a.string().optional(),pause:Pe.optional()});var m=xe.context().create(),K=m.router({getSessions:m.procedure.query(({ctx:n})=>({sessions:n.sessionManager.getAllSessions()})),getSession:m.procedure.input(B.object({id:B.string()})).query(({ctx:n,input:e})=>({session:n.sessionManager.getSession(e.id)||null})),getSettings:m.procedure.query(({ctx:n})=>({settings:n.sessionManager.getSettings()})),updateSettings:m.procedure.input(z).mutation(({ctx:n,input:e})=>(n.sessionManager.updateSettings(e.settings),{settings:n.sessionManager.getSettings()})),allowRequest:m.procedure.input(L).mutation(({ctx:n,input:e})=>({success:n.sessionManager.allowRequest(e.requestId)})),mockRequest:m.procedure.input($).mutation(({ctx:n,input:e})=>({success:n.sessionManager.mockRequest(e.requestId,e.content)})),preInterceptAllow:m.procedure.input(J).mutation(({ctx:n,input:e})=>({success:n.sessionManager.preInterceptAllow(e.requestId)})),preInterceptEdit:m.procedure.input(H).mutation(({ctx:n,input:e})=>({success:n.sessionManager.preInterceptEdit(e.requestId,e.editedBody)})),preInterceptUseCache:m.procedure.input(W).mutation(({ctx:n,input:e})=>({success:n.sessionManager.preInterceptUseCache(e.requestId)})),preInterceptMock:m.procedure.input(G).mutation(({ctx:n,input:e})=>({success:n.sessionManager.preInterceptMock(e.requestId,e.mockContent)})),health:m.procedure.query(()=>({status:"ok",timestamp:new Date().toISOString()}))});import{createParser as Ie}from"eventsource-parser";var I=class{callbacks;toolCalls=new Map;accumulatedContent="";parser;constructor(e={}){this.callbacks=e,this.parser=Ie(t=>{t.type==="event"&&this.handleEvent(t.data)})}feed(e){this.parser.feed(e)}getContent(){return this.accumulatedContent}getToolCalls(){return Array.from(this.toolCalls.values())}hasToolCalls(){return this.toolCalls.size>0}getAssembledMessage(){let e=this.getToolCalls();return e.length>0?{role:"assistant",content:null,tool_calls:e.map(t=>({id:t.id,type:"function",function:{name:t.name,arguments:t.arguments}}))}:{role:"assistant",content:this.accumulatedContent||null}}reset(){this.toolCalls.clear(),this.accumulatedContent="",this.parser.reset()}handleEvent(e){if(e==="[DONE]"){this.callbacks.onDone?.();return}try{let t=JSON.parse(e);for(let o of t.choices){let s=o.delta;if(s.content&&(this.accumulatedContent+=s.content,this.callbacks.onContent?.(s.content)),s.tool_calls)for(let r of s.tool_calls){let i=this.toolCalls.get(r.index);if(i)r.function?.arguments&&(i.arguments+=r.function.arguments,this.callbacks.onToolCallUpdate?.(r.index,r.function.arguments));else{let c={index:r.index,id:r.id||"",name:r.function?.name||"",arguments:r.function?.arguments||""};this.toolCalls.set(r.index,c),this.callbacks.onToolCall?.(c)}}}}catch(t){this.callbacks.onError?.(t instanceof Error?t:new Error(String(t)))}}};function T(n){return new I(n)}var Te="https://api.openai.com";function Y(n){return!n||n.length<8?"****":`${n.substring(0,4)}...${n.substring(n.length-4)}`}function Ee(n,e=!0){let t=new Headers,o=["authorization","content-type","accept","openai-organization","openai-project","user-agent"];for(let s of o){let r=n[s];if(r){let i=Array.isArray(r)?r[0]:r;i&&t.set(s,i)}}return e?t.set("accept","text/event-stream"):t.set("accept","application/json"),t}async function E(n){let t=`${n.upstreamUrl||Te}${n.path}`,o=typeof n.body=="object"&&n.body!==null&&"stream"in n.body&&typeof n.body.stream=="boolean"?n.body.stream:!0,s=Ee(n.headers,o),r=await fetch(t,{method:n.method,headers:s,body:JSON.stringify(n.body)});return{status:r.status,headers:r.headers,body:r.body,ok:r.ok}}async function*V(n){let e=n.getReader(),t=new TextDecoder;try{for(;;){let{done:s,value:r}=await e.read();if(s)break;yield t.decode(r,{stream:!0})}let o=t.decode();o&&(yield o)}finally{e.releaseLock()}}import{mkdir as Me,writeFile as Ae}from"fs/promises";import{dirname as je,join as Oe}from"path";import{createHash as _e}from"crypto";function _(n){if(n==null)return null;if(Array.isArray(n))return n.map(_);if(typeof n=="object"){let e=n,t={},o=Object.keys(e).filter(s=>!["stream","request_id","timestamp"].includes(s)).sort();for(let s of o)t[s]=_(e[s]);return t}return n}function S(n){let e=_(n),t=JSON.stringify(e);return _e("sha256").update(t).digest("hex")}var Ne=".playingpack/tapes",w=class{tapesDir;chunks=[];lastChunkTime=0;requestBody;model="unknown";endpoint;hash="";constructor(e=Ne){this.tapesDir=e,this.endpoint="/v1/chat/completions"}start(e){if(this.requestBody=e,this.chunks=[],this.lastChunkTime=Date.now(),this.hash=S(e),typeof e=="object"&&e!==null){let t=e;typeof t.model=="string"&&(this.model=t.model)}}recordChunk(e){let t=Date.now(),o=this.chunks.length===0?0:t-this.lastChunkTime;this.chunks.push({c:e,d:o}),this.lastChunkTime=t}async save(e=200){let t={meta:{id:crypto.randomUUID(),hash:this.hash,timestamp:new Date().toISOString(),model:this.model,endpoint:this.endpoint},request:{body:this.requestBody},response:{status:e,chunks:this.chunks}},o=this.getTapePath();return await Me(je(o),{recursive:!0}),await Ae(o,JSON.stringify(t,null,2),"utf-8"),o}getTapePath(){return Oe(this.tapesDir,`${this.hash}.json`)}getHash(){return this.hash}getChunkCount(){return this.chunks.length}};import{readFile as De,access as Ue}from"fs/promises";import{join as Z}from"path";var Q=".playingpack/tapes";async function X(n,e=Q){let t=S(n),o=Z(e,`${t}.json`);try{return await Ue(o),!0}catch{return!1}}async function qe(n,e=Q){let t=S(n),o=Z(e,`${t}.json`);try{let s=await De(o,"utf-8");return JSON.parse(s)}catch{return null}}var M=class{tape;currentIndex=0;aborted=!1;constructor(e){this.tape=e}getMeta(){return this.tape.meta}getStatus(){return this.tape.response.status}abort(){this.aborted=!0}async*replay(){this.currentIndex=0,this.aborted=!1;for(let e of this.tape.response.chunks){if(this.aborted||(e.d>0&&await this.delay(e.d),this.aborted))break;yield e.c,this.currentIndex++}}async*replayFast(){for(let e of this.tape.response.chunks){if(this.aborted)break;yield e.c}}delay(e){return new Promise(t=>setTimeout(t,e))}};async function ee(n,e){let t=await qe(n,e);return t?new M(t):null}function A(){return`chatcmpl-mock-${Date.now()}`}function oe(n,e=4){let t=[];for(let o=0;o<n.length;o+=e)t.push(n.slice(o,o+e));return t}function k(n){return`data: ${n}
|
|
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
|
|
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 Fe(O,Je(o))}catch{}}var f={info:(n,e)=>R("info",n,e),warn:(n,e)=>R("warn",n,e),error:(n,e)=>R("error",n,e),debug:(n,e)=>R("debug",n,e)};var y={upstream:"https://api.openai.com",tapesDir:".playingpack/tapes",record:"auto"};function ge(n,e){e&&(y=e),n.post("/v1/chat/completions",async(t,o)=>{await He(t,o)}),n.get("/health",async(t,o)=>o.send({status:"ok"})),n.all("/v1/*",async(t,o)=>{await ze(t,o)})}async function He(n,e){let t=h(),o=crypto.randomUUID(),s=n.body,r=s.stream!==!1,i=n.headers.authorization||"";console.log(`[${o.slice(0,8)}] POST /v1/chat/completions`),console.log(` Model: ${s.model||"unknown"}`),console.log(` Stream: ${r}`),console.log(` Auth: ${Y(i.replace("Bearer ",""))}`),f.info("Request received",{requestId:o,path:"/v1/chat/completions",model:s.model,stream:r}),t.createSession(o,"POST","/v1/chat/completions",s),t.updateState(o,"LOOKUP");let c=y.record!=="off",p=y.record==="auto",l=y.record==="replay-only",d=c&&await X(s,y.tapesDir);if(t.shouldPreIntercept(o)){console.log(` [PRE-INTERCEPT] Pausing before LLM call (cache ${d?"available":"not available"})`),f.info("Pre-intercept",{requestId:o,model:s.model,hasTape:d});let u=await t.preIntercept(o,d);switch(console.log(` [PRE-ACTION] ${u.action}`),u.action){case"use_cache":if(d){console.log(" [CACHE] Using cached response"),await ue(n,e,o,s,r);return}console.log(" [WARN] Cache requested but no tape available, proceeding to upstream");break;case"mock":console.log(" [MOCK] Sending mock response"),await N(e,o,u.mockContent||"",r);return;case"edit":u.editedBody&&(Object.assign(s,u.editedBody),console.log(" [EDIT] Request body modified"));break;case"allow":break}}else if(d){console.log(" [CACHE HIT] Replaying from tape"),f.info("Cache hit",{requestId:o,model:s.model}),await ue(n,e,o,s,r);return}if(l&&!d){console.log(" [REPLAY-ONLY] No tape found, rejecting request"),t.error(o,"No tape found (replay-only mode)"),e.code(404).send({error:{message:"No recorded tape found for this request (replay-only mode)",type:"tape_not_found"}});return}console.log(" [CACHE MISS] Forwarding to upstream"),f.info("Cache miss",{requestId:o,model:s.model}),t.updateState(o,"CONNECT");try{let u=await E({method:"POST",path:"/v1/chat/completions",headers:n.headers,body:s,upstreamUrl:y.upstream});if(!u.ok||!u.body){t.error(o,`Upstream error: ${u.status}`),e.code(u.status).header("content-type","application/json").send(await D(u.body));return}r?await We(n,e,o,u,s,p):await Ge(n,e,o,u,s,p)}catch(u){let g=u instanceof Error?u.message:"Unknown error";console.error(` [ERROR] ${g}`),f.error("Request failed",{requestId:o,error:g}),t.error(o,g),e.code(500).send({error:{message:g,type:"proxy_error"}})}}async function We(n,e,t,o,s,r){let i=h(),c=r?new w(y.tapesDir):null;c?.start(s),i.updateState(t,"STREAMING");let p=T({onToolCall:g=>{i.addToolCall(t,g),console.log(` [TOOL CALL] ${g.name}`)},onContent:g=>{i.updateContent(t,g)}});if(!o.body){e.raw.end();return}let l=[],d=V(o.body);for await(let g of d)p.feed(g),c?.recordChunk(g),l.push(g);let u=p.getAssembledMessage();if(i.setRawResponse(t,JSON.stringify(u,null,2)),c)try{let g=await c.save(o.status);console.log(` [TAPE] Saved to ${g}`)}catch(g){console.error(" [TAPE ERROR] Failed to save tape:",g)}if(i.shouldIntercept(t)){console.log(" [INTERCEPT] Pausing for user action");let g=await i.intercept(t);if(console.log(` [ACTION] ${g.action}`),g.action==="mock"){await N(e,t,g.mockContent||"",!0);return}}e.code(o.status).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive");for(let g of l)e.raw.write(g);i.complete(t,o.status,!1),f.info("Request completed",{requestId:t,status:o.status,cached:!1}),e.raw.end()}async function Ge(n,e,t,o,s,r){let i=h(),c=r?new w(y.tapesDir):null;if(c?.start(s),i.updateState(t,"STREAMING"),!o.body){e.code(o.status).header("content-type","application/json").send("");return}let p=await D(o.body);c?.recordChunk(p);try{let l=JSON.parse(p);if(l.choices?.[0]?.message){let d=l.choices[0].message;if(d.content&&i.updateContent(t,d.content),d.tool_calls)for(let u of d.tool_calls)i.addToolCall(t,{index:0,id:u.id,name:u.function.name,arguments:u.function.arguments}),console.log(` [TOOL CALL] ${u.function.name}`)}i.setRawResponse(t,JSON.stringify(l,null,2))}catch{i.setRawResponse(t,p)}if(c)try{let l=await c.save(o.status);console.log(` [TAPE] Saved to ${l}`)}catch(l){console.error(" [TAPE ERROR] Failed to save tape:",l)}if(i.shouldIntercept(t)){console.log(" [INTERCEPT] Pausing for user action");let l=await i.intercept(t);if(console.log(` [ACTION] ${l.action}`),l.action==="mock"){await N(e,t,l.mockContent||"",!1);return}}e.code(o.status).header("content-type","application/json").send(p),i.complete(t,o.status,!1),f.info("Request completed",{requestId:t,status:o.status,cached:!1})}async function ue(n,e,t,o,s){let r=h();r.updateState(t,"REPLAY");let i=await ee(o,y.tapesDir);if(!i){r.error(t,"Failed to load tape"),e.code(500).send({error:{message:"Tape not found",type:"proxy_error"}});return}if(s){e.code(i.getStatus()).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive").header("x-playingpack-cached","true");let c=T({onToolCall:l=>{r.addToolCall(t,l),console.log(` [TOOL CALL] ${l.name}`)},onContent:l=>{r.updateContent(t,l)}});for await(let l of i.replay())c.feed(l),e.raw.write(l);let p=c.getAssembledMessage();r.setRawResponse(t,JSON.stringify(p,null,2)),r.complete(t,i.getStatus(),!0),f.info("Request completed",{requestId:t,status:i.getStatus(),cached:!0}),e.raw.end()}else{let c="";for await(let p of i.replay())c+=p;try{let p=JSON.parse(c);if(p.choices?.[0]?.message){let l=p.choices[0].message;if(l.content&&r.updateContent(t,l.content),l.tool_calls)for(let d of l.tool_calls)r.addToolCall(t,{index:0,id:d.id,name:d.function.name,arguments:d.function.arguments}),console.log(` [TOOL CALL] ${d.function.name}`)}r.setRawResponse(t,JSON.stringify(p,null,2))}catch{r.setRawResponse(t,c)}e.code(i.getStatus()).header("content-type","application/json").header("x-playingpack-cached","true").send(c),r.complete(t,i.getStatus(),!0),f.info("Request completed",{requestId:t,status:i.getStatus(),cached:!0})}}async function N(n,e,t,o=!0){let s=h(),r=ce(t);if(r.type==="error"){n.code(400).header("content-type","application/json").send(ae(r.content)),s.complete(e,400,!1);return}if(o){n.code(200).header("content-type","text/event-stream").header("cache-control","no-cache").header("connection","keep-alive").header("x-playingpack-mocked","true");let i=r.type==="tool_call"?re(r.functionName||"mock_function",r.content):se(r.content);for await(let c of i)n.raw.write(c);s.complete(e,200,!1),n.raw.end()}else{let i=r.type==="tool_call"?[{id:`call_mock_${Date.now()}`,type:"function",function:{name:r.functionName||"mock_function",arguments:r.content}}]:void 0,c=r.type==="text"?r.content:null,p=ie(c,i);n.code(200).header("content-type","application/json").header("x-playingpack-mocked","true").send(JSON.stringify(p)),s.complete(e,200,!1)}}async function ze(n,e){try{let t=await E({method:n.method,path:n.url,headers:n.headers,body:n.body,upstreamUrl:y.upstream});if(e.code(t.status),t.headers.forEach((o,s)=>{["content-encoding","transfer-encoding"].includes(s.toLowerCase())||e.header(s,o)}),t.body){let o=await D(t.body);e.send(o)}else e.send()}catch(t){let o=t instanceof Error?t.message:"Unknown error";e.code(500).send({error:{message:o,type:"proxy_error"}})}}async function D(n){if(!n)return"";let e=n.getReader(),t=new TextDecoder,o="";for(;;){let{done:s,value:r}=await e.read();if(s)break;o+=t.decode(r,{stream:!0})}return o}import{join as de,dirname as Be}from"path";import{fileURLToPath as Ke}from"url";import{access as Ye}from"fs/promises";import Ve from"@fastify/static";var Ze=Be(Ke(import.meta.url));function fe(){return de(Ze,"../public")}async function Qe(){try{return await Ye(de(fe(),"index.html")),!0}catch{return!1}}async function me(n){let e=fe();return await Qe()?(await n.register(Ve,{root:e,prefix:"/",wildcard:!1}),n.setNotFoundHandler((o,s)=>o.url.startsWith("/v1")||o.url.startsWith("/api")||o.url.startsWith("/ws")?s.code(404).send({error:"Not Found"}):s.sendFile("index.html")),console.log(" UI available at root path"),!0):(console.log(" UI not available (development mode)"),console.log(" Run the UI separately: cd packages/web && pnpm dev"),!1)}var P=new Set;function he(n){P.add(n);let e=h(),t=e.getAllSessions();for(let s of t)U(n,{type:"request_update",session:s});let o=e.subscribe(s=>{U(n,s)});n.on("message",s=>{try{let r=JSON.parse(s.toString());Xe(n,r)}catch{}}),n.on("close",()=>{P.delete(n),o()}),n.on("error",()=>{P.delete(n),o()})}function Xe(n,e){if(typeof e!="object"||e===null)return;let t=e,o=h();switch(t.type){case"allow":typeof t.requestId=="string"&&o.allowRequest(t.requestId);break;case"mock":typeof t.requestId=="string"&&typeof t.content=="string"&&o.mockRequest(t.requestId,t.content);break;case"pre_allow":typeof t.requestId=="string"&&o.preInterceptAllow(t.requestId);break;case"pre_edit":typeof t.requestId=="string"&&typeof t.editedBody=="object"&&o.preInterceptEdit(t.requestId,t.editedBody);break;case"pre_use_cache":typeof t.requestId=="string"&&o.preInterceptUseCache(t.requestId);break;case"pre_mock":typeof t.requestId=="string"&&typeof t.mockContent=="string"&&o.preInterceptMock(t.requestId,t.mockContent);break;case"ping":U(n,{type:"pong"});break}}function U(n,e){if(n.readyState===n.OPEN)try{n.send(JSON.stringify(e))}catch{P.delete(n)}}async function ye(n){let e=n.port,t=n.host;q({pause:n.pause}),await pe(n.logsDir),await f.info("Server starting",{upstream:n.upstream,tapesDir:n.tapesDir,record:n.record,headless:n.headless});let o=await st({port:e});o!==e&&console.log(` Port ${e} in use, using ${o}`);let s=et({logger:!1,bodyLimit:50*1024*1024});return await s.register(tt,{origin:!0,credentials:!0}),await s.register(nt),s.get("/ws",{websocket:!0},r=>{he(r)}),await s.register(ot,{prefix:"/api/trpc",trpcOptions:{router:K,createContext:F}}),ge(s,{upstream:n.upstream,tapesDir:n.tapesDir,record:n.record}),n.headless?console.log(" Running in headless mode (no UI)"):await me(s),await s.listen({port:o,host:t}),await f.info("Server listening",{port:o,host:t}),{server:s,port:o,host:t}}import{readFile as rt}from"fs/promises";import{existsSync as at}from"fs";import{join as ke}from"path";import{createJiti as it}from"jiti";var ct=["playingpack.config.ts","playingpack.config.mts","playingpack.config.js","playingpack.config.mjs"],lt=["playingpack.config.jsonc","playingpack.config.json",".playingpackrc.json",".playingpackrc"],pt={upstream:"https://api.openai.com",tapesDir:".playingpack/tapes",logsDir:".playingpack/logs",record:"auto",headless:!1,port:4747,host:"0.0.0.0",pause:"off"};function ut(n){let e=n.replace(/\/\*[\s\S]*?\*\//g,"");return e=e.replace(/(?<!["'])\/\/.*$/gm,""),e}async function gt(n){for(let e of ct){let t=ke(n,e);if(at(t))try{let s=await it(import.meta.url,{interopDefault:!0}).import(t),r=s&&typeof s=="object"&&"default"in s?s.default:s;if(!r||typeof r!="object"){console.warn(` Warning: ${e} must export a config object`);continue}return{config:x.parse(r),filename:e}}catch(o){console.warn(` Warning: Error loading ${e}:`,o.message)}}return null}async function dt(n){for(let e of lt){let t=ke(n,e);try{let o=await rt(t,"utf-8"),s=JSON.parse(ut(o));return{config:x.parse(s),filename:e}}catch(o){o.code!=="ENOENT"&&(o instanceof SyntaxError||o.name==="ZodError")&&console.warn(` Warning: Invalid config in ${e}:`,o.message)}}return null}async function ft(n){let e=await gt(n);if(e)return console.log(` Config loaded from ${e.filename}`),e.config;let t=await dt(n);return t?(console.log(` Config loaded from ${t.filename}`),t.config):{}}async function be(n={}){let e=process.cwd(),t=await ft(e),o={...pt,...t};return n.port!==void 0&&(o.port=n.port),n.host!==void 0&&(o.host=n.host),n.ui!==void 0&&(o.headless=!n.ui),n.upstream!==void 0&&(o.upstream=n.upstream),n.tapesDir!==void 0&&(o.tapesDir=n.tapesDir),n.record!==void 0&&(o.record=n.record),o}var Se="1.0.0";v.name("playingpack").description("Chrome DevTools for AI Agents - Local reverse proxy and debugger").version(Se);v.command("start").description("Start the PlayingPack proxy server").option("-p, --port <port>","Port to listen on").option("-h, --host <host>","Host to bind to").option("--no-ui","Run without UI server (headless mode for CI/CD)").option("--upstream <url>","Upstream API URL (default: https://api.openai.com)").option("--tapes-dir <path>","Directory for tape storage (default: .playingpack/tapes)").option("--record <mode>","Recording mode: auto, off, replay-only (default: auto)").action(async n=>{console.log(),console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),console.log(" \u2551 \u2551"),console.log(" \u2551 \u2593\u2593\u2593\u2593 PlayingPack - The Flight Simulator \u2593\u2593\u2593\u2593 \u2551"),console.log(" \u2551 Chrome DevTools for AI Agents \u2551"),console.log(" \u2551 \u2551"),console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),console.log();try{let e=await be({port:n.port?parseInt(n.port,10):void 0,host:n.host,ui:n.ui,upstream:n.upstream,tapesDir:n.tapesDir,record:n.record}),{port:t,host:o}=await ye(e),s=`http://localhost:${t}`,r=o==="0.0.0.0"?`http://<your-ip>:${t}`:`http://${o}:${t}`;console.log(),console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),console.log(" \u2502 Server running! \u2502"),console.log(" \u2502 \u2502"),console.log(` \u2502 Local: ${s.padEnd(44)}\u2502`),console.log(` \u2502 Network: ${r.padEnd(44)}\u2502`),console.log(" \u2502 \u2502"),console.log(" \u2502 To use with your AI agent, set: \u2502"),console.log(` \u2502 baseURL = "${s}/v1"`.padEnd(60)+"\u2502"),e.headless||(console.log(" \u2502 \u2502"),console.log(" \u2502 Dashboard: Open the local URL in your browser \u2502")),console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"),console.log(),console.log(" Waiting for requests..."),console.log();let i=async()=>{console.log(`
|
|
7
|
-
Shutting down...`),process.exit(0)};process.on("SIGINT",i),process.on("SIGTERM",i)}catch(e){console.error("Failed to start server:",e),process.exit(1)}});
|
|
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
|
+
"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": {
|
|
@@ -29,18 +29,7 @@
|
|
|
29
29
|
"url": "https://github.com/playingpack/playingpack/issues"
|
|
30
30
|
},
|
|
31
31
|
"author": "Geoptly Intelligence Inc.",
|
|
32
|
-
"scripts": {
|
|
33
|
-
"dev": "tsx watch src/index.ts",
|
|
34
|
-
"build": "tsup",
|
|
35
|
-
"start": "node dist/index.js",
|
|
36
|
-
"test": "vitest",
|
|
37
|
-
"test:run": "vitest run",
|
|
38
|
-
"test:coverage": "vitest run --coverage",
|
|
39
|
-
"typecheck": "tsc --noEmit",
|
|
40
|
-
"prepublishOnly": "cd ../.. && pnpm run build:all"
|
|
41
|
-
},
|
|
42
32
|
"dependencies": {
|
|
43
|
-
"jiti": "^2.4.2",
|
|
44
33
|
"@fastify/cors": "^9.0.1",
|
|
45
34
|
"@fastify/static": "^7.0.1",
|
|
46
35
|
"@fastify/websocket": "^10.0.1",
|
|
@@ -49,17 +38,21 @@
|
|
|
49
38
|
"eventsource-parser": "^1.1.2",
|
|
50
39
|
"fastify": "^4.26.0",
|
|
51
40
|
"get-port": "^7.0.0",
|
|
41
|
+
"jiti": "^2.4.2",
|
|
42
|
+
"open": "^11.0.0",
|
|
52
43
|
"ws": "^8.16.0",
|
|
53
44
|
"zod": "^3.22.4"
|
|
54
45
|
},
|
|
55
46
|
"devDependencies": {
|
|
56
47
|
"@types/node": "^20.11.0",
|
|
57
48
|
"@types/ws": "^8.5.10",
|
|
49
|
+
"@vitest/coverage-v8": "^1.2.0",
|
|
58
50
|
"tsup": "^8.0.1",
|
|
59
51
|
"tsx": "^4.7.0",
|
|
60
52
|
"typescript": "^5.3.3",
|
|
61
|
-
"
|
|
62
|
-
"vitest": "^1.2.0"
|
|
53
|
+
"vite-node": "^5.2.0",
|
|
54
|
+
"vitest": "^1.2.0",
|
|
55
|
+
"@playingpack/shared": "1.0.0"
|
|
63
56
|
},
|
|
64
57
|
"engines": {
|
|
65
58
|
"node": ">=20.0.0"
|
|
@@ -77,5 +70,14 @@
|
|
|
77
70
|
"publishConfig": {
|
|
78
71
|
"access": "public",
|
|
79
72
|
"registry": "https://registry.npmjs.org/"
|
|
73
|
+
},
|
|
74
|
+
"scripts": {
|
|
75
|
+
"dev": "vite-node --watch src/index.ts",
|
|
76
|
+
"build": "tsup",
|
|
77
|
+
"start": "node dist/index.js",
|
|
78
|
+
"test": "vitest",
|
|
79
|
+
"test:run": "vitest run",
|
|
80
|
+
"test:coverage": "vitest run --coverage",
|
|
81
|
+
"typecheck": "tsc --noEmit"
|
|
80
82
|
}
|
|
81
|
-
}
|
|
83
|
+
}
|
|
@@ -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}
|