Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,44 @@ commit a version number refers to.

## [Unreleased]

## [0.1.7] — 2026-09-25

Catches the plugin up with `flutterflow_cli` / FlutterFlow AI SDK 0.0.41.

### Fixed
- **`flutterflow ai init` failed for new users.** The hook pinned
`flutterflow_cli` 0.0.38, but `init` always downloads the latest SDK, and SDK
0.0.41 (released 2026-09-23) requires CLI ≥ 0.0.41 — so fresh installs stopped
with *"This FlutterFlow AI snapshot requires flutterflow_cli >= 0.0.41"*.

### Changed
- **The hook now tracks the latest `flutterflow_cli` instead of a pin**, so it
can't fall behind the SDK again. Fresh installs get the latest release. For
existing installs, the hook reads the installed version from the pub-global
lockfile and, at most once every 6h, checks pub.dev (3s timeout, silent when
offline); if a newer release exists it runs
`dart pub global activate flutterflow_cli <latest>`. Forward-only (never
downgrades), skipped for path/git activations, logged to
`~/.cache/flutterflow-claude/upgrade.log`. Covered by new hook tests with
pub.dev and `dart` faked.
- **Build skill: removed the `flutterflow ai projects --json` step** added in
0.1.6 — no released CLI/SDK (through 0.0.41 and beta 0.0.41+1) ships that
command. The skill asks for the project URL instead.
- Build skill refreshed against CLI/SDK 0.0.41:
- New "CLI is too old" recovery for the minimum-CLI error.
- Page-file editing workflow (`init --page-dsl`, `orient` → edit →
`run --dry-run` → `run`, `scaffold edit`), with the workspace `CLAUDE.md` as
the authority.
- Orient gains `orient` and `review`; other surfaces gain `assets` and
`scaffold`, with a note that media must be uploaded, not pushed in the DSL.
- Re-running `init` in an existing workspace refreshes managed guidance.
- Gotchas: the known Supabase/Postgres list/detail validation gap
([#1](https://github.com/FlutterFlow/flutterflow-claude/issues/1)) and its
workaround, and that MCP commit tools require `userPrompts` (stored
server-side — never include secrets).
- README: documents the CLI auto-upgrade behavior and what the CLI sends
(installation id with its opt-out, AI commit attribution, MCP user prompts).

## [0.1.6] — 2026-07-15

### Changed
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ as possible.
It ships one plugin, **`flutterflow`**, which:

- **Auto-installs the FlutterFlow CLI** (`dart pub global activate flutterflow_cli`)
on session start — and **handles a missing Dart SDK** by pointing the user at an
install instead of failing silently.
on session start and keeps it at the latest release (the FlutterFlow AI SDK
requires a minimum CLI) — and **handles a missing Dart SDK** by pointing the
user at an install instead of failing silently.
- **Prompts for the API token** at enable time and stores it securely.
- Adds a **guided build skill** (workspace `init`, then orient → validate → apply)
that drives `flutterflow ai` over the Bash tool (which reads your shell profile),
Expand Down Expand Up @@ -70,6 +71,13 @@ session start the CLI installs itself; after that the hook is a fast pass that j
refreshes your PATH and the key file (no install work) — so a rotated key is picked
up next session.

**CLI version.** The hook tracks the latest `flutterflow_cli` on pub.dev. At most
once every 6 hours it checks pub.dev (3-second timeout, silent when offline) and
upgrades an older install (log at `~/.cache/flutterflow-claude/upgrade.log`). It
never downgrades and leaves path/git activations alone. Staying current matters
because `flutterflow ai init` always downloads the latest FlutterFlow AI SDK, and an
SDK build refuses to run under a CLI older than its minimum.

## Using it

Ask Claude to build something in FlutterFlow — the **build** skill
Expand Down Expand Up @@ -124,6 +132,18 @@ flutterflow ai logout --all # clears ~/.flutterflow/credentials.json (bare `lo
prompt or passed via `--api-key`; keys provided through the env file are never
persisted, so `logout --all` may report nothing to clear.)

## What the FlutterFlow CLI sends

These are FlutterFlow CLI/SDK behaviors, not something this plugin adds:

- **Installation id** (CLI ≥ 0.0.40): each API call carries a random per-machine
id stored at `~/.flutterflow/install_id` (no hostname, MAC, or user name). Set
`FLUTTERFLOW_NO_TELEMETRY=1` or `DO_NOT_TRACK=1` to turn it off.
- **AI attribution on commits**: commits made through the CLI/MCP show an AI badge
in FlutterFlow's version history; the API-key owner stays the author. The MCP
commit tools also send the user prompt(s) behind each change, stored server-side
and not visible to collaborators.

## Native MCP — registered automatically per workspace

There's nothing to wire up by hand: `flutterflow ai init` (and
Expand Down Expand Up @@ -189,7 +209,7 @@ claude plugin validate ./plugins/flutterflow --strict

Run the SessionStart hook's security-property tests (token file perms, safe
`%q` quoting, cleanup when the token is cleared, refusal to follow a symlinked
config dir). These also run in CI on every push and PR:
config dir, CLI upgrade checks with pub.dev faked). These also run in CI on every push and PR:

```bash
bash plugins/flutterflow/hooks/session-start.test.sh
Expand Down
2 changes: 1 addition & 1 deletion plugins/flutterflow/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "flutterflow",
"displayName": "FlutterFlow",
"version": "0.1.6",
"version": "0.1.7",
"description": "Agentic building with FlutterFlow. Auto-installs the FlutterFlow CLI (handles a missing Dart SDK gracefully) and provides a guided orient → validate → apply workflow over `flutterflow ai`.",
"author": {
"name": "FlutterFlow",
Expand Down
84 changes: 76 additions & 8 deletions plugins/flutterflow/hooks/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Goal: make the `flutterflow` CLI available with zero user effort, and fail
# soft no matter what. Runs on every session start/resume; it is a fast no-op
# once the CLI is installed.
# once the CLI is installed (plus a pub.dev version check at most every 6h).
#
# It never exits non-zero (a failing SessionStart hook must not block a session).

Expand All @@ -20,9 +20,11 @@ if [ -z "${HOME:-}" ]; then
exit 0
fi

# Pin the CLI version so this auto-run hook never floats to an unreviewed
# 'latest' from pub.dev. Bump deliberately alongside the plugin version.
FF_CLI_VERSION="0.0.38"
# Track the latest flutterflow_cli on pub.dev rather than a pin. The CLI
# downloads the latest FlutterFlow AI SDK at `init`/`upgrade`, and each SDK
# build declares a minimum CLI version — a pinned CLI that falls behind breaks
# `init` for everyone, so the hook keeps the CLI current instead.
PUB_API_URL="https://pub.dev/api/packages/flutterflow_cli"

# -----------------------------------------------------------------------------
# 1. Make common Dart / Flutter / pub-cache bin dirs visible.
Expand Down Expand Up @@ -110,14 +112,80 @@ else
fi
fi

STAMP_DIR="$HOME/.cache/flutterflow-claude"

# Version of the pub-global flutterflow_cli, read from its lockfile (cheap — no
# CLI launch). Prints nothing for path/git activations (a developer's local
# checkout) or when the CLI didn't come from `dart pub global activate`.
installed_cli_version() {
local lock="${PUB_CACHE:-$HOME/.pub-cache}/global_packages/flutterflow_cli/pubspec.lock"
[ -f "$lock" ] || return 0
awk '
/^ flutterflow_cli:/ { f = 1; next }
f && /^ [^ ]/ { exit }
f && /^ source:/ { src = $2 }
f && /^ version:/ { v = $2; gsub(/"/, "", v) }
END { if (src == "hosted" && v != "") print v }
' "$lock" 2>/dev/null
}

# version_lt A B — true when A < B, comparing MAJOR.MINOR.PATCH numerically
# (any -prerelease / +build suffix is ignored).
version_lt() {
local a="${1%%[-+]*}" b="${2%%[-+]*}" i x y
for i in 1 2 3; do
x=$(printf '%s' "$a" | cut -d. -f"$i"); y=$(printf '%s' "$b" | cut -d. -f"$i")
x=${x:-0}; y=${y:-0}
case "$x$y" in *[!0-9]*) return 1 ;; esac
[ "$x" -lt "$y" ] && return 0
[ "$x" -gt "$y" ] && return 1
done
return 1
}

# Latest published flutterflow_cli version from pub.dev, or nothing on any
# failure (offline, no curl, slow network, unexpected response). Only a plain
# MAJOR.MINOR.PATCH is accepted, since it's passed to `dart pub global activate`.
latest_cli_version() {
command -v curl >/dev/null 2>&1 || return 0
local v
v=$(curl -fsS --max-time 3 "$PUB_API_URL" 2>/dev/null \
| grep -o '"latest":{"version":"[^"]*"' | head -1 | sed 's/.*"version":"//; s/"$//')
case "$v" in
*[!0-9.]* | "" | .* | *. | *..*) return 0 ;;
*.*.*) printf '%s' "$v" ;;
esac
}

# -----------------------------------------------------------------------------
# 3. Already installed? Nothing more to do.
# 3. Already installed? Upgrade to the latest release if it's older, else done.
# Checks pub.dev at most once / 6h (3s timeout, silent when offline).
# Forward-only (never downgrades) and skipped for path/git activations.
# -----------------------------------------------------------------------------
if command -v flutterflow >/dev/null 2>&1; then
CURRENT=$(installed_cli_version)
if [ -n "$CURRENT" ] && command -v dart >/dev/null 2>&1; then
mkdir -p "$STAMP_DIR" 2>/dev/null
STAMP="$STAMP_DIR/last-update-check"
if [ -z "$(find "$STAMP" -mmin -360 2>/dev/null)" ]; then
# Stamp before the network call so an offline machine isn't re-checked
# (and delayed by the timeout) on every session start.
: > "$STAMP"
LATEST=$(latest_cli_version)
if [ -n "$LATEST" ] && version_lt "$CURRENT" "$LATEST"; then
log "Upgrading the FlutterFlow CLI ($CURRENT → $LATEST)…"
if dart pub global activate flutterflow_cli "$LATEST" >"$STAMP_DIR/upgrade.log" 2>&1; then
log "✓ FlutterFlow CLI upgraded to $LATEST. Existing workspaces refresh their SDK on the next command."
else
log "✗ CLI upgrade failed. See the log: $STAMP_DIR/upgrade.log"
log " Upgrade manually with: dart pub global activate flutterflow_cli"
fi
fi
fi
fi
exit 0
fi

STAMP_DIR="$HOME/.cache/flutterflow-claude"
mkdir -p "$STAMP_DIR" 2>/dev/null

# -----------------------------------------------------------------------------
Expand All @@ -134,8 +202,8 @@ if command -v dart >/dev/null 2>&1; then
exit 0
fi

log "Installing the FlutterFlow CLI (flutterflow_cli $FF_CLI_VERSION)…"
if dart pub global activate flutterflow_cli "$FF_CLI_VERSION" >"$STAMP_DIR/activate.log" 2>&1; then
log "Installing the FlutterFlow CLI (latest flutterflow_cli)…"
if dart pub global activate flutterflow_cli >"$STAMP_DIR/activate.log" 2>&1; then
log "✓ FlutterFlow CLI installed — you're ready for agentic building."
if ! command -v flutterflow >/dev/null 2>&1; then
log "One more step: add Dart's pub-cache bin to your PATH, then restart:"
Expand Down
65 changes: 64 additions & 1 deletion plugins/flutterflow/hooks/session-start.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ printf '#!/bin/sh\nexit 0\n' > "$WORK/bin/flutterflow"
chmod +x "$WORK/bin/flutterflow"

# run <HOME> <TOKEN> <stderr-file>
run() { HOME="$1" PATH="$WORK/bin:$PATH" CLAUDE_PLUGIN_OPTION_API_TOKEN="$2" bash "$HOOK" 2>"$3"; }
# PUB_CACHE points into the throwaway HOME (no lockfile there), so the hook's
# upgrade check never sees the runner's real CLI and never runs a real `dart`.
run() { HOME="$1" PUB_CACHE="$1/.pub-cache" PATH="$WORK/bin:$PATH" CLAUDE_PLUGIN_OPTION_API_TOKEN="$2" bash "$HOOK" 2>"$3"; }

echo "== A: token set -> file 600, dir 700, both exports present =="
H="$WORK/A"; mkdir -p "$H"
Expand Down Expand Up @@ -104,6 +106,67 @@ run "$H" '' "$WORK/g2.log"
grep -q 'app.flutterflow.io/account' "$WORK/g2.log" \
&& fail "notice repeated within throttle window" || pass "notice throttled on the next run"

echo
echo "== U: installed CLI older than pub.dev latest -> upgrade, throttled; never downgrade =="
# Fakes: `dart` records its arguments; `curl` serves a pub.dev-shaped response
# for $FAKE_LATEST (or fails when it's empty) — no network, no real installs.
mkdir -p "$WORK/ubin"
cp "$WORK/bin/flutterflow" "$WORK/ubin/flutterflow"
printf '#!/bin/sh\necho "$*" >> "$DART_LOG"\nexit 0\n' > "$WORK/ubin/dart"
cat > "$WORK/ubin/curl" <<'SH'
#!/bin/sh
echo x >> "$CURL_LOG"
[ -n "$FAKE_LATEST" ] || exit 7
printf '{"name":"flutterflow_cli","latest":{"version":"%s","pubspec":{}},"versions":[{"version":"0.0.1"}]}' "$FAKE_LATEST"
SH
chmod +x "$WORK/ubin/dart" "$WORK/ubin/curl"
# seed_lock <HOME> <version> <source> — fake a pub-global lockfile entry.
seed_lock() {
mkdir -p "$1/.pub-cache/global_packages/flutterflow_cli"
printf 'packages:\n flutterflow_cli:\n dependency: "direct main"\n source: %s\n version: "%s"\n glob:\n version: "9.9.9"\n' \
"$3" "$2" > "$1/.pub-cache/global_packages/flutterflow_cli/pubspec.lock"
}
# urun <HOME> <fake-latest> — run the hook with the fakes on PATH.
urun() {
HOME="$1" PUB_CACHE="$1/.pub-cache" PATH="$WORK/ubin:$PATH" DART_LOG="$1/dart.log" \
CURL_LOG="$1/curl.log" FAKE_LATEST="$2" CLAUDE_PLUGIN_OPTION_API_TOKEN=x \
bash "$HOOK" 2>"$1/hook.log"
}

H="$WORK/U1"; mkdir -p "$H"; seed_lock "$H" "0.0.1" hosted
urun "$H" "0.0.41"
grep -qx "pub global activate flutterflow_cli 0.0.41" "$H/dart.log" 2>/dev/null \
&& pass "older hosted CLI upgraded to pub.dev latest" || fail "older CLI not upgraded to latest"
urun "$H" "0.0.41"
{ [ "$(wc -l < "$H/dart.log")" -eq 1 ] && [ "$(wc -l < "$H/curl.log")" -eq 1 ]; } \
&& pass "version check throttled on the next run" || fail "re-checked within the throttle window"

H="$WORK/U2"; mkdir -p "$H"; seed_lock "$H" "0.0.41" hosted
urun "$H" "0.0.41"
[ ! -e "$H/dart.log" ] && pass "CLI already at latest left alone" || fail "dart invoked for an up-to-date CLI"

H="$WORK/U3"; mkdir -p "$H"; seed_lock "$H" "99.0.0" hosted
urun "$H" "0.0.41"
[ ! -e "$H/dart.log" ] && pass "newer CLI never downgraded" || fail "dart invoked for a newer CLI"

H="$WORK/U4"; mkdir -p "$H"; seed_lock "$H" "0.0.1" path
urun "$H" "0.0.41"
{ [ ! -e "$H/dart.log" ] && [ ! -e "$H/curl.log" ]; } \
&& pass "path-activated (dev checkout) CLI left alone, no network" || fail "path activation was checked or upgraded"

H="$WORK/U5"; mkdir -p "$H"
urun "$H" "0.0.41"
{ [ ! -e "$H/dart.log" ] && [ ! -e "$H/curl.log" ]; } \
&& pass "no lockfile (non-pub install) -> no check, no upgrade" || fail "checked or upgraded without a lockfile"

H="$WORK/U6"; mkdir -p "$H"; seed_lock "$H" "0.0.1" hosted
if urun "$H" ""; then pass "offline (curl fails) -> exit 0"; else fail "non-zero exit when offline"; fi
[ ! -e "$H/dart.log" ] && pass "offline -> no upgrade attempted" || fail "dart invoked while offline"

H="$WORK/U7"; mkdir -p "$H"; seed_lock "$H" "0.0.1" hosted
urun "$H" '0.0.99;touch pwned'
[ ! -e "$H/dart.log" ] && pass "malformed latest version rejected" || fail "dart invoked with a malformed version"

echo
echo "== F: HOME unset -> clean exit 0, no crash, no writes =="
if env -u HOME PATH="$WORK/bin:$PATH" CLAUDE_PLUGIN_OPTION_API_TOKEN=x bash "$HOOK" 2>/dev/null; then
Expand Down
Loading
Loading