Pew Pew Plx Hero

Pew Pew Plx

License Dart GitHub Stars GitHub Issues

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

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 credentials
  • agent: Agent configuration
  • create_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:

  1. Watches project files and reports changes as JSON
  2. Accepts JSON commands on stdin and responds on stdout
  3. 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:

  1. Starts watching the current directory for file changes
  2. Reads JSON lines from stdin
  3. 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:

  1. StdinRequestRouter parses the JSON and sees type: "agent.run".
  2. It calls AgentRunHandler.handleRun().
  3. AgentRunHandler validates that cwd exists and is a directory, generates a run_id if you didn't provide one, then calls the ClaudeCodeBackend to run the agent.
  4. ClaudeCodeBackend spawns claude -p "Say hi" --output-format stream-json in the given cwd, reads the subprocess stdout line-by-line, parses the JSON, and converts it into AgentEventDto objects that it yields as a stream.
  5. AgentRunHandler receives each event and pushes it to the AgentEventBus.
  6. 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"}:

  1. StdinRequestRouter sees type: "agent.sessions.list".
  2. It calls SessionStore.listSessions().
  3. 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.
  4. 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

  1. plx is a JSON-over-stdin/stdout CLI for file watching and agent control.
  2. StdinRequestRouter routes each JSON line by type to the right handler.
  3. AgentEventBus is a pub/sub for agent events; one subscriber writes them to stdout.
  4. ClaudeCodeBackend spawns the claude CLI and converts its output into events.
  5. SessionStore reads Claude's session files from disk.
  6. 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.

Libraries

pew_pew_plx