diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 74af8e4..8ccadea 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -121,6 +121,15 @@ "skills": [ "./skills/flutter-marionette-usage" ] + }, + { + "name": "flutter-read-logs", + "source": "./plugins/flutter-read-logs", + "description": "Read the running Flutter app's latest run logs as on-demand context — inspect what a flutter run already produced (incl. build errors and crashes), no live connection or instrumentation, via /read-logs.", + "skills": [ + "./skills/flutter-read-logs-usage", + "./skills/read-logs" + ] } ] } diff --git a/README.md b/README.md index d8c104f..9733f55 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ Most plugins are pure rules and skills with no setup. A few need one-time toolin - [`flutter-ui`](plugins/flutter-ui/) - design-system-driven UI, loading/error patterns, localized presentation text, and UI implementation checklists - [`flutter-patrol`](plugins/flutter-patrol/) - Patrol test architecture, key conventions, and Patrol MCP workflow for AI-assisted E2E work - [`flutter-marionette`](plugins/flutter-marionette/) - runtime interaction with a live debug app through Marionette MCP for exploration, smoke checks, and UI debugging +- [`flutter-read-logs`](plugins/flutter-read-logs/) - read the running app's latest `flutter run` logs as on-demand context via `/read-logs` ### Every plugin has a `-usage` skill diff --git a/plugins/flutter-read-logs/.claude-plugin/plugin.json b/plugins/flutter-read-logs/.claude-plugin/plugin.json new file mode 100644 index 0000000..803fcfc --- /dev/null +++ b/plugins/flutter-read-logs/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "flutter-read-logs", + "displayName": "Flutter Read Logs", + "description": "Read the running Flutter app's latest run logs as on-demand context — inspect what a flutter run already produced (including build errors and crashes), no live connection or instrumentation. Surfaces the relevant slice to Claude via /read-logs.", + "version": "0.1.0", + "author": { + "name": "LeanCode" + }, + "homepage": "https://github.com/leancodepl/ai-plugins", + "repository": "https://github.com/leancodepl/ai-plugins", + "keywords": [ + "flutter", + "logs", + "debugging", + "runtime", + "dap" + ], + "skills": "./skills/" +} diff --git a/plugins/flutter-read-logs/CHANGELOG.md b/plugins/flutter-read-logs/CHANGELOG.md new file mode 100644 index 0000000..948ac71 --- /dev/null +++ b/plugins/flutter-read-logs/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## 0.1.0 + +- Initial `flutter-read-logs` plugin. +- `skills/read-logs/SKILL.md` — capture and read the latest `flutter run` output as context, via `/read-logs`. Auto-detects Zed `script` transcripts and VS Code/Cursor `dapLogFile` (DAP) logs, reads task-led, flags stale runs, and guides first-run capture setup. +- `skills/flutter-read-logs-usage/SKILL.md` — explain what the plugin does and route to setup. diff --git a/plugins/flutter-read-logs/README.md b/plugins/flutter-read-logs/README.md new file mode 100644 index 0000000..ee7f56f --- /dev/null +++ b/plugins/flutter-read-logs/README.md @@ -0,0 +1,141 @@ +# flutter-read-logs + +LeanCode Flutter plugin that lets Claude read the running app's `flutter run` output as +on-demand context. Instead of pasting terminal logs, you run the app, then ask: + +``` +/read-logs why is the login screen stuck +``` + +Claude reads the most recent run's logs and works your task against them — it's a context +loader, not an auto-analyzer. + +## Included assets + +- `skills/read-logs/SKILL.md` — the `/read-logs` workhorse: resolves the per-project log, + auto-detects format, reads task-led, flags stale runs, and guides first-run setup. +- `skills/flutter-read-logs-usage/SKILL.md` — explains the plugin and routes to setup. + +## How it works + +Each project logs to its own file, `/tmp/flutter-.log`, where `` is derived +from the shared `.git` (`git rev-parse --git-common-dir`) — so the main checkout and every +git worktree resolve to the **same** file. The file is overwritten on every launch (always +the latest run) and lives in `/tmp` — outside the repo, nothing to gitignore, cleared on +reboot. `/read-logs` derives the path at runtime and auto-detects the format. + +The only per-developer step is making your editor write that file — **do it once.** If you +run `/read-logs` before setting it up, the skill walks you through it (and offers to apply +the change). It only edits **local** config; it never commits anything. + +## ⚠️ Security / data handling — read this first + +**Reading a run sends its contents to the model. That *is* the leak — it's how the tool +works, and it can't be prevented.** Run logs routinely contain auth/refresh/push tokens, +emails, account details, and other customer data. The file stays local in `/tmp`, but the +moment `/read-logs` reads it, that slice goes to the model. + +Treat this as a **conscious decision**: + +- **Enabling capture** (the one-time setup below) is your opt-in — the skill confirms it + with you before wiring anything up. +- **Each `/read-logs`** announces the file it's about to read before reading it. +- **Prefer test/staging data** when you'll `/read-logs`; avoid production or real-customer + runs unless you've accepted the exposure. + +If you need to read production-shaped logs, the durable mitigation is a **redaction pass** +(mask tokens/emails/keys before the model sees them) — not yet built; tracked as a possible +fast-follow. Raise it if your team wants it before adopting this widely. + +In the snippets below, replace `myapp` with your repo's folder name (it must match the path +`/read-logs` derives — i.e. `/tmp/flutter-.log`). + +## VS Code / Cursor (one-time) — keeps F5 + +Add to your `.vscode/settings.json` (gitignored in most projects, so it stays local — +correct, since the path is machine-specific): + +```json +{ + "dart.dapLogFile": "/tmp/flutter-myapp.log", + "dart.maxLogLineLength": 1000000 +} +``` + +- Keeps your normal debug workflow — **F5** and **Ctrl+F5** ("Run Without Debugging") both + capture, because both run through the debug adapter. Breakpoints intact, no terminal hacks. +- `maxLogLineLength` defaults to 2000 and truncates long lines; bump it so output and stack + traces aren't cut. +- Reload the window after adding (`Developer: Reload Window`) so Dart-Code picks it up. +- **Cursor** is a VS Code fork using the same Dart-Code extension and `.vscode/settings.json` + — identical setup. +- Do **not** use `dapLogFile`'s `${workspaceName}` variable — it expands to the workspace + folder (differs per worktree) and won't match how `/read-logs` derives the path. + +**Caveats:** + +- `dapLogFile` captures **debug-adapter sessions** (F5 / Ctrl+F5). If you launch via a plain + terminal Run Task instead, use the Zed/`script` approach below. +- App logs only reach the debug console (and the file) on devices that forward them: **iOS + simulator / Android / macOS / the `chrome` device**. A **`-d web-server`** run sends app + logs to the *browser's* DevTools console, not the editor — so the file will have + build/launch output but no app logs. +- Historical note: the old `dart.flutterRunLogFile` was **removed** from Dart-Code with the + legacy debug adapters; `dapLogFile` is its replacement. + +## Zed / terminal run task (one-time) — clean transcript + +Wrap your run task's command in `script` so output is teed to the file while keeping the +interactive console (hot reload). **`script`'s syntax differs by OS.** + +**macOS (BSD `script`)** — logfile, then the command as trailing args, in `.zed/tasks.json`: + +```json +{ + "label": "Run app", + "command": "script", + "args": ["-q", "/tmp/flutter-myapp.log", "flutter", "run", "-t", "lib/main.dart"], + "use_new_terminal": true +} +``` + +**Linux (GNU `script`)** — the command goes through `-c` as a single string, logfile **last**: + +```json +{ + "label": "Run app", + "command": "script", + "args": ["-q", "-c", "flutter run -t lib/main.dart", "/tmp/flutter-myapp.log"], + "use_new_terminal": true +} +``` + +(Wrap whatever your existing run command is — including `fvm flutter …`, flavors, and +`--dart-define`s — don't replace it.) **Windows** has no `script`; use the VS Code/Cursor +`dapLogFile` path above, which is cross-platform. + +`script` runs Flutter under a pseudo-TTY, so hot reload (`r`) / hot restart (`R`) keep +working — unlike a plain `… | tee` pipe, which makes Flutter drop to non-interactive mode. +This produces a clean text transcript (no JSON parsing needed). Web logs are captured here +too, as long as the task uses `-d chrome` (not `-d web-server`). + +## Note on duplicated setup + +These setup steps also appear **inline** in `skills/read-logs/SKILL.md`, on purpose: that +skill applies them at runtime, when the developer is in their editor and not reading this +README. This `README.md` is the canonical copy — keep the two in sync when changing setup. + +## Example usage + +- `/read-logs ` — read the latest run's logs as context for ``. +- `/flutter-read-logs-usage` — short explanation of what this plugin does and how to set it up. + +## Related plugins + +- [`flutter-marionette`](../flutter-marionette/) — the **proactive** counterpart: it drives + a *live* app (taps, navigation, hot reload, query state) via MCP, needing instrumentation + and a running connection. `flutter-read-logs` is **reactive** — it reads what a run + *already produced*, including build failures and crashed/exited runs, with no + instrumentation or live app. Rule of thumb: **marionette to *make* things happen, + read-logs to *see what happened*.** They're complementary — each catches what the other + can't (marionette: live state; read-logs: build/crash output before any connection). diff --git a/plugins/flutter-read-logs/skills/flutter-read-logs-usage/SKILL.md b/plugins/flutter-read-logs/skills/flutter-read-logs-usage/SKILL.md new file mode 100644 index 0000000..9634498 --- /dev/null +++ b/plugins/flutter-read-logs/skills/flutter-read-logs-usage/SKILL.md @@ -0,0 +1,43 @@ +--- +name: flutter-read-logs-usage +description: Explain what the `flutter-read-logs` plugin does and how to use it. Use when the user invokes `/flutter-read-logs-usage`, asks what this plugin covers, or needs help setting up run-log capture for `/read-logs`. +--- + +# Read Logs Usage + +## How to respond + +- If the user invoked this skill without a concrete task, explain what the plugin does and when to reach for `/read-logs`. +- If they need setup, point them at this plugin's `README.md` (the canonical setup home) — VS Code/Cursor `dapLogFile`, or the Zed/`script` task. +- If they already have a concrete log-reading task, briefly explain the fit and then run `/read-logs`. +- Do not reply with filler like "skill loaded" or "ready for the task" before explaining the plugin. + +## What this plugin does + +- Lets Claude read the running app's latest `flutter run` output as on-demand context, via `/read-logs ` — instead of pasting terminal logs. +- Captures `flutter run` output per-project to `/tmp/flutter-.log` (worktree-stable via `--git-common-dir`, overwritten each launch). Two formats, auto-detected: Zed `script` transcript and VS Code/Cursor `dapLogFile` (DAP). +- Reads **task-led** — reads the whole short run, or investigates a long one against your question (errors, event ordering, a feature, …) — flags stale runs, and on first use guides editor setup. + +## When to use it + +- "Why did the app crash / behave like this at runtime?" +- "Does the auth flow actually reach the API?" +- "Trace the order these events fired" (races / timing). + +## Setup + +One-time per developer — full instructions in this plugin's `README.md` (VS Code/Cursor +`dapLogFile` keeps the F5 workflow; Zed uses a `script`-wrapped run task). `/read-logs` also +walks you through setup, and offers to apply it, the first time if no log file exists yet. + +## Heads-up + +Run logs can contain tokens and customer data, and `/read-logs` sends what it reads to the +model — see the caution in `README.md`. + +## Related + +- `flutter-marionette` — the **proactive** counterpart: *interact* with a live app via MCP. + Reach for it to *make* things happen; reach for `/read-logs` to *inspect* what a run + already logged (including crashes/build errors marionette can't see, since it needs a live + connection). diff --git a/plugins/flutter-read-logs/skills/read-logs/SKILL.md b/plugins/flutter-read-logs/skills/read-logs/SKILL.md new file mode 100644 index 0000000..f4efbd3 --- /dev/null +++ b/plugins/flutter-read-logs/skills/read-logs/SKILL.md @@ -0,0 +1,240 @@ +--- +name: read-logs +description: Read the running Flutter app's latest run logs as context for a task — an auth flow, a crash, a race, "why did X happen at runtime". Use when the user invokes `/read-logs`, asks why the app behaved a certain way during a run, or wants the most recent run's logs as evidence. Reads only; never commits. +argument-hint: "[what to look for in the run logs]" +--- + +# Read run logs + +> **⚠️ Reading a run sends its contents to the model — that *is* the leak, and it's how +> this skill works.** Run logs routinely contain auth/refresh/push tokens, emails, and +> customer data; the moment you read one, that data is sent to the model. Invoking +> `/read-logs` means **consciously accepting** that this captured run gets sent. Don't run +> it against production or real-customer data unless you've accepted that risk. (See the +> plugin README for redaction options and the full data-handling note.) + +Load the running app's `flutter run` output as context for the task the user gave when +invoking this skill. Editor setup (making the app write the log file) is documented in this +plugin's `README.md` — that is canonical. The **First-run setup** section below intentionally +repeats those steps inline because this skill applies them at runtime, when the dev is in +their editor and not reading GitHub. Keep the two in sync. + +Logs are captured per-project to `/tmp/flutter-.log` (`` derived from the shared +`.git` via `--git-common-dir`, so the main checkout and every git worktree resolve to the +*same* file), overwritten on every launch (always the latest run), living in `/tmp` — +outside the repo, nothing to gitignore. Two capture formats exist depending on editor; the +skill auto-detects which: +- **Zed / terminal run task:** raw `script` TTY transcript (plain text, ANSI codes). +- **VS Code / Cursor (`dart.dapLogFile`):** Debug Adapter Protocol log (JSON-framed; the + app's output is inside `event:"output"` messages). + +**This skill loads logs as CONTEXT — it is not a standalone analyzer.** Don't summarize the +run, surface all errors, or volunteer a diagnosis on your own. Read the logs so you have +them, then carry out the task you were invoked with (e.g. "why did login flash", "does auth +reach the API"). If no task was given, confirm the logs are loaded and ask what they want — +don't analyze unprompted. + +--- + +## Step 1 — Resolve the path and check it exists (Bash) + +```bash +# Worktree-stable name: derive from the shared .git so the main checkout and all its +# worktrees resolve to ONE file. (--git-common-dir is relative from the main repo and +# absolute from a worktree, so cd into it and resolve before taking the repo dir name.) +CDIR=$(git rev-parse --git-common-dir 2>/dev/null) +if [ -n "$CDIR" ]; then REPO=$(basename "$(dirname "$(cd "$CDIR" && pwd -P)")"); else REPO=$(basename "$(pwd)"); fi +L="/tmp/flutter-$REPO.log" +echo "$L" +if [ ! -f "$L" ]; then + echo "MISSING" +else + echo "exists — $(wc -l < "$L") lines" + # Freshness: how old is the run, and did code change since? + MT=$(stat -f %m "$L" 2>/dev/null || stat -c %Y "$L" 2>/dev/null) + if [ -n "$MT" ]; then + AGE_MIN=$(( ($(date +%s) - MT) / 60 )) + echo "captured $(stat -f '%Sm' "$L" 2>/dev/null || stat -c '%y' "$L" 2>/dev/null) (${AGE_MIN} min ago)" + [ "$AGE_MIN" -gt 15 ] && echo "⚠️ this run is ${AGE_MIN} min old" + fi + if [ -d lib ] && [ -n "$(find lib -name '*.dart' -newer "$L" 2>/dev/null | head -1)" ]; then + echo "⚠️ Dart files in lib/ changed AFTER this run — the log predates your current code" + fi +fi +``` + +- **Exists** → go to Step 2 (note any freshness ⚠️ for Step 3). +- **MISSING** → do NOT just report missing. Go to the **First-run setup** section. + +## Step 2 — Read the run (format-aware, task-led) + +The file holds **only the most recent run** (it's overwritten on every launch), so there +are no older runs to scroll past and no need for a flat tail. First normalize it to plain +text, then read it with the **task as your lens** — not a fixed filter. + +**Normalize** (sniff the first lines for format) into a clean temp file you can page through: + +```bash +CLEAN="${L%.log}.clean.log" +if head -50 "$L" | grep -qE '\[DAP\]|"event"[[:space:]]*:[[:space:]]*"output"'; then + echo "[format: DAP — extracting app output]" + # grep first: only the output-event lines reach the JSON parser, not the whole protocol — + # so extraction cost tracks app output, not total DAP traffic. -E (not \|) for portable + # alternation; [[:space:]]* tolerates non-compact JSON. + grep -E '"event"[[:space:]]*:[[:space:]]*"output"' "$L" | python3 -c ' +import json, sys +for line in sys.stdin: + i = line.find("{") + if i < 0: continue + try: m = json.loads(line[i:]) + except Exception: continue + if m.get("event") == "output": + sys.stdout.write(m.get("body", {}).get("output", "")) +' > "$CLEAN" 2>/dev/null + # jq fallback if python3 is unavailable: + # grep -E '"event"[[:space:]]*:[[:space:]]*"output"' "$L" | sed -E 's/^[^{]*//' | jq -rj 'select(.event=="output") | .body.output' > "$CLEAN" +else + echo "[format: plain transcript]" + sed -E $'s/\x1b\\[[0-9;?]*[ -\\/]*[@-~]//g; s/\r$//' "$L" > "$CLEAN" +fi +wc -l "$CLEAN" +``` + +(If a DAP log can't be extracted because neither `python3` nor `jq` is present: `python3` +ships on most systems; otherwise install `jq` via the platform's package manager — +`brew install jq` on macOS, `apt install jq` / `dnf install jq` on Linux. Until then, fall +back to reading matching raw lines so the user isn't blocked.) + +**Transparency:** before reading, surface one line to the dev — *"reading `` — its +contents go to the model"* — so each read is a visible, conscious step (no blocking prompt). + +**Then read `$CLEAN` with the task as the lens — go as deep as the task needs:** + +- **Small run (roughly ≤ 600 lines):** read the **whole** file (`Read` it / `cat`). It's one + run; the full thing is the cleanest context. No filtering. +- **Large run** (long session, many hot reloads): **don't dump it.** Investigate against the + task — `grep` the cleaned text for what it's actually about, read those windows (`Read` + with offset/limit, or `grep -B/-A`), and **iterate deeper** (widen context, follow the + thread, read more) until you can answer. The lens comes from the task: + - crash / "doesn't work" → error markers: `^E/flutter`, `[SEVERE]`, `Exception:`, + `^#[0-9]+ ` (stack frames), `═══`, build failures (`FAILURE:`, `BUILD FAILED`, + `Error (Xcode`, `lib/...: Error`). + - race / ordering / timing → trace the relevant cubits/events/timestamps **in sequence**; + error markers are irrelevant here. + - a specific feature/screen → grep its names and read around them. + + Errors are just **one possible lens — never the only one.** Don't reduce every log read to + error-hunting. + +## Step 3 — Use as context + +Carry out the task you were invoked with against the logs. No run summary or error report +unless the task asks for it. + +If Step 1 flagged the run as stale — old, or (especially) Dart files changed after it was +captured — **and** the task is recency-sensitive ("does my fix work", "I just changed X"), +lead with that: tell the dev the log may not reflect their current code and suggest +re-running before you draw conclusions. It's a heads-up, not a refusal — if they clearly +want the existing run (e.g. investigating a past race), just proceed. + +--- + +## First-run setup (log is MISSING) + +Goal: leave the dev with their editor configured to capture logs, with no surprises. Never +guess silently and never commit anything. (The `README.md` is the canonical copy of these +steps; this inline version exists so setup works at runtime.) + +**This is the conscious opt-in moment.** Before wiring anything up, make sure the dev +understands that enabling capture means Claude will read run logs into context — which +**sends them to the model** — and that runs can contain tokens and customer data. Get a +clear yes before applying the setup; if they prefer not to take that risk, don't wire it. + +### 1. Determine which editor the dev runs the app in + +Detection is a convenience to skip a question — never a hard dependency. In order: + +```bash +echo "TERM_PROGRAM=$TERM_PROGRAM" # vscode (also Cursor) | zed | iTerm.app | Apple_Terminal | tmux ... +test -d .zed && echo "has .zed"; test -d .vscode && echo "has .vscode" +``` + +- `$TERM_PROGRAM` clearly says **vscode** → VS Code/Cursor path. **zed** → Zed path. +- Inconclusive (plain terminal) → fall back to config dirs: only `.zed/` → Zed; only + `.vscode/` → VS Code/Cursor. +- Still ambiguous (both dirs, or neither, and no host signal) → **ask** the dev which + editor they launch the app in. Asking once here is fine — this is one-time setup. + +Note: Cursor is a VS Code fork — it uses the same Dart-Code extension and reads +`.vscode/settings.json`, so it follows the VS Code path. A `.cursor/` dir is irrelevant to +logging. + +### 2. Check whether logging is already wired (then decide) + +The expected path is `L` from Step 1. + +- **VS Code/Cursor** — Read `.vscode/settings.json`; logging is wired iff + `"dart.dapLogFile"` equals `L`. (Legacy note: `dart.flutterRunLogFile` is **removed** in + current Dart-Code — ignore it if present.) +- **Zed / terminal task** — Read `.zed/tasks.json` (or `.vscode/tasks.json` for a VS Code + task runner); wired iff the `flutter run` tasks use `command: "script"` with `-q` `L`. + +Then: +- **Already wired correctly** → setup is fine; the app just hasn't been launched since (or + `/tmp` was cleared). Tell them to run the app, then re-run `/read-logs`. +- **Not wired** → explain what's missing and **offer to set it up** (next step). Don't edit + until they agree. + +### 3. Propose the fix (only after the dev agrees) + +Pick by editor. For VS Code/Cursor, prefer `dapLogFile` (keeps the F5 / Run-and-Debug +workflow — it captures F5 **and** Ctrl+F5 debug sessions). Only use the `script` task +variant if they launch via a non-debug terminal task (then `dapLogFile` would stay empty). + +**Before editing, check whether the target file is tracked — and warn if so:** + +```bash +TARGET=.vscode/settings.json # or .zed/tasks.json +git check-ignore -q "$TARGET" && echo "ignored (local-only — safe to edit)" || echo "TRACKED/committable — edit will show in git" +``` + +If TRACKED, tell the dev before applying: *"heads-up — `$TARGET` is tracked in this repo, +so this change will appear in `git status`. A `dapLogFile` line is a personal pref, and +wrapping committed run tasks in `script` changes the run command for the whole team — +commit it only if your team wants shared logging."* Then let them decide. + +**VS Code / Cursor** — merge into `.vscode/settings.json` (never drop existing keys like +`dart.flutterSdkPath`); create the file with just these keys if absent: +```json +{ + "dart.dapLogFile": "", + "dart.maxLogLineLength": 1000000 +} +``` +(`maxLogLineLength` default is 2000 and truncates long lines — bump it so output/stack +traces aren't cut.) **Bake the resolved literal ``** (the `--git-common-dir`-derived +path from Step 1) — do **not** use `dapLogFile`'s `${workspaceName}` variable: it expands to +the workspace folder, which differs per worktree and won't match the read derivation. Then: +reload the VS Code window, F5/Ctrl+F5 a run, re-run `/read-logs`. + + ⚠️ `dapLogFile` only captures **debug-adapter sessions** (F5 / Ctrl+F5), and app logs reach + the editor only on **simulator / macOS / Android / the `chrome` device**. A `-d web-server` + run sends app logs to the *browser's* console — the file will then hold build/launch + output but no app logs. If a dev sees an empty/launch-only capture on web, that's why. + +**Zed / terminal task** — wrap each `flutter run` task in `script`, **preserving its +existing flavor / target / dart-define args** (wrap what's there — don't invent args). +`script`'s syntax differs by OS, so detect it first: `uname -s`. +- **macOS (Darwin), BSD `script`:** set `command` to `"script"`, args become + `["-q", "", ]` (logfile, then the command as trailing + args). E.g. `["-q", "", "flutter", "run", "-t", "lib/main.dart"]`. +- **Linux, GNU `script`:** the command goes through `-c` as a single string and the logfile + is **last**: args become `["-q", "-c", "", ""]`. + E.g. `["-q", "-c", "flutter run -t lib/main.dart", ""]`. +- **Windows:** there's no `script`; that dev should use the VS Code/Cursor `dapLogFile` + path instead (it's cross-platform). + +Then: launch the app once, re-run `/read-logs`. + +If neither `.vscode/` nor `.zed/` exists and the dev's editor is unknown, ask which editor +they run the app in, then apply the matching setup above.