framewatch 0.6.0

Event-driven, change-triggered window capture that emits timestamped screenshots + metadata for AI agents.
{
  "tool": "framewatch",
  "version": "0.6.0",
  "description": "Event-driven, change-triggered single-window capture that emits timestamped screenshots + metadata for AI agents; plus a `record` mode that bundles a narrated screen recording + timestamped transcript into an LLM package.",
  "docs": "docs/AGENT_INTEGRATION.md",
  "codex": {
    "skill": ".agents/skills/framewatch/SKILL.md",
    "invoke": "$framewatch",
    "discovery": "Codex discovers this skill automatically in the Framewatch checkout. Copy .agents/skills/framewatch into another repository, or ~/.agents/skills/framewatch for user-wide use. Ensure a current framewatch.exe is on PATH; the skill verifies its version and required subcommand before capture."
  },
  "sample_session": "dist/sample-session",
  "sample_recording": "dist/sample-recording",
  "binary": {
    "relative_path": "dist/framewatch.exe",
    "path_note": "Paths are relative to the repository root. Resolve relative_path against your checkout to get an absolute path.",
    "platform": "windows-x86_64",
    "features": ["cli", "wgc", "gui", "record"],
    "version_command": "framewatch.exe --version"
  },
  "commands": [
    {
      "name": "windows",
      "summary": "List capturable windows.",
      "invoke": "framewatch.exe windows",
      "blocking": false,
      "stdout": "table with columns: HWND, EXE, TITLE"
    },
    {
      "name": "watch",
      "summary": "Capture a window into a session directory until stopped.",
      "invoke": "framewatch.exe watch <selector> [--out <dir>] [--settle-ms <n>] [--value-sample-ms <n>] [--config <file>]",
      "blocking": true,
      "stop": "Ctrl+C, or the target window closes",
      "selectors": {
        "--title": "case-insensitive substring matched against the window title (literal, not a regex)",
        "--exe": "executable basename, e.g. Code.exe (case-insensitive)",
        "--hwnd": "exact numeric window handle",
        "--pid": "exact: the window owned by this process id (avoids stale-window matches on repeat captures of the same exe)"
      },
      "options": {
        "--out": "output parent directory (default ./.framewatch)",
        "--settle-ms": "quiescence ms before 'settled' (default 350)",
        "--value-sample-ms": "throttle ms for volatile samples (default 1000)",
        "--roi": "crop capture + detection + output to a pixel region X,Y,W,H (clips host chrome); window.rect still reports the full source window",
        "--config": "path to a framewatch.toml base config (flags override it)"
      },
      "lifecycle_options": {
        "--wait": "poll/retry up to N seconds for the target window to appear (no launch-order race)",
        "--until-settled": "exit after the first settled frame (deterministic one-shot)",
        "--duration": "exit after N seconds (time-bounded; also stops an idle window)",
        "--frames": "exit after N images have been saved"
      },
      "one_shot_example": "framewatch.exe watch --title \"My App\" --wait 15 --until-settled --out ./.framewatch",
      "stdout_first_line": "framewatch: writing session to <out>/<session_id>"
    },
    {
      "name": "shot",
      "summary": "One settled frame to a single chosen file; optionally launch+capture+kill a program. Best for scripted/batch capture.",
      "invoke": "framewatch.exe shot (--launch \"<cmd>\" | <selector>) --out-file <path> [--timeout <secs>] [--settle-ms <n>] [--roi X,Y,W,H] [--settle-best-effort]",
      "blocking": true,
      "selectors": {
        "--launch": "spawn this program and capture its window by PID, then kill it on exit (whitespace-split; \"...\" groups an arg)",
        "--pid": "capture the window owned by this process id (exact)",
        "--title": "case-insensitive substring matched against the window title (literal, not a regex)",
        "--exe": "executable basename",
        "--hwnd": "exact numeric window handle"
      },
      "options": {
        "--out-file": "exact PNG path to write (required, deterministic)",
        "--timeout": "overall budget seconds to wait for window + settle (default 20)",
        "--settle-ms": "quiescence ms before 'settled'",
        "--roi": "crop region X,Y,W,H",
        "--settle-best-effort": "if nothing settles before timeout, write the latest frame anyway"
      },
      "contract": "On success prints the written path to stdout and exits 0. If no frame settles before --timeout (and --settle-best-effort is unset), writes nothing and exits non-zero (3).",
      "example": "framewatch.exe shot --launch \"game.exe --freecam\" --out-file shot.png --timeout 25"
    },
    {
      "name": "record",
      "summary": "Continuously record a window to video while narrating into the mic, transcribe locally with automatically provisioned managed Whisper, and write an LLM package (video + timestamped transcript + prompt). Requires the `record` feature and ffmpeg on PATH; first transcription downloads about 150 MiB into the user cache.",
      "invoke": "framewatch.exe record <selector|--launch \"<cmd>\"> [--out <dir>] [--duration <secs>] [--fps <n>] [--mic <name>] [--roi X,Y,W,H] [--transcribe-cmd \"<cmd>\" | --no-transcribe | --no-audio]",
      "blocking": true,
      "stop": "Ctrl+C (mp4 is finalized cleanly), or after --duration",
      "selectors": {
        "--title": "case-insensitive substring matched against the window title (literal, not a regex)",
        "--exe": "executable basename",
        "--hwnd": "exact numeric window handle",
        "--pid": "exact: the window owned by this process id",
        "--launch": "spawn this program and record its window by PID, then kill it on stop"
      },
      "options": {
        "--out": "output parent directory (a per-recording subdir is created)",
        "--duration": "auto-stop after N seconds measured from encoder startup, after target resolution and the first frame (otherwise record until Ctrl+C)",
        "--fps": "target video frames per second (default 30, clamped 1..=60)",
        "--mic": "microphone device name substring (default: system default input)",
        "--no-audio": "record video only (no microphone / transcription); also the automatic fallback when no input device is available",
        "--roi": "crop the recorded region to a pixel rect X,Y,W,H",
        "--wait": "wait up to N seconds for the target window to appear; --launch defaults to 15 seconds when neither this option nor a config wait is set",
        "--transcribe-cmd": "override managed Whisper by shelling out to a local transcriber; {audio}/{output} substituted; command emits framewatch JSON or SRT",
        "--no-transcribe": "record video + audio only (empty transcript); skips managed Whisper setup/download"
      },
      "managed_transcription": {
        "engine": "whisper.cpp",
        "version": "1.9.2",
        "model": "base.en",
        "first_use_download_bytes_approx": 157000000,
        "cache_override_env": "FRAMEWATCH_WHISPER_DIR",
        "runtime_sha256": "49dcc16de826f20bd53d44f947a1ae49dfa81f86cad67a64d80820cb192d674a",
        "model_sha256": "a03779c86df3323075f5e796cb2ce5029f00ec8869eee3fdfb897afe36c6d002"
      },
      "stdout": "first line: the package directory; last line: the PROMPT.md path",
      "example": "framewatch.exe record --title \"My Game\" --duration 60"
    },
    {
      "name": "transcriber setup",
      "summary": "Pre-download, checksum-verify, and validate the managed whisper.cpp runtime and base.en model. Optional because record performs the same setup automatically on first use.",
      "invoke": "framewatch.exe transcriber setup",
      "blocking": true,
      "stdout": "absolute whisper_cli and model paths"
    },
    {
      "name": "gui",
      "summary": "Launch the GUI window picker / ROI editor (for humans).",
      "invoke": "framewatch.exe gui [--config <file>]",
      "blocking": true
    }
  ],
  "output": {
    "session_dir": "<out_dir>/<session_id>",
    "session_id_format": "%Y-%m-%dT%H-%M-%S_<exe-stem>",
    "default_out_dir": "./.framewatch",
    "files": {
      "manifest": "session.json",
      "timeline": "timeline.jsonl",
      "agent_readme": "README_FOR_AGENT.md",
      "frames": "frames/<seq:06>_<kind>.png"
    },
    "timeline_format": "JSON Lines (one event object per line, chronological)",
    "event_kinds": [
      "initial",
      "transition_start",
      "busy_start",
      "busy_end",
      "value_sample",
      "settled",
      "manual"
    ],
    "images_saved_for_default": ["initial", "settled", "busy_end", "manual"],
    "open_images_for": ["settled", "busy_end"],
    "timeline_event_fields": {
      "seq": "u64 monotonic event index",
      "id": "string 'f' + zero-padded seq",
      "kind": "one of event_kinds",
      "wall_time": "RFC3339 UTC timestamp",
      "elapsed_ms": "u64 ms since session start",
      "image": "string path relative to session dir, or null",
      "window": "{ title, exe, class, hwnd, rect:[x,y,w,h], dpi, foreground }",
      "change": "{ changed_tiles, tile_grid:[cols,rows], area_ratio, bboxes:[[x,y,w,h]], dhash?, hamming_to_prev_emit? }",
      "busy": "{ active, regions:[{ label, active }] }",
      "timing": "{ since_prev_emit_ms?, active_for_ms?, quiescent_for_ms? }",
      "coalesced_frames": "u32 frames collapsed since previous saved image",
      "note": "human-readable summary"
    }
  },
  "consume_algorithm": [
    "Read session.json for target app, time range, config, and ROI hints.",
    "Stream timeline.jsonl line by line (each line is a complete JSON object).",
    "Open images only for events with kind == 'settled' or kind == 'busy_end'.",
    "Use coalesced_frames to know how much activity each saved image represents.",
    "Use busy_start / value_sample notes for timing context without opening images."
  ],
  "recording_output": {
    "note": "Produced by the `record` command (see docs section 6 + dist/sample-recording).",
    "package_dir": "<out_dir>/<session_id>",
    "files": {
      "prompt": "PROMPT.md (start here; transcript embedded inline)",
      "video": "recording.mp4 (window video; narration muxed in)",
      "audio": "audio.wav (raw mic narration)",
      "transcript_json": "transcript.json",
      "transcript_srt": "transcript.srt",
      "manifest": "recording.json",
      "agent_readme": "README_FOR_AGENT.md"
    },
    "transcript_json_shape": "{ language?, duration_ms, segments: [{ start_ms, end_ms, text }] }",
    "timestamp_meaning": "segment start_ms/end_ms are ms from the start of recording.mp4 — seek the video to a quoted instruction, e.g. ffmpeg -ss <start_ms/1000> -i recording.mp4 -frames:v 1 frame.png",
    "consume_algorithm": [
      "Read PROMPT.md — it is self-contained (transcript inline) for text-only models.",
      "If you can ingest video, open recording.mp4 and use each segment's start_ms to locate the on-screen moment an instruction refers to.",
      "recording.json records the engine used and video/audio metadata."
    ]
  },
  "library": {
    "crate": "framewatch",
    "cargo_dependency": "framewatch = { path = \"../framewatch\", default-features = false }  # or version = \"0.1\" once published",
    "live_capture_features": ["wgc"],
    "entry_points": [
      "framewatch::watch(config, sink)",
      "framewatch::record_with_duration(config, duration)",
      "framewatch::default_backend(&config)",
      "framewatch::enumerate_windows()",
      "framewatch::Engine::process(&frame, now)",
      "framewatch::Config::builder()",
      "framewatch::DirectorySink::new(&config)"
    ]
  },
  "platform_notes": "Live capture (windows/watch commands and default_backend) requires Windows with the wgc feature. The pure detection engine and sinks compile and run on all platforms."
}