Skip to main content

crumbs 0.0.13 copy "crumbs: ^0.0.13" to clipboard
crumbs: ^0.0.13 copied to clipboard

Inspect a running Flutter app from an AI agent or the CLI: HTTP traffic, logs, exceptions, queries and events captured in memory, read over MCP.

Changelog #

All notable changes to this project will be documented in this file.

This project follows Semantic Versioning 2.0.0. Entries follow the Keep a Changelog shape.


[Unreleased] #


0.0.13 - 2026-11-14 #

Changed #

  • Breaking, nothing is migrated automatically. In a consumer project: (1) swap the dependencies with flutter pub remove fluttersdk_telescope fluttersdk_artisan && flutter pub add crumbs dowel and rename the imports (package:fluttersdk_telescope/telescope.dart is package:crumbs/crumbs.dart) and the Telescope* identifiers; (2) follow the dowel 0.0.21 migration (move .artisan/ to .dowel/, delete .dowel/plugins.json and .dowel/installed/fluttersdk_telescope.json), delete bin/dispatcher.dart, bin/fsa, lib/app/_plugins.g.dart and lib/app/commands/_index.g.dart (the installers keep a file that exists, and the old ones import fluttersdk_artisan, so ./bin/dowel would not build), replace package:fluttersdk_artisan/artisan.dart with package:dowel/dowel.dart and Artisan* with Dowel* in your own commands, run dart run crumbs crumbs:install again, then dart run dowel commands:refresh to register your commands in the new index; (3) rename telescope_* to crumbs_* in any saved MCP allow list, prompt or script (a Claude Code allow-list entry mcp__fluttersdk__telescope_* becomes mcp__dowel__crumbs_*, since the server key is now dowel), telescope:* to crumbs:* in CLI calls and TELESCOPE_DISABLE to CRUMBS_DISABLE in a build that must not register the extensions; (4) delete the stale fluttersdk server entry from .mcp.json (dowel leaves entries an earlier release wrote) and reconnect the MCP client; (5) in a Magic app, until a magic_devtools release built on crumbs ships, MagicTelescopeIntegration feeds fluttersdk_telescope, not crumbs, so the HTTP, event, gate, query and cache buffers of crumbs stay empty; then upgrade magic_devtools and rerun dart run crumbs crumbs:install (see the installer entry below).
  • Renamed from fluttersdk_telescope to crumbs. The package, the repository (anilcancakir/crumbs), the barrels (package:crumbs/crumbs.dart and package:crumbs/cli.dart), every Telescope* class (now Crumbs*, and TelescopeArtisanProvider is CrumbsDowelProvider), the executable (dart run crumbs) and the file names carry the new name. Also renamed: the ext.telescope.* VM Service extensions (now ext.crumbs.*), the MCP tools (telescope_* is crumbs_*), the CLI commands (telescope:* is crumbs:*), the environment variable (TELESCOPE_DISABLE is CRUMBS_DISABLE), the MCP registry name (io.github.anilcancakir/crumbs), the skill (fluttersdk-telescope is crumbs) and the docs URLs (https://anilcancakir.com/crumbs).
  • The floor is Dart 3.13 and Flutter 3.47. Flutter 3.50 deprecates package:flutter/material.dart in favour of material_ui, which needs that floor; the example app now builds on material_ui, the package itself imports neither, and the repository is formatted with the tall style that floor enables. A consumer on an older SDK stays on the last fluttersdk_telescope release.
  • Built on dowel. fluttersdk_artisan ^0.0.20 is now dowel ^0.0.21. The .mcp.json server key written by mcp:install is dowel.
  • The Magic step of crumbs:install needs a magic_devtools built on crumbs. When the consumer's pubspec names magic_devtools and the resolved release ships package:magic_devtools/crumbs.dart, the installer wires MagicCrumbsIntegration from it. The magic_devtools releases that still ship MagicTelescopeIntegration have no such library, so with one of them the installer skips the Magic lines and warns; upgrade magic_devtools to a release built on crumbs and rerun dart run crumbs crumbs:install (see Fixed).

Fixed #

  • crumbs:install wires Magic only when the installed magic_devtools ships crumbs.dart. The pubspec entry alone decided it, so a magic_devtools release that predates crumbs got an import of a library it does not have and a lib/main.dart that did not compile. The installer now resolves the package through the nearest .dart_tool/package_config.json at or above the project (a pub workspace member uses the one at the workspace root; an unreadable or invalid one counts as unresolved) and, without lib/crumbs.dart, skips the Magic lines and warns to upgrade and rerun. (lib/src/commands/crumbs_install_command.dart)
  • crumbs:install resolves a magic_devtools path dependency outside the project. The walk to .dart_tool/package_config.json kept a trailing separator, so a relative rootUri that climbs out of the project (../../magic_devtools for path: ../magic_devtools) resolved one level too deep; a release that ships lib/crumbs.dart read as missing and the installer skipped the Magic lines with the upgrade warning. The walk now stays on directory URIs. (lib/src/commands/crumbs_install_command.dart)
  • crumbs:install leaves a lib/main.dart that already calls CrumbsPlugin.install() alone. Only the exact injected block counted as wired, so a file wired by hand (comments between the calls, as in the example app, or another shape) got a second block, and the chained watchers recorded every exception and debugPrint twice. Any CrumbsPlugin.install( call now skips the crumbs block and its imports; the Magic lines are still added when they are missing. (lib/src/commands/crumbs_install_command.dart)
  • The crumbs_dumps and crumbs_exceptions tool descriptions say what the watchers capture. DumpWatcher overrides debugPrint only, so a plain print() is not captured (the description said it was); ExceptionWatcher hooks PlatformDispatcher.onError as well as FlutterError.onError. The --limit help of crumbs:queries and crumbs:caches says the newest N come back oldest first, which is the order they print in. (lib/src/crumbs_dowel_provider.dart, lib/src/commands/)
  • The DioHttpAdapter docs and the crumbs_requests tool description describe the adapter that ships. The class doc showed a DioHttpAdapter(dio).asDioInterceptor() that does not exist and the tool description named a crumbs Dio interceptor that does not ship; both now say the app's own interceptor calls DioHttpAdapter.recordRequest. (lib/src/adapters/dio_http_adapter.dart, lib/src/crumbs_dowel_provider.dart)

Removed #

  • The FluttersdkTelescopeArtisanProvider alias in package:crumbs/cli.dart. The class dowel's plugins:refresh generates for this package, CrumbsDowelProvider, is the provider itself, so the alias is gone.

0.0.12 - 2026-10-13 #

Changed #

  • The artisan floor names this batch's release. fluttersdk_artisan moves ^0.0.19 to ^0.0.20. The old range already admitted 0.0.20, so a fresh pub get resolves nothing differently; what changes is that the floor names the release this package is verified against. artisan 0.0.20 is not breaking, and a consumer inherits one behaviour from it: a telescope:* command run while the app is still booting, before ext.telescope.* is registered, now prints one retry line and exits 75 instead of an RPC stack and exit 3. The requirements tables in doc/getting-started/ and the stack lines in CLAUDE.md and .github/copilot-instructions.md quote the new floor. (pubspec.yaml, doc/getting-started/, CLAUDE.md, .github/copilot-instructions.md)

0.0.11 - 2026-10-10 #

Added #

  • TelescopeRedaction.redactor, a host-set text redactor applied to every log, event, exception and HTTP record at insert. recordLog, recordEvent, recordException and recordHttp (after its key masking) run it over every String field, every String and map key inside an event payload's nested Maps and Lists, and the stack text, before the record enters its queue or its stream; a record the redactor throws on is dropped, neither buffered nor emitted. Each of the four records gains a redacted getter (true when a redactor ran), serialized in toJson. Only the store's redaction pass sets it: the constructors take no redacted argument and HttpRequestRecord.copyWith returns an unredacted copy, so no caller can build a record a persisting consumer would take for masked. A record buffered while no redactor was registered stays false, so a consumer that persists records can write only the redacted ones. A subtree nested past 64 levels is masked whole. redactLogRecord, redactExceptionRecord and redactEventRecord join redactHttpRecord as the per-record entry points, each returning null for a dropped record, and resetForTesting() unregisters the redactor. (lib/src/telescope_redaction.dart, lib/src/telescope_store.dart, lib/src/records/)
  • Per-kind buffer capacity. TelescopeStore.setCapacity(int, {TelescopeKind? kind}) caps one of the ten buffers (TelescopeKind); the call without a kind keeps today's shared behaviour, and a kind cap wins over the shared one. setFramePerfCapacity now calls the framePerf kind. (lib/src/telescope_store.dart)
  • LogRecordEntry.atUs, a monotonic timestamp. The same FlutterTimeline.now clock EventRecord.atUs uses, stamped at construction (LogRecordEntry.fromLogRecord included) and serialized in toJson. (lib/src/records/log_record_entry.dart)
  • TelescopeFileSink, a rotating JSONL timeline on disk that writes only redacted records. TelescopeFileSink.start(directory:, maxFileBytes: 1048576, maxFiles: 8) creates the host-given directory and writes every log, event and exception record from onLogRecord, onEventRecord and onExceptionRecord as one line, {"kind": "log"|"event"|"exception", ...toJson()}, to timeline-<launch stamp>-<n>.jsonl, batched at 64 lines or 500 ms. It starts a new file before a line would pass maxFileBytes and deletes the oldest timeline files in the directory, earlier launches included, beyond maxFiles. Fail closed: a record with redacted false is never written, only counted in droppedUnredacted. A batch write no caller awaits that fails stops the sink instead of raising an uncaught error, counted in writeFailures, and a sink that cannot create its directory or first file neither listens nor becomes current. files() lists {name, bytes} of every timeline file in the directory, earlier launches included, newest first; read(name, offset:, maxBytes:) returns whole lines as {lines, next}, decodes an offset inside a multi-byte character without throwing, and refuses (returns null) any name that is not exactly one of those listed files, so a path, a .. segment or a link reaches nothing. A sink started again within the same second (the stamp has second precision) appends to the file of the sink it replaced, counting its bytes toward maxFileBytes, rather than truncating it. flush() writes the batch, stop() flushes and closes, and TelescopeFileSink.current is the running sink. The package looks no directory up. (lib/src/telescope_file_sink.dart)
  • A cursor and filters on ext.telescope.events and ext.telescope.console. Both accept since (microseconds, exclusive, compared with atUs); events also type (prefix of eventType), console also logger (exact name). The filters run before limit: with since, limit keeps the OLDEST N after it, extended to the end of the run of records sharing the last one's atUs (the page may exceed limit by those ties), so paging by cursor skips nothing; without since it keeps the newest N of what matched. Both answer a cursor: the largest atUs returned, else the given since, else null, so passing it back as since yields only newer records. A since that is not an integer is refused with invalidParams rather than read as "from the start". (lib/src/extensions/register_telescope_extensions.dart)
  • ext.telescope.files and ext.telescope.file. Read the running TelescopeFileSink.current: files answers {files: [{name, bytes}]} newest first, file takes name, offset and maxBytes and answers {lines, next}. Both answer {error} when no sink runs, and file answers one fixed {error} message for every refusal; the name only reaches the sink's own allowlist. (lib/src/extensions/register_telescope_extensions.dart)
  • --since, --type / --logger, --json and --follow on telescope:events and telescope:tail. --json prints one JSON object per line, the payload as JSON; --follow re-reads every second from the returned cursor until interrupted. (lib/src/commands/)
  • followCursor, the one --follow loop. followCursor(read:, since:, interval:, delay:, shouldStop:) calls read with the cursor the previous round returned and waits interval between rounds; a null cursor keeps the last one. Pure Dart, exported from package:fluttersdk_telescope/cli.dart, and now behind both telescope:events and telescope:tail. (lib/src/cursor_follow.dart, lib/cli.dart)
  • telescope:files [--name=<file>] [--offset=<n>]. Lists the timeline files, or prints the lines of one; prints the extension's error message, not a stack, when no sink runs. (lib/src/commands/telescope_files_command.dart, lib/src/telescope_artisan_provider.dart)

Changed #

  • Event payloads are JSON-safe at record time. recordEvent stores a value jsonEncode cannot write (a DateTime, a Duration, any object) as its toString(), and a non-String map key likewise, so one bad payload can no longer fail the whole ext.telescope.events response. A payload that is already safe keeps its identity. (lib/src/telescope_redaction.dart)
  • toJson of the log, event, exception and HTTP records always carries redacted. (lib/src/records/)
  • BREAKING: TelescopeRedaction.redactHttpRecord returns HttpRequestRecord?. It was HttpRequestRecord; it now returns null, the record dropped, when the registered redactor throws, so a caller outside the store adds a null check. (lib/src/telescope_redaction.dart)

Fixed #

  • An unknown minLevel matched every log line. recentLogs(minLevel: 'bogus') now returns an empty list instead of the whole buffer. (lib/src/telescope_store.dart)
  • A negative limit threw. Every recentX(limit:) returns an empty list for a zero or negative limit instead of a RangeError. (lib/src/telescope_store.dart)
  • A payload jsonEncode cannot write failed the whole ext.telescope.events response. The payload is now made JSON-safe when the event is recorded (see Changed). (lib/src/telescope_redaction.dart)

0.0.10 - 2026-10-09 #

Changed #

  • The artisan floor names this batch's release. fluttersdk_artisan moves ^0.0.17 to ^0.0.19. The old range already admitted 0.0.19, so a fresh pub get resolves nothing differently; what changes is that the floor names the release this package is verified against. Neither artisan 0.0.18 nor 0.0.19 is breaking: 0.0.18 adds installer operations telescope does not use, and 0.0.19 widens artisan's xml constraint to admit 7.x, which a consumer now inherits. (pubspec.yaml, doc/getting-started/, CLAUDE.md, .github/copilot-instructions.md)
  • The example app moves cupertino_icons to ^2.0.0. It touches only the unpublished example/ app, not the package. (#41, example/pubspec.yaml, example/pubspec.lock)

0.0.9 - 2026-09-29 #

Added #

  • A monotonic clock, an interaction link, and exclusive self time land on every telescope record. HttpRequestRecord, QueryRecord, EventRecord, MagicModelRecord, MagicCacheRecord and FramePerfRecord all gain atUs (an int? atUs constructor param stored as atUs ?? FlutterTimeline.now, comparable across record types regardless of wall-clock skew), interactionId (String?) and linkedBy (String?, one of zone / frame / window), all three serialized in toJson. HttpRequestRecord additionally carries requestId, startUs and endUs for exact request/response pairing. FramePerfRecord gains vsyncStartUs from FrameTiming.timestampInMicroseconds(FramePhase.vsyncStart); its doc comment marks clock parity with atUs as needing live validation, since the engine documents that timestamp only as microseconds "from some epoch" and a widget test can only inject synthetic FrameTiming values, never a real engine-reported one. Every block in a FramePerfRecord.blocks map gains selfMicros: FramePerfWatcher's drain now computes each block's exclusive duration from AggregatedTimings.timedBlocks start/end nesting (a stack walk over blocks sorted by start, tie-broken by the longer block first) and subtracts directly nested children from the existing inclusive micros. telescope:frames and telescope:requests print the new fields, linkedBy on its own when there is no interactionId (the window case). Every new constructor parameter is optional or defaulted, so a constructor call that does not pass blocks by an explicit record type keeps compiling (magic_devtools/lib/src/telescope_integration.dart does); the blocks type is the one exception, below. The skill's records.md, mcp-tools.md and cli-commands.md document every new key. (lib/src/records/*.dart, lib/src/watchers/frame_perf_watcher.dart, lib/src/commands/telescope_{frames,requests}_command.dart, skills/fluttersdk-telescope/references/)

  • TelescopeRedaction, the credential lists behind the HTTP buffer, and HttpRequestRecord.copyWith. hideRequestHeaders, hideRequestParameters and hideResponseParameters add names to the defaults (Laravel Telescope merges the same way, it never replaces), hiddenRequestHeaders / hiddenRequestParameters / hiddenResponseParameters read the lowercased lists, redactParameters(data, keys) returns a copy of a Map/List structure with every value under a matching key, at any depth, replaced by mask ('********'), never mutating its input, and redactBody(body, keys) does the same for a JSON string, or pair by pair for a form-encoded one. An adapter calls one of them before it stringifies or truncates a body, since neither a Map.toString() nor a cut JSON string parses, and the store cannot mask what it cannot parse. copyWith replaces requestHeaders, requestBody and responseBody and keeps every other field, atUs included. (lib/src/telescope_redaction.dart, lib/src/internal/redaction_lists.dart, lib/src/records/http_request_record.dart)

Changed #

  • BREAKING: FramePerfRecord.blocks is Map<String, ({int micros, int selfMicros, int count})>. It was Map<String, ({int micros, int count})>, and Dart record types with different fields are not assignable to each other, so code that builds a FramePerfRecord from a map of the old record type, or declares a variable of the old type from record.blocks, stops compiling. Add selfMicros to the record literal (the inclusive micros is a safe value when the nesting is unknown) or let the type be inferred. The JSON shape only gains a key, so a reader of ext.telescope.frames output is unaffected. (lib/src/records/frame_perf_record.dart)

  • The artisan floor names this batch's release. fluttersdk_artisan moves ^0.0.16 to ^0.0.17. The old range already admitted 0.0.17, so a fresh pub get resolves nothing differently; what changes is that the floor names the release this package is verified against. artisan 0.0.17 changes stop, restart and a failed start --cdp-port to reap the app's whole process group, and telescope calls none of those APIs. (pubspec.yaml)

Security #

  • The HTTP buffer no longer stores credentials verbatim. It is served to AI agents over ext.telescope.requests and MCP, and a Magic app's bearer header, a login body's password and a login answer's Sanctum token all landed in it as sent. TelescopeStore.recordHttp now masks, before buffering and before emitting on onHttpRecord: the value of every request header named authorization, proxy-authorization, cookie, set-cookie or x-api-key, and, in a body that parses as a JSON object or array or reads as form-encoded (grant_type=password&password=..., where only the matching pair's value changes, and a bracketed key such as user[password] or codes[] matches on its last part, as a nested JSON key does), the value under password, password_confirmation, current_password, new_password, token, access_token, refresh_token, secret, client_secret, authorization_code, id_token, two_factor_token or recovery_code (request) and token, access_token, refresh_token, plain_text_token, secret, client_secret, id_token, two_factor_token, recovery_codes, qr_url or qr_svg (response), at any depth. qr_url and qr_svg are on the list because both carry the TOTP secret that secret masks. Names match case-insensitively. An empty value (null, false, '', [], {}) stays visible, a subtree deeper than 64 levels is masked whole so a pathological body cannot overflow the stack, any other body (plain text, a truncated snippet) is kept as given, and a JSON body with nothing to hide keeps its exact bytes. (lib/src/telescope_store.dart, lib/src/telescope_redaction.dart)

0.0.8 - 2026-09-28 #

Added #

  • telescope:events, telescope:gates and telescope:dumps, the CLI mirrors of the three MCP-only buffers. A consumer that drives the app from a shell instead of wiring the MCP server (whose 58 tool schemas cost an agent context on every turn) had no way to read event, gate or debugPrint records at all. Each command reads its ext.telescope.* extension, takes --limit (default 50), prints one line per record and warns on an empty buffer, the same shape as telescope:queries. exceptions stays MCP-only; dusk:exceptions reads that buffer from a shell. (lib/src/commands/telescope_{events,gates,dumps}_command.dart)

0.0.7 - 2026-09-22 #

Fixed #

  • The exception watcher's coexistence note gives the order the code requires: Sentry or Bugsnag first, telescope second. The watcher table said to register telescope FIRST so the chain-preserve wraps the next handler. The chain-preserve saves the handler already in place and calls it after its own capture, so it wraps the PREVIOUS one, and the example further down the same page already installed Sentry first. Following the table left a reporter installed after telescope receiving errors only if that reporter preserves the chain itself. Documentation only; no code moved. (doc/watchers/index.md) (#33)

0.0.6 - 2026-09-19 #

Added #

  • server.json, the manifest that lists this package on the official MCP registry. The ecosystem was absent from every MCP directory, so an agent looking for a Flutter runtime inspector had no way to find it. The entry carries repository and websiteUrl and deliberately no packages block: registryType documents npm, pypi, oci, nuget and mcpb with no pub equivalent, and both packages and remotes are optional on ServerDetail, which requires only name, description and version. Nothing in the package reads the file and no workflow publishes it, so test/server_json_version_test.dart guards the version against pubspec.yaml, along with the schema's 100 character description cap. Excluded from the pub archive as repo configuration rather than package content. (server.json, .pubignore, test/server_json_version_test.dart)

Changed #

  • The fluttersdk_artisan floor moves ^0.0.8 to ^0.0.16. telescope is a plugin on artisan, and 0.0.16 is where a plugin injection that matches nothing stops reporting Success over a file it never touched. The old range admitted 0.0.16 already, so nothing resolves differently on a fresh pub get; what changes is that the floor names the release this package is verified against. The requirements tables in doc/getting-started/index.md and doc/getting-started/installation.md said ^0.0.2 and CLAUDE.md said ^0.0.8, both stale enough to mislead a reader who pins from them. (pubspec.yaml, doc/getting-started/index.md, doc/getting-started/installation.md, CLAUDE.md)

0.0.5 - 2026-08-25 #

Added #

  • A tenth ring buffer, FramePerfRecord, for per-frame performance data. Carries the fields FrameTiming exposes (buildMicros, rasterMicros, vsyncOverheadMicros, totalSpanMicros) plus a per-frame block-attribution map. TelescopeStore.recordFramePerf/.recentFramePerf/.onFramePerfRecord follow the shape of the existing nine buffers, with one deliberate difference: the buffer reads its own setFramePerfCapacity (default 3600, about a minute at 60fps) instead of the shared setCapacity, so a useful frame window does not simultaneously inflate the HTTP, log, exception, dump, model, cache, event, gate and query buffers. clearFramePerf() is a new public, non-test-only method that empties only this buffer, for a production caller that needs to zero it at the start of a measurement session without touching the other nine. (lib/src/records/frame_perf_record.dart, lib/src/telescope_store.dart, lib/telescope.dart)

  • FramePerfWatcher, which fills that buffer by joining the two sources the engine reports separately. Frame magnitude comes from SchedulerBinding.addTimingsCallback, which carries FrameTiming.frameNumber but arrives late and batched (roughly 100ms later on web). Per-frame attribution comes from draining FlutterTimeline at the end of every frame, which knows what ran but not which frame number it was. So the drain parks its block map in a bounded pending map and the timings callback is the sole emission point, writing one complete record per timing. A timing with no block map still emits, because frame magnitude without attribution is still worth having; a block map whose timing never arrives is dropped by the bound.

    Two things about the drain are easy to get wrong and are pinned in the source. It re-registers itself as its last act, because addPostFrameCallback is one-shot and a drain that does not would run exactly once, freezing the liveness counter at 1 and making every later session unreportable. And the collect runs in a MICROTASK scheduled by the post-frame callback rather than inline in it: SchedulerBinding.handleDrawFrame wraps the whole post-frame phase in its own POST_FRAME span, so an inline collect runs with a non-empty nesting stack and trips assert(_stackPointer == 0) in FlutterTimeline. That assert is stripped outside debug, where the same call would instead write a stale start time into the freshly swapped buffer and report an enormous duration for a block that never ran.

    The watcher also exposes a static, monotonic livenessCounter incremented once per frame drawn. It is the only reliable proof the engine is rendering: SchedulerBinding.framesEnabled was measured reporting true, with a resumed lifecycle and an armed onReportTimings, on a Chrome page that had produced one frame in two seconds. The watcher is opt-in and is NOT auto-installed by TelescopePlugin.install(); register it with TelescopePlugin.registerWatcher(FramePerfWatcher()). (lib/src/watchers/frame_perf_watcher.dart, lib/telescope.dart)

  • ext.telescope.frames, telescope:frames and telescope_frames, the wire surface for the new frame-perf buffer. The extension returns the buffer's records alongside FramePerfWatcher.livenessCounter, so a caller reading an empty result can tell a quiet app from a stalled engine without a second round trip. The naming follows telescope's plural-noun convention (requests, exceptions, queries, ...); the wire says frames, not frame_perf, even though the Dart types keep that prefix to name the record and the watcher. TelescopeArtisanProvider now ships 7 CLI commands and 10 MCP tools. (lib/src/extensions/register_telescope_extensions.dart, lib/src/commands/telescope_frames_command.dart, lib/src/telescope_artisan_provider.dart, test/src/commands/telescope_frames_command_test.dart)

Changed #

  • The registry dispatch fires on a published release now, not on every push that touches the skill. Under the push trigger fluttersdk/ai climbed to v1.3.75, and most of those releases re-published identical skill content: a docs commit and a release commit each cost the registry a version. The registry version now tracks published telescope releases instead of counting commits. workflow_dispatch stays as the manual escape hatch when a skill fix has to reach users before the next release. (.github/workflows/dispatch-to-registry.yml)

Fixed #

  • The registry dispatch could never fire, so a release would have shipped a skill the registry never received. dispatch-to-registry.yml declared release: [published], but publish.yml creates the release with softprops/action-gh-release under the default GITHUB_TOKEN, and GitHub does not start workflow runs from events raised by that token. The run history confirms it: no release event has ever appeared there, only the manual run and the retired push trigger. It has also had no chance to be noticed, since the switch to that trigger landed after the last release (0.0.4, 2026-06-17). The failure mode is silent, because publish.yml goes green either way and the only symptom is a user installing a skill one release behind. publish.yml now calls the workflow directly with needs: github-release, which removes the cross-workflow event and sequences the dispatch after pub.dev has accepted the release rather than alongside it; workflow_call replaces the dead trigger and workflow_dispatch stays as the manual escape hatch. Secrets are passed by name rather than inherited, since the called workflow needs exactly two. Second bug in the same file, fixed alongside: the version extractor tested the ref against ^v[0-9]+\.[0-9]+\.[0-9]+, but this repo tags without the v prefix, so that branch never matched a real tag and always fell through to reading pubspec.yaml. (.github/workflows/dispatch-to-registry.yml, .github/workflows/publish.yml)

  • doc/mcp/tool-reference.md still promised newest-first records and a 200-entry cap. 0.0.3 corrected the MCP tool descriptions to the real wire shape and retired the newest-first shorthand, but this page kept it in all eight limit rows, plus a "typically 200-500 depending on buffer type" capacity note. limit returns the most recent N records in chronological order (_trim takes list.sublist(list.length - limit), so the oldest of that window is first and the newest is last), and every buffer caps at 500. A client reading this page and not reversing its iteration displayed the window backwards. (doc/mcp/tool-reference.md)

  • telescope_tail's MCP schema told agents the log buffer holds 200 entries; it holds 500. The limit parameter description read "enforced by the ring-buffer size, typically 200" while TelescopeStore._cap is 500 and every other tool descriptor (telescope_requests, telescope_exceptions, telescope_events, telescope_gates, telescope_queries) correctly said 500. An agent budgeting a tail window against the documented number would under-read by 300 entries and conclude records had been evicted when they were still in the buffer. (lib/src/telescope_artisan_provider.dart)

  • CI could not have passed on its next run, and publish.yml would have blocked the release with it. The Flutter tool ships an analysis_options.yaml migrator that appends an analyzer.exclude block for build/ plus the six platform runner directories, and it runs on every flutter pub get. That is the first step of both ci.yml and publish.yml, so by the time dart pub publish --dry-run ran later in the same job the checkout was dirty: 1 checked-in file is modified in git, exit 65. Reproduced on a clean checkout of master with the exit code read directly rather than through a pipe. Nothing had reported it because no CI run here postdates the migrator. Both files now carry the block the migrator wants, which makes it a no-op; the hand-written comment above it survives a pub get, since the migrator reads the parsed YAML, finds the excludes present and skips the file. Reverting the file inside the workflow was rejected as the alternative: it hides the drift and leaves every contributor's tree dirty after a pub get. The same fix landed in fluttersdk_wind (#178) and fluttersdk_wind_diagnostics_contracts (#1). (analysis_options.yaml, example/analysis_options.yaml)

0.0.4 - 2026-06-17 #

Changed #

  • fluttersdk_artisan constraint bumped ^0.0.6 -> ^0.0.8. Required for co-installability with fluttersdk_dusk 0.0.7, which declares fluttersdk_artisan: ^0.0.8. Without this bump, a downstream package listing both fluttersdk_dusk: ^0.0.7 and fluttersdk_telescope would fail pub dependency resolution. No public API change; constraint only.
  • telescope:install now injects import 'package:magic_devtools/telescope.dart'; and gates the Magic-stack wiring on the magic_devtools dependency instead of the removed package:magic/telescope_integration.dart. Coordinated with the magic_devtools extraction that moved MagicTelescopeIntegration (plus the 5 Magic watchers and MagicHttpFacadeAdapter) out of the magic core package. The injected MagicTelescopeIntegration.install() call and all other wiring are unchanged.

Fixed #

  • telescope:install no longer injects the magic_devtools import when lib/main.dart has no await Magic.init( anchor. Previously, the magic-side wiring block fired for any project that listed magic_devtools in pubspec regardless of whether the app called Magic.init. On a vanilla Flutter app this left an unused import that broke dart analyze in the consumer. The block is now gated on hasMagicInit && _hasMagicDevtoolsDep() so the import and MagicTelescopeIntegration.install() call are only injected for Magic-stack apps that actually call Magic.init. The existing try/catch around injectAfterMagicInit is retained as a defensive backstop.

Documentation #

  • Synced docs, skill files, README.md, and CLAUDE.md to the magic_devtools extraction and the fluttersdk_artisan ^0.0.8 bump. The Magic-stack telescope adapter is now documented as shipping in magic_devtools (imported via package:magic_devtools/telescope.dart, added as a dev_dependency); the installation / quickstart / watchers pages, the MCP setup snippet, and the skill (SKILL.md + references) reflect the magic_devtools dependency gate and import. Dependency-version pins bumped from ^0.0.3 to ^0.0.4 across README.md, doc/getting-started/installation.md, doc/getting-started/quickstart.md, and doc/mcp/setup.md; skill version stamp bumped to 0.0.4.

0.0.3 - 2026-05-28 #

Added #

  • Skill v0.0.3: new ## 8. Community: star + issue (optional, once per session) section in skills/fluttersdk-telescope/SKILL.md plus a new skills/fluttersdk-telescope/references/community.md reference page. Trigger split: star CTA fires after the user confirms a telescope task end-to-end (captured HTTP record after a gesture, level-filtered tail slice, surfaced uncaught exception, clear-then-repro delta, or clean telescope:install); issue CTA fires only on a genuine telescope-side bug (malformed MCP envelope, kInvalidParams for documented params, TelescopeStore losing entries before the 500-cap, clear returning anything but {"cleared": true}, shipped watchers throwing on a clean install, telescope:install exiting non-zero on a fresh consumer, or registerExtensionIdempotent violating idempotency). Issue CTA explicitly excludes the documented wired-but-empty buffers, swallowed try / catch invisibility, consumer-app exceptions, raw dart:io HttpClient traffic gaps, the missing telescope_models MCP tool, and FIFO eviction past 500. Preflight gates on gh presence and auth; failure prints the URL only, no open / xdg-open / start. Both CTAs are prose-permission (not AskUserQuestion), maximum one star and one issue per session, declining one suppresses only that CTA. Labels: only bug is applied (the agent-reported label does not exist on fluttersdk/telescope, drop the flag).
  • Repo flow adopted GitHub Flow (single long-lived master; retired the develop accumulator). CLAUDE.md and .github/copilot-instructions.md now carry Golden Rule 7 plus a ## Branching section documenting task-branch naming, squash-merge policy, and the release-tag shape. delete_branch_on_merge: true enabled on origin so merged branches auto-cleanup.

Changed #

  • fluttersdk_artisan constraint bumped ^0.0.4 -> ^0.0.6. Consumers were already pulling 0.0.6 transitively (via the post-install fluttersdk_artisan: any line the telescope:install bootstrap appends to the consumer pubspec); telescope's own dev resolution now tracks the same version so tests, format, and pub publish --dry-run run against the artisan that consumers actually execute. Picks up the 0.0.5 + 0.0.6 fixes: _plugins.g.dart AOT staleness detection, MCP serverInfo.version sync to 0.0.6, atomic .mcp.json writes via .tmp + rename, the mcp:install --invocation plugin-aware fallback, and the dusk_evaluate VM-routed fix. Future artisan 0.0.7 will need a coordinated bump.
  • telescope_* MCP tool descriptions now state the actual wire shape ("oldest-first; last entry is newest"). Previously seven of the eight read tools claimed "Returns newest-first" while the handler delivered oldest-first; the SKILL.md Law 5 disclaimer ("presenter shorthand") that papered over the gap has been retired. Clients reading the description verbatim no longer assume a reversed order.
  • mcp:install fallback now writes dart run fluttersdk_telescope mcp:serve when bin/fsa is absent (via the wrapper's --invocation pass-through to artisan's mcp:install, gated on the 0.0.6 trim-whitespace behavior).
  • telescope:install no longer depends on the AOT-compiled bin/fsa. The chained subprocess calls (install + plugin:install fluttersdk_telescope) now spawn dart run fluttersdk_telescope ... directly through the telescope CLI wrapper, mirroring the Cat C subprocess pattern landed in fluttersdk_dusk. Consumers on a clean checkout (where fsa has not been compiled yet) can complete the bootstrap chain without a ProcessException: No such file or directory failure. Behavior delta: even consumers with bin/fsa scaffolded now invoke plugin:install through dart run (a few seconds slower than the fsa AOT proxy on a single telescope:install invocation). Requires dart on PATH (always true on a Flutter dev box). Matches dusk's unconditional dart run pattern for cross-plugin consistency.

Fixed #

  • telescope_clear MCP descriptor claimed it cleared "three ring buffers (http, logs, exceptions)" but the implementation has always wiped all 9 buffers atomically (per Core Law 6). Rewrote the description and Usage bullets in lib/src/telescope_artisan_provider.dart to enumerate the 9 buffers (http, logs, exceptions, events, gates, dumps, queries, caches, magic models), document the {"cleared": true} envelope, and make the upstream-sink isolation (Sentry, Bugsnag still receive events) explicit. The wire behavior was already correct; this is a descriptor-string fix only.
  • bin/fluttersdk_telescope.dart now forces collectMcpTools: true when dispatching mcp:serve, so dart run fluttersdk_telescope mcp:serve surfaces all 9 telescope_* MCP tools. Previously returned 0 plugin tools.

0.0.2 - 2026-05-22 #

Fixed #

  • CHANGELOG correction. The 0.0.1 archive on pub.dev shipped with a populated [Unreleased] block left over from release prep: every entry listed there (magic dev-dep drop, pubspec_overrides.yaml removal, test/src/magic/ deletion, magic tag cleanup in dart_test.yaml + CI workflows + agent-instruction files, example_magic/ removal, sub-barrel import path swap in telescope:install) actually shipped INSIDE 0.0.1; nothing was published before it. This 0.0.2 republishes the corrected CHANGELOG so the consolidated 0.0.1 history surfaces on pub.dev.
  • README pinned-install snippet bumped from ^0.0.1 to ^0.0.2 so the example matches the published version.

Unchanged #

  • No code, no test, no runtime behavior, no public API surface changed. lib/, bin/, test/, example/, and all 11 ext.telescope.* VM Service extensions plus 9 telescope_* MCP tools are byte-identical to 0.0.1.

0.0.1 - 2026-05-22 #

Initial public release of fluttersdk_telescope. Passive runtime inspector for Flutter apps with a framework-agnostic core and optional Magic-stack integration. Plugin of fluttersdk_artisan ^0.0.4 (hosted-only; no path overrides). Vanilla-Flutter clean: zero magic references in the production or default-test surface. Magic-stack integration is opt-in via runtime detection in telescope:install, which injects import 'package:magic/telescope_integration.dart'; and an if (kDebugMode) MagicTelescopeIntegration.install(); block after await Magic.init( when the consumer's pubspec lists magic:.

Watchers #

9 watchers across vanilla Flutter and Magic-stack:

  • LogWatcher (auto-installed): package:logging Logger calls captured to the logs ring buffer.
  • ExceptionWatcher: FlutterError.onError + PlatformDispatcher.instance.onError, chain-preserve previous handlers.
  • DumpWatcher: debugPrint capture (vanilla Flutter); debug-only.
  • MagicHttpFacadeAdapter: Magic Http facade interceptor.
  • MagicModelWatcher: ModelCreated / ModelSaved / ModelDeleted events from Magic.
  • MagicCacheWatcher: CacheHit / CacheMiss / CachePut / CacheForget / CacheFlush events.
  • MagicEventWatcher: curated event subscription (auth, db connection, gate-define).
  • MagicGateWatcher: GateAccessChecked event after every Gate.allows / Gate.denies.
  • MagicQueryWatcher: QueryExecuted event from the magic database connector.

Records #

9 immutable record types: HttpRequestRecord, LogRecordEntry, ExceptionRecord, MagicModelRecord, MagicCacheRecord, EventRecord, GateRecord, DumpRecord, QueryRecord.

9-buffer TelescopeStore #

Per-buffer Queue

VM Service extensions (11) #

ext.telescope.requests, .console, .exceptions, .events, .gates, .dumps, .queries, .caches, .clear, .pause, .resume. Every registration goes through registerExtensionIdempotent (from fluttersdk_artisan) for hot-restart safety.

MCP tools (9) #

telescope_tail, telescope_requests, telescope_exceptions, telescope_clear, telescope_events, telescope_gates, telescope_dumps, telescope_queries, telescope_caches. Each is a McpToolDescriptor const instance contributed via TelescopeArtisanProvider.mcpTools().

CLI commands (6) #

telescope:install, telescope:tail, telescope:requests, telescope:queries, telescope:caches, telescope:clear. telescope:install is a one-shot bootstrap that scaffolds the consumer artisan harness, runs plugin:install fluttersdk_telescope, and injects TelescopePlugin.install() into lib/main.dart (Magic-stack anchor or vanilla runApp anchor).

Three public contracts #

  • TelescopeWatcher: name getter + install() + uninstall().
  • TelescopeHttpAdapter: same 3-method shape + optional pendingCount getter (default 0).
  • McpToolDescriptor: const-constructible; shape owned by fluttersdk_artisan.

TelescopeStore extension surface #

  • pendingHttpCount getter sums TelescopeHttpAdapter.pendingCount across every registered adapter; consumed by ext.dusk.wait_for_network_idle for network-idle detection.

CI + automated publishing #

  • .github/workflows/ci.yml: format + analyze + flutter test (--exclude-tags integration) + 80% line-coverage floor (lcov + awk gate) + codecov upload + dart pub publish --dry-run.
  • .github/workflows/publish.yml: SemVer tag push triggers validate -> pub.dev publish via the official dart-lang/setup-dart/.github/workflows/publish.yml@v1 reusable workflow with OIDC authentication + github-release job auto-extracting CHANGELOG entry.
  • .github/dependabot.yml: weekly pub root + weekly github-actions bumps.

Documentation #

  • README.md two-path Quick Start (one-shot self-bootstrap via dart run fluttersdk_telescope telescope:install; manual wiring for consumers who prefer to drive the artisan dispatcher by hand). After install, the consumer's ./bin/fsa native AOT launcher is the recommended entry point for every subsequent telescope command.
  • doc/ tree: getting-started/, watchers/, mcp/.
  • llms.txt at repo root per llmstxt.org spec.
  • skills/fluttersdk-telescope/ LLM-agent skill (SKILL.md + 2 references).

Compatibility #

  • Dart SDK >=3.4.0 <4.0.0; Flutter >=3.22.0.
  • Platforms: Android, iOS, macOS, Linux, Windows, Web (debug-only on every platform; release builds tree-shake the entire telescope subsystem via kDebugMode gate).
  • Magic-stack integration optional. Vanilla Flutter consumers use Dio adapter + LogWatcher + ExceptionWatcher + DumpWatcher with no Magic dependency.

Test coverage #

249 tests green at release time across watchers, records, commands, extensions, and the artisan provider. 80% line coverage floor enforced in CI (current measured coverage 95.60%).

0
likes
160
points
--
downloads

Documentation

API reference

Publisher

verified publisheranilcancakir.com

Inspect a running Flutter app from an AI agent or the CLI: HTTP traffic, logs, exceptions, queries and events captured in memory, read over MCP.

Homepage
Repository (GitHub)
View/report issues

Topics

#mcp-server #ai-agents #inspector #observability #debugging

License

MIT (license)

Dependencies

dowel, flutter, logging, meta

More

Packages that depend on crumbs