
Pew Pew Plx
A Dart CLI for project file monitoring and tooling automation.
Features
- Real-time file change monitoring with throttled JSON output
- Bidirectional communication via stdin/stdout JSON protocol
- File read (
get) and directory listing (list) operations - Clipboard-to-file paste via
pbpaste - File content appending with duplicate detection
- Image generation from text prompts via Gemini API (
plx create image) - List feedback markers (
plx list feedback) and PR comments (plx list comments) with markdown/JSON/YAML output - Copy feedback markers or PR comments to clipboard (
plx copy feedback,plx copy comments) - Authentication with PLX backend (
plx login,plx logout,plx show auth) - Configurable watch extensions, ignore folders, and throttle rate
- Path traversal protection and extension validation
Platform Support
| Platform | Status | Notes |
|---|---|---|
| macOS (arm64, x64) | Fully supported | Primary development platform |
| Linux (x64) | Partially supported | Clipboard commands require xclip installed |
| Windows | Not supported | — |
Table of Contents
- Platform Support
- Installation
- External Dependencies
- Commands
- Usage
- Configuration
- How It Works
- Development
- Troubleshooting
- License
Installation
Homebrew (macOS / Linux)
brew tap appboypov/tap
brew install plx
pub.dev
From the repo (path-based, for development or when using local symlinks):
dart pub global activate --source path .
When the package is published to pub.dev:
dart pub global activate pew_pew_plx
External Dependencies
These tools must be available on your PATH for full functionality:
| Tool | Required By | Install |
|---|---|---|
git |
All commands | Pre-installed on macOS, apt install git on Linux |
gh |
copy comments, list comments |
GitHub CLI |
xclip |
copy, paste (Linux only) |
apt install xclip |
claude |
watch (agent mode) |
Claude Code CLI |
Commands
| Command | Description |
|---|---|
plx watch project |
Monitor project files for changes with JSON output |
plx paste <filename> |
Write clipboard content to a file in the current directory |
plx append <source> <target> |
Append source file content to target file and delete source |
plx create image <prompt> |
Generate images from text prompts via Gemini API |
plx list feedback |
List feedback markers in source files (markdown by default) |
plx list comments |
List unresolved PR review comments (markdown by default) |
plx copy feedback |
Find feedback markers and copy categorized report to clipboard |
plx copy comments |
Fetch unresolved PR comments and copy to clipboard |
plx login |
Authenticate with the PLX backend via browser |
plx logout |
Clear stored authentication credentials |
plx show auth |
Show current authentication status |
plx init |
Ensure config schema is up to date |
plx --version |
Print the current version |
Usage
Watch Project Files
Monitor project files for changes with throttled JSON output:
plx watch project
Output (stdout)
File changes are emitted as JSON:
{"event": "create", "path": "docs/readme.md", "content": "# Hello"}
{"event": "modify", "path": "docs/readme.md", "content": "# Hello World"}
{"event": "delete", "path": "docs/readme.md", "content": null}
Input (stdin)
Send JSON requests to write or delete files:
{"event": "create", "path": "docs/new.md", "content": "# New File", "id": "req-1"}
{"event": "modify", "path": "docs/existing.md", "content": "Updated content", "id": "req-2"}
{"event": "delete", "path": "docs/old.md", "id": "req-3"}
Responses mirror the request structure:
{"event": "create", "path": "docs/new.md", "content": "# New File", "id": "req-1"}
{"event": "error", "path": "docs/invalid.txt", "content": "File extension not allowed", "id": "req-4"}
Read Operations (stdin)
Request file content or directory listings:
{"event": "get", "path": "docs/readme.md", "id": "req-5"}
{"event": "list", "path": "docs", "id": "req-6"}
Get response (single file):
{"event": "get", "path": "docs/readme.md", "content": "# Hello", "lastModified": 1737475200000, "id": "req-5"}
List response (directory contents):
{"event": "list", "path": "docs", "files": [{"path": "docs/readme.md", "content": "# Hello", "lastModified": 1737475200000}], "id": "req-6"}
Event Schema
| Field | Type | Description |
|---|---|---|
event |
string | create, modify, delete, get, list, or error |
path |
string | Relative path within watched directory |
content |
string? | File content (null for delete/list/error) |
id |
string? | Optional correlation ID (echoed in response) |
lastModified |
int? | File timestamp in milliseconds (get responses) |
files |
array? | File entries with path, content, lastModified (list responses) |
Constraints:
- Paths must be within the watched directory
- File extension must match configured extensions
- Paths in ignored folders are rejected
- Parent directories are created automatically for writes
Paste Clipboard to File
Write clipboard content to a file in the current directory using pbpaste (macOS only):
plx paste notes.md
This reads the system clipboard and writes its content to the specified file, creating parent directories if needed.
Append File Content
Append the content of a source file to a target file, then delete the source:
plx append fragment.md document.md
The command:
- Verifies both files exist
- Checks the target does not already contain the source content (duplicate detection)
- Appends source content to the end of the target
- Deletes the source file after a successful append
Create Image
Generate images from text prompts using the Gemini API:
plx create image "a red apple"
plx create image "a blue cat" --output ./my-images
plx create image "sunset over mountains" --filename landscape.png --count 4
Options:
--output,-o: Output directory (default:workspace/images)--filename,-f: Output filename or path--count,-n: Number of images to generate (1-8, default: 1)--model,-m: Gemini model override (e.g.gemini-2.0-flash-exp)
API key resolution (first found): GEMINI_API_KEY env, PLX_GEMINI_API_KEY env, or create_image.api_key in config.
List Feedback
List feedback markers (#FEEDBACK) found in source files. Default output is markdown; use -o json or -o yaml for structured output:
plx list feedback
plx list feedback -o json
plx list feedback -o yaml
Requires feedback config in .plx/config.yaml (marker, extensions, ignore_folders).
List Comments
List unresolved PR review comments from the current branch. Requires gh and a branch with an open PR. Default output is markdown:
plx list comments
plx list comments -o json
plx list comments -o yaml
Copy Feedback
Find all feedback markers (#FEEDBACK) in source files and copy a categorized report to the clipboard. Uses pbcopy (macOS only). Requires feedback config in .plx/config.yaml:
plx copy feedback
The report is printed to stdout and copied to the clipboard.
Copy Comments
Fetch unresolved PR review comments from the current branch and copy a formatted report to the clipboard. Uses pbcopy (macOS only). Requires gh and a branch with an open PR:
plx copy comments
The report is printed to stdout and copied to the clipboard.
Authentication
Authenticate with the PLX backend for features that require it:
plx login
Opens a browser for sign-in. If the browser does not open, the verification URL and code are printed. Credentials are stored locally.
Check current status:
plx show auth
Clear stored credentials:
plx logout
Configuration
Configuration is split across two files. Run plx init to merge new config keys or migrate legacy keys.
User config (~/.plx/config.yaml)
Stores personal credentials and per-user settings:
auth: Firebase authentication credentialsagent: Agent configurationcreate_image.api_key: Gemini API key (fallback if env vars not set)
Project config (.plx/config.yaml)
Stored in the project root (auto-created with defaults) and checked in per project:
watch:
throttle_ms: 1000
extensions:
- .md
ignore_folders:
- .git
- node_modules
- build
- .dart_tool
- .plx
create_image:
default_output_dir: workspace/images
default_model: gemini-2.0-flash-exp
feedback:
marker: "#FEEDBACK"
extensions: [.dart, .ts, .tsx, .js, .jsx, .kt, .swift, .java, .go]
ignore_folders: [.git, node_modules, build, .dart_tool, .plx]
| Setting | Description | Default |
|---|---|---|
throttle_ms |
Minimum milliseconds between events | 1000 |
extensions |
File extensions to watch | .md |
ignore_folders |
Folders to exclude from watching | .git, node_modules, build, .dart_tool, .plx |
create_image.default_output_dir |
Default output directory for generated images | workspace/images |
create_image.default_model |
Default Gemini model for image generation | gemini-2.0-flash-exp |
feedback.marker |
Feedback marker string to scan for | #FEEDBACK |
feedback.extensions |
File extensions to scan for feedback | .dart, .ts, .tsx, .js, .jsx, .kt, .swift, .java, .go |
feedback.ignore_folders |
Folders to exclude from feedback scan | .git, node_modules, build, .dart_tool, .plx |
How It Works
This section explains the architecture of plx watch project for developers new to the project.
What is it?
Pew Pew Plx is a Dart CLI that:
- Watches project files and reports changes as JSON
- Accepts JSON commands on stdin and responds on stdout
- Can run Claude Code agents and list their sessions
It's designed to be driven by another app (e.g. plaza) that sends JSON and reads JSON back.
The Big Picture: plx watch project
When you run plx watch project, the process:
- Starts watching the current directory for file changes
- Reads JSON lines from stdin
- Writes JSON lines to stdout
It's a long-running process that communicates via stdin/stdout using JSON.
Two Kinds of Output
There are two main output streams:
| Source | What it does |
|---|---|
| File watcher | When files change, it emits events like {"event":"create","path":"docs/new.md",...} |
| Agent system | When you run agents or list sessions, it emits events like {"type":"agent","run_id":"...","stream":"lifecycle",...} |
Both go to stdout, but they're distinguished by fields like event vs type.
How stdin Requests Are Handled
Every line from stdin is JSON. The first thing the system does is look at the type field to decide what to do:
stdin line → parse JSON → check "type" → route to the right handler
The router (StdinRequestRouter) does this branching:
type value |
Handler | What happens |
|---|---|---|
agent.run |
AgentRunHandler | Runs a Claude Code agent |
agent.sessions.list |
SessionStore | Lists Claude Code sessions |
agent.session.get |
SessionStore | Gets a session transcript |
| anything else | FileReaderService / FileWriterService | File read/write (get, list, create, modify, delete) |
So the router is a dispatcher: one JSON line in, one or more JSON lines out.
Agent Flow: agent.run
When you send:
{"type":"agent.run","prompt":"Say hi","args":["--output-format","stream-json"],"cwd":"/path/to/project"}
the flow is:
- StdinRequestRouter parses the JSON and sees
type: "agent.run". - It calls AgentRunHandler.handleRun().
- AgentRunHandler validates that
cwdexists and is a directory, generates arun_idif you didn't provide one, then calls the ClaudeCodeBackend to run the agent. - ClaudeCodeBackend spawns
claude -p "Say hi" --output-format stream-jsonin the givencwd, reads the subprocess stdout line-by-line, parses the JSON, and converts it intoAgentEventDtoobjects that it yields as a stream. - AgentRunHandler receives each event and pushes it to the AgentEventBus.
- ProjectCommand has subscribed to the bus and writes each event to stdout as
{"type":"agent","run_id":"...","stream":"lifecycle","data":{"phase":"start"},...}.
So: stdin request → handler → backend (subprocess) → event bus → stdout.
Agent Event Bus
The AgentEventBus is a simple pub/sub:
- Publish:
emitAgentEvent(event)— anyone can emit events - Subscribe:
onAgentEvent(callback)— returns an unsubscribe function
There's typically one subscriber: the one that writes agent events to stdout. That keeps the "write to stdout" logic in one place instead of scattered across handlers.
Session List: agent.sessions.list
When you send {"type":"agent.sessions.list"}:
- StdinRequestRouter sees
type: "agent.sessions.list". - It calls SessionStore.listSessions().
- SessionStore resolves the Claude data dir (env
PLX_CLAUDE_DATA_DIR, config, or~/.claude), scans~/.claude/projects/...for JSONL session files, and reads the first line of each file to build session summaries. - The router writes the response directly to stdout:
{"type":"agent.sessions.list","sessions":[{"session_id":"...","project_dir":"..."},...]}.
No event bus here — the router writes the response itself.
Dependency Injection (GetIt)
The project uses GetIt for dependency injection:
- Lazy singletons: e.g.
AgentEventBus,SessionStore— created on first use - Factories: e.g.
AgentRunHandler,StdinRequestRouter— new instance each time
Registration happens in LocatorService at startup. Commands and services get their dependencies via GetIt.I.get<SomeType>() instead of constructing them manually.
Summary Diagram
stdin (JSON lines)
│
▼
┌──────────────────────┐
│ StdinRequestRouter │
│ (parse, switch on │
│ "type") │
└──────────┬────────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
agent.run agent.sessions watch (get/list/
│ .list create/modify)
│ │ │
▼ ▼ ▼
AgentRunHandler SessionStore FileReader/
│ │ FileWriter
▼ │ │
ClaudeCodeBackend │ │
(spawn claude) │ │
│ │ │
▼ │ │
AgentEventBus ◄────────┘ │
│ │
└───────────────┬────────────────┘
│
▼
stdout (JSON lines)
Key Takeaways
- plx is a JSON-over-stdin/stdout CLI for file watching and agent control.
- StdinRequestRouter routes each JSON line by
typeto the right handler. - AgentEventBus is a pub/sub for agent events; one subscriber writes them to stdout.
- ClaudeCodeBackend spawns the
claudeCLI and converts its output into events. - SessionStore reads Claude's session files from disk.
- ProjectCommand ties everything together: file watcher, stdin loop, and bus subscriber.
Development
Local setup: This package uses path dependencies under symlinks/ that must point to the turbo_* packages (e.g. turbo_plx_cli, turbo_promptable, turbo_response, turbo_serializable). Create the symlinks if missing:
# From pew_pew_plx repo root, with turbo_packages as sibling directory:
ln -sf ../turbo_packages/turbo_plx_cli symlinks/turbo_plx_cli
ln -sf ../turbo_packages/turbo_promptable symlinks/turbo_promptable
ln -sf ../turbo_packages/turbo_response symlinks/turbo_response
ln -sf ../turbo_packages/turbo_serializable symlinks/turbo_serializable
# Then get dependencies and activate
dart pub get && dart pub global activate --source path .
# Run in development mode
dart run bin/plxdev.dart watch project
# Full CI pipeline (get, build, analyze, test)
make all
# Individual commands
make get # Get dependencies
make build # Generate code (build_runner)
make analyze # Static analysis
make test # Run tests with coverage
make format # Format code
make dev # Compile and install plxdev
make prod # Compile and install plx (requires PLX_FIREBASE_API_KEY)
# Production build requires Firebase API key
make prod PLX_FIREBASE_API_KEY=<your-key>
# Firebase Functions (contact form, auth) require RESEND_API_KEY, RESEND_FROM_EMAIL, RESEND_TO_EMAIL — see firebase/README.md
# Or compile manually:
dart compile exe bin/plx.dart -o build/plx --define=env=prod --define=PLX_FIREBASE_API_KEY=<your-key>
Troubleshooting
If you see "The current activation of pew_pew_plx cannot resolve to the same set of dependencies", ensure the four symlinks/ path dependencies exist, then from the repo root run:
rm -rf .dart_tool/pub/bin/pew_pew_plx && dart pub get && dart pub global activate --source path .
License
See LICENSE for details.