crumbs 0.0.13
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 doweland rename the imports (package:fluttersdk_telescope/telescope.dartispackage:crumbs/crumbs.dart) and theTelescope*identifiers; (2) follow the dowel 0.0.21 migration (move.artisan/to.dowel/, delete.dowel/plugins.jsonand.dowel/installed/fluttersdk_telescope.json), deletebin/dispatcher.dart,bin/fsa,lib/app/_plugins.g.dartandlib/app/commands/_index.g.dart(the installers keep a file that exists, and the old ones importfluttersdk_artisan, so./bin/dowelwould not build), replacepackage:fluttersdk_artisan/artisan.dartwithpackage:dowel/dowel.dartandArtisan*withDowel*in your own commands, rundart run crumbs crumbs:installagain, thendart run dowel commands:refreshto register your commands in the new index; (3) renametelescope_*tocrumbs_*in any saved MCP allow list, prompt or script (a Claude Code allow-list entrymcp__fluttersdk__telescope_*becomesmcp__dowel__crumbs_*, since the server key is nowdowel),telescope:*tocrumbs:*in CLI calls andTELESCOPE_DISABLEtoCRUMBS_DISABLEin a build that must not register the extensions; (4) delete the stalefluttersdkserver entry from.mcp.json(dowel leaves entries an earlier release wrote) and reconnect the MCP client; (5) in a Magic app, until amagic_devtoolsrelease built on crumbs ships,MagicTelescopeIntegrationfeedsfluttersdk_telescope, not crumbs, so the HTTP, event, gate, query and cache buffers of crumbs stay empty; then upgrademagic_devtoolsand rerundart run crumbs crumbs:install(see the installer entry below). - Renamed from
fluttersdk_telescopetocrumbs. The package, the repository (anilcancakir/crumbs), the barrels (package:crumbs/crumbs.dartandpackage:crumbs/cli.dart), everyTelescope*class (nowCrumbs*, andTelescopeArtisanProviderisCrumbsDowelProvider), the executable (dart run crumbs) and the file names carry the new name. Also renamed: theext.telescope.*VM Service extensions (nowext.crumbs.*), the MCP tools (telescope_*iscrumbs_*), the CLI commands (telescope:*iscrumbs:*), the environment variable (TELESCOPE_DISABLEisCRUMBS_DISABLE), the MCP registry name (io.github.anilcancakir/crumbs), the skill (fluttersdk-telescopeiscrumbs) 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.dartin favour ofmaterial_ui, which needs that floor; the example app now builds onmaterial_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 lastfluttersdk_telescoperelease. - Built on
dowel.fluttersdk_artisan ^0.0.20is nowdowel ^0.0.21. The.mcp.jsonserver key written bymcp:installisdowel. - The Magic step of
crumbs:installneeds amagic_devtoolsbuilt on crumbs. When the consumer's pubspec namesmagic_devtoolsand the resolved release shipspackage:magic_devtools/crumbs.dart, the installer wiresMagicCrumbsIntegrationfrom it. Themagic_devtoolsreleases that still shipMagicTelescopeIntegrationhave no such library, so with one of them the installer skips the Magic lines and warns; upgrademagic_devtoolsto a release built on crumbs and rerundart run crumbs crumbs:install(see Fixed).
Fixed #
crumbs:installwires Magic only when the installedmagic_devtoolsshipscrumbs.dart. The pubspec entry alone decided it, so amagic_devtoolsrelease that predates crumbs got an import of a library it does not have and alib/main.dartthat did not compile. The installer now resolves the package through the nearest.dart_tool/package_config.jsonat or above the project (a pub workspace member uses the one at the workspace root; an unreadable or invalid one counts as unresolved) and, withoutlib/crumbs.dart, skips the Magic lines and warns to upgrade and rerun. (lib/src/commands/crumbs_install_command.dart)crumbs:installresolves amagic_devtoolspath dependency outside the project. The walk to.dart_tool/package_config.jsonkept a trailing separator, so a relativerootUrithat climbs out of the project (../../magic_devtoolsforpath: ../magic_devtools) resolved one level too deep; a release that shipslib/crumbs.dartread 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:installleaves alib/main.dartthat already callsCrumbsPlugin.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 anddebugPrinttwice. AnyCrumbsPlugin.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_dumpsandcrumbs_exceptionstool descriptions say what the watchers capture.DumpWatcheroverridesdebugPrintonly, so a plainprint()is not captured (the description said it was);ExceptionWatcherhooksPlatformDispatcher.onErroras well asFlutterError.onError. The--limithelp ofcrumbs:queriesandcrumbs:cachessays the newest N come back oldest first, which is the order they print in. (lib/src/crumbs_dowel_provider.dart,lib/src/commands/) - The
DioHttpAdapterdocs and thecrumbs_requeststool description describe the adapter that ships. The class doc showed aDioHttpAdapter(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 callsDioHttpAdapter.recordRequest. (lib/src/adapters/dio_http_adapter.dart,lib/src/crumbs_dowel_provider.dart)
Removed #
- The
FluttersdkTelescopeArtisanProvideralias inpackage:crumbs/cli.dart. The class dowel'splugins:refreshgenerates 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_artisanmoves^0.0.19to^0.0.20. The old range already admitted 0.0.20, so a freshpub getresolves 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: atelescope:*command run while the app is still booting, beforeext.telescope.*is registered, now prints one retry line and exits 75 instead of an RPC stack and exit 3. The requirements tables indoc/getting-started/and the stack lines inCLAUDE.mdand.github/copilot-instructions.mdquote 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,recordExceptionandrecordHttp(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 aredactedgetter (true when a redactor ran), serialized intoJson. Only the store's redaction pass sets it: the constructors take noredactedargument andHttpRequestRecord.copyWithreturns 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,redactExceptionRecordandredactEventRecordjoinredactHttpRecordas the per-record entry points, each returning null for a dropped record, andresetForTesting()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.setFramePerfCapacitynow calls theframePerfkind. (lib/src/telescope_store.dart) LogRecordEntry.atUs, a monotonic timestamp. The sameFlutterTimeline.nowclockEventRecord.atUsuses, stamped at construction (LogRecordEntry.fromLogRecordincluded) and serialized intoJson. (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 fromonLogRecord,onEventRecordandonExceptionRecordas one line,{"kind": "log"|"event"|"exception", ...toJson()}, totimeline-<launch stamp>-<n>.jsonl, batched at 64 lines or 500 ms. It starts a new file before a line would passmaxFileBytesand deletes the oldest timeline files in the directory, earlier launches included, beyondmaxFiles. Fail closed: a record withredactedfalse is never written, only counted indroppedUnredacted. A batch write no caller awaits that fails stops the sink instead of raising an uncaught error, counted inwriteFailures, 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 towardmaxFileBytes, rather than truncating it.flush()writes the batch,stop()flushes and closes, andTelescopeFileSink.currentis the running sink. The package looks no directory up. (lib/src/telescope_file_sink.dart)- A cursor and filters on
ext.telescope.eventsandext.telescope.console. Both acceptsince(microseconds, exclusive, compared withatUs); events alsotype(prefix ofeventType), console alsologger(exact name). The filters run beforelimit: withsince,limitkeeps the OLDEST N after it, extended to the end of the run of records sharing the last one'satUs(the page may exceedlimitby those ties), so paging bycursorskips nothing; withoutsinceit keeps the newest N of what matched. Both answer acursor: the largestatUsreturned, else the givensince, else null, so passing it back assinceyields only newer records. Asincethat is not an integer is refused withinvalidParamsrather than read as "from the start". (lib/src/extensions/register_telescope_extensions.dart) ext.telescope.filesandext.telescope.file. Read the runningTelescopeFileSink.current:filesanswers{files: [{name, bytes}]}newest first,filetakesname,offsetandmaxBytesand answers{lines, next}. Both answer{error}when no sink runs, andfileanswers 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,--jsonand--followontelescope:eventsandtelescope:tail.--jsonprints one JSON object per line, the payload as JSON;--followre-reads every second from the returned cursor until interrupted. (lib/src/commands/)followCursor, the one--followloop.followCursor(read:, since:, interval:, delay:, shouldStop:)callsreadwith the cursor the previous round returned and waitsintervalbetween rounds; a null cursor keeps the last one. Pure Dart, exported frompackage:fluttersdk_telescope/cli.dart, and now behind bothtelescope:eventsandtelescope: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.
recordEventstores a valuejsonEncodecannot write (aDateTime, aDuration, any object) as itstoString(), and a non-String map key likewise, so one bad payload can no longer fail the wholeext.telescope.eventsresponse. A payload that is already safe keeps its identity. (lib/src/telescope_redaction.dart) toJsonof the log, event, exception and HTTP records always carriesredacted. (lib/src/records/)- BREAKING:
TelescopeRedaction.redactHttpRecordreturnsHttpRequestRecord?. It wasHttpRequestRecord; 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
minLevelmatched every log line.recentLogs(minLevel: 'bogus')now returns an empty list instead of the whole buffer. (lib/src/telescope_store.dart) - A negative
limitthrew. EveryrecentX(limit:)returns an empty list for a zero or negative limit instead of aRangeError. (lib/src/telescope_store.dart) - A payload
jsonEncodecannot write failed the wholeext.telescope.eventsresponse. 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_artisanmoves^0.0.17to^0.0.19. The old range already admitted 0.0.19, so a freshpub getresolves 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'sxmlconstraint 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_iconsto^2.0.0. It touches only the unpublishedexample/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,MagicCacheRecordandFramePerfRecordall gainatUs(anint? atUsconstructor param stored asatUs ?? FlutterTimeline.now, comparable across record types regardless of wall-clock skew),interactionId(String?) andlinkedBy(String?, one ofzone/frame/window), all three serialized intoJson.HttpRequestRecordadditionally carriesrequestId,startUsandendUsfor exact request/response pairing.FramePerfRecordgainsvsyncStartUsfromFrameTiming.timestampInMicroseconds(FramePhase.vsyncStart); its doc comment marks clock parity withatUsas needing live validation, since the engine documents that timestamp only as microseconds "from some epoch" and a widget test can only inject syntheticFrameTimingvalues, never a real engine-reported one. Every block in aFramePerfRecord.blocksmap gainsselfMicros:FramePerfWatcher's drain now computes each block's exclusive duration fromAggregatedTimings.timedBlocksstart/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 inclusivemicros.telescope:framesandtelescope:requestsprint the new fields,linkedByon its own when there is nointeractionId(thewindowcase). Every new constructor parameter is optional or defaulted, so a constructor call that does not passblocksby an explicit record type keeps compiling (magic_devtools/lib/src/telescope_integration.dartdoes); theblockstype is the one exception, below. The skill'srecords.md,mcp-tools.mdandcli-commands.mddocument 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, andHttpRequestRecord.copyWith.hideRequestHeaders,hideRequestParametersandhideResponseParametersadd names to the defaults (Laravel Telescope merges the same way, it never replaces),hiddenRequestHeaders/hiddenRequestParameters/hiddenResponseParametersread 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 bymask('********'), never mutating its input, andredactBody(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 aMap.toString()nor a cut JSON string parses, and the store cannot mask what it cannot parse.copyWithreplacesrequestHeaders,requestBodyandresponseBodyand keeps every other field,atUsincluded. (lib/src/telescope_redaction.dart,lib/src/internal/redaction_lists.dart,lib/src/records/http_request_record.dart)
Changed #
-
BREAKING:
FramePerfRecord.blocksisMap<String, ({int micros, int selfMicros, int count})>. It wasMap<String, ({int micros, int count})>, and Dart record types with different fields are not assignable to each other, so code that builds aFramePerfRecordfrom a map of the old record type, or declares a variable of the old type fromrecord.blocks, stops compiling. AddselfMicrosto the record literal (the inclusivemicrosis a safe value when the nesting is unknown) or let the type be inferred. The JSON shape only gains a key, so a reader ofext.telescope.framesoutput is unaffected. (lib/src/records/frame_perf_record.dart) -
The artisan floor names this batch's release.
fluttersdk_artisanmoves^0.0.16to^0.0.17. The old range already admitted 0.0.17, so a freshpub getresolves nothing differently; what changes is that the floor names the release this package is verified against. artisan 0.0.17 changesstop,restartand a failedstart --cdp-portto 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.requestsand MCP, and a Magic app's bearer header, a login body'spasswordand a login answer's Sanctumtokenall landed in it as sent.TelescopeStore.recordHttpnow masks, before buffering and before emitting ononHttpRecord: the value of every request header namedauthorization,proxy-authorization,cookie,set-cookieorx-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 asuser[password]orcodes[]matches on its last part, as a nested JSON key does), the value underpassword,password_confirmation,current_password,new_password,token,access_token,refresh_token,secret,client_secret,authorization_code,id_token,two_factor_tokenorrecovery_code(request) andtoken,access_token,refresh_token,plain_text_token,secret,client_secret,id_token,two_factor_token,recovery_codes,qr_urlorqr_svg(response), at any depth.qr_urlandqr_svgare on the list because both carry the TOTP secret thatsecretmasks. 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:gatesandtelescope: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 ordebugPrintrecords at all. Each command reads itsext.telescope.*extension, takes--limit(default 50), prints one line per record and warns on an empty buffer, the same shape astelescope:queries.exceptionsstays MCP-only;dusk:exceptionsreads 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 carriesrepositoryandwebsiteUrland deliberately nopackagesblock:registryTypedocuments npm, pypi, oci, nuget and mcpb with no pub equivalent, and bothpackagesandremotesare optional onServerDetail, which requires only name, description and version. Nothing in the package reads the file and no workflow publishes it, sotest/server_json_version_test.dartguards the version againstpubspec.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_artisanfloor moves^0.0.8to^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 freshpub get; what changes is that the floor names the release this package is verified against. The requirements tables indoc/getting-started/index.mdanddoc/getting-started/installation.mdsaid^0.0.2andCLAUDE.mdsaid^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 fieldsFrameTimingexposes (buildMicros,rasterMicros,vsyncOverheadMicros,totalSpanMicros) plus a per-frame block-attribution map.TelescopeStore.recordFramePerf/.recentFramePerf/.onFramePerfRecordfollow the shape of the existing nine buffers, with one deliberate difference: the buffer reads its ownsetFramePerfCapacity(default 3600, about a minute at 60fps) instead of the sharedsetCapacity, 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 fromSchedulerBinding.addTimingsCallback, which carriesFrameTiming.frameNumberbut arrives late and batched (roughly 100ms later on web). Per-frame attribution comes from drainingFlutterTimelineat 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
addPostFrameCallbackis 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.handleDrawFramewraps the whole post-frame phase in its ownPOST_FRAMEspan, so an inline collect runs with a non-empty nesting stack and tripsassert(_stackPointer == 0)inFlutterTimeline. 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
livenessCounterincremented once per frame drawn. It is the only reliable proof the engine is rendering:SchedulerBinding.framesEnabledwas measured reportingtrue, with aresumedlifecycle and an armedonReportTimings, on a Chrome page that had produced one frame in two seconds. The watcher is opt-in and is NOT auto-installed byTelescopePlugin.install(); register it withTelescopePlugin.registerWatcher(FramePerfWatcher()). (lib/src/watchers/frame_perf_watcher.dart,lib/telescope.dart) -
ext.telescope.frames,telescope:framesandtelescope_frames, the wire surface for the new frame-perf buffer. The extension returns the buffer's records alongsideFramePerfWatcher.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 saysframes, notframe_perf, even though the Dart types keep that prefix to name the record and the watcher.TelescopeArtisanProvidernow 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/aiclimbed 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_dispatchstays 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.ymldeclaredrelease: [published], butpublish.ymlcreates the release withsoftprops/action-gh-releaseunder the defaultGITHUB_TOKEN, and GitHub does not start workflow runs from events raised by that token. The run history confirms it: noreleaseevent has ever appeared there, only the manual run and the retiredpushtrigger. 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, becausepublish.ymlgoes green either way and the only symptom is a user installing a skill one release behind.publish.ymlnow calls the workflow directly withneeds: github-release, which removes the cross-workflow event and sequences the dispatch after pub.dev has accepted the release rather than alongside it;workflow_callreplaces the dead trigger andworkflow_dispatchstays 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 thevprefix, so that branch never matched a real tag and always fell through to readingpubspec.yaml. (.github/workflows/dispatch-to-registry.yml,.github/workflows/publish.yml) -
doc/mcp/tool-reference.mdstill 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 eightlimitrows, plus a "typically 200-500 depending on buffer type" capacity note.limitreturns the most recent N records in chronological order (_trimtakeslist.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. Thelimitparameter description read "enforced by the ring-buffer size, typically 200" whileTelescopeStore._capis 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.ymlwould have blocked the release with it. The Flutter tool ships ananalysis_options.yamlmigrator that appends ananalyzer.excludeblock forbuild/plus the six platform runner directories, and it runs on everyflutter pub get. That is the first step of bothci.ymlandpublish.yml, so by the timedart pub publish --dry-runran later in the same job the checkout was dirty:1 checked-in file is modified in git, exit 65. Reproduced on a clean checkout ofmasterwith 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 apub 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 apub get. The same fix landed influttersdk_wind(#178) andfluttersdk_wind_diagnostics_contracts(#1). (analysis_options.yaml,example/analysis_options.yaml)
0.0.4 - 2026-06-17 #
Changed #
fluttersdk_artisanconstraint bumped^0.0.6->^0.0.8. Required for co-installability withfluttersdk_dusk0.0.7, which declaresfluttersdk_artisan: ^0.0.8. Without this bump, a downstream package listing bothfluttersdk_dusk: ^0.0.7andfluttersdk_telescopewould fail pub dependency resolution. No public API change; constraint only.telescope:installnow injectsimport 'package:magic_devtools/telescope.dart';and gates the Magic-stack wiring on themagic_devtoolsdependency instead of the removedpackage:magic/telescope_integration.dart. Coordinated with themagic_devtoolsextraction that movedMagicTelescopeIntegration(plus the 5 Magic watchers andMagicHttpFacadeAdapter) out of the magic core package. The injectedMagicTelescopeIntegration.install()call and all other wiring are unchanged.
Fixed #
telescope:installno longer injects themagic_devtoolsimport whenlib/main.darthas noawait Magic.init(anchor. Previously, the magic-side wiring block fired for any project that listedmagic_devtoolsin pubspec regardless of whether the app calledMagic.init. On a vanilla Flutter app this left an unused import that brokedart analyzein the consumer. The block is now gated onhasMagicInit && _hasMagicDevtoolsDep()so the import andMagicTelescopeIntegration.install()call are only injected for Magic-stack apps that actually callMagic.init. The existing try/catch aroundinjectAfterMagicInitis retained as a defensive backstop.
Documentation #
- Synced docs, skill files,
README.md, andCLAUDE.mdto themagic_devtoolsextraction and thefluttersdk_artisan ^0.0.8bump. The Magic-stack telescope adapter is now documented as shipping inmagic_devtools(imported viapackage: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 themagic_devtoolsdependency gate and import. Dependency-version pins bumped from^0.0.3to^0.0.4acrossREADME.md,doc/getting-started/installation.md,doc/getting-started/quickstart.md, anddoc/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 inskills/fluttersdk-telescope/SKILL.mdplus a newskills/fluttersdk-telescope/references/community.mdreference 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 cleantelescope:install); issue CTA fires only on a genuine telescope-side bug (malformed MCP envelope,kInvalidParamsfor documented params,TelescopeStorelosing entries before the 500-cap,clearreturning anything but{"cleared": true}, shipped watchers throwing on a clean install,telescope:installexiting non-zero on a fresh consumer, orregisterExtensionIdempotentviolating idempotency). Issue CTA explicitly excludes the documented wired-but-empty buffers, swallowedtry / catchinvisibility, consumer-app exceptions, rawdart:io HttpClienttraffic gaps, the missingtelescope_modelsMCP tool, and FIFO eviction past 500. Preflight gates onghpresence and auth; failure prints the URL only, noopen/xdg-open/start. Both CTAs are prose-permission (notAskUserQuestion), maximum one star and one issue per session, declining one suppresses only that CTA. Labels: onlybugis applied (theagent-reportedlabel does not exist onfluttersdk/telescope, drop the flag). - Repo flow adopted GitHub Flow (single long-lived
master; retired thedevelopaccumulator).CLAUDE.mdand.github/copilot-instructions.mdnow carry Golden Rule 7 plus a## Branchingsection documenting task-branch naming, squash-merge policy, and the release-tag shape.delete_branch_on_merge: trueenabled on origin so merged branches auto-cleanup.
Changed #
fluttersdk_artisanconstraint bumped^0.0.4->^0.0.6. Consumers were already pulling 0.0.6 transitively (via the post-installfluttersdk_artisan: anyline the telescope:install bootstrap appends to the consumer pubspec); telescope's own dev resolution now tracks the same version so tests, format, andpub publish --dry-runrun against the artisan that consumers actually execute. Picks up the 0.0.5 + 0.0.6 fixes:_plugins.g.dartAOT staleness detection, MCPserverInfo.versionsync to0.0.6, atomic.mcp.jsonwrites via.tmp+ rename, themcp:install --invocationplugin-aware fallback, and thedusk_evaluateVM-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:installfallback now writesdart run fluttersdk_telescope mcp:servewhenbin/fsais absent (via the wrapper's--invocationpass-through to artisan'smcp:install, gated on the 0.0.6 trim-whitespace behavior).telescope:installno longer depends on the AOT-compiledbin/fsa. The chained subprocess calls (install+plugin:install fluttersdk_telescope) now spawndart run fluttersdk_telescope ...directly through the telescope CLI wrapper, mirroring the Cat C subprocess pattern landed influttersdk_dusk. Consumers on a clean checkout (where fsa has not been compiled yet) can complete the bootstrap chain without aProcessException: No such file or directoryfailure. Behavior delta: even consumers withbin/fsascaffolded now invokeplugin:installthroughdart run(a few seconds slower than the fsa AOT proxy on a singletelescope:installinvocation). RequiresdartonPATH(always true on a Flutter dev box). Matches dusk's unconditionaldart runpattern for cross-plugin consistency.
Fixed #
telescope_clearMCP 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 inlib/src/telescope_artisan_provider.dartto 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.dartnow forcescollectMcpTools: truewhen dispatchingmcp:serve, sodart run fluttersdk_telescope mcp:servesurfaces all 9telescope_*MCP tools. Previously returned 0 plugin tools.
0.0.2 - 2026-05-22 #
Fixed #
- CHANGELOG correction. The
0.0.1archive on pub.dev shipped with a populated[Unreleased]block left over from release prep: every entry listed there (magic dev-dep drop,pubspec_overrides.yamlremoval,test/src/magic/deletion,magictag cleanup indart_test.yaml+ CI workflows + agent-instruction files,example_magic/removal, sub-barrel import path swap intelescope:install) actually shipped INSIDE0.0.1; nothing was published before it. This0.0.2republishes the corrected CHANGELOG so the consolidated0.0.1history surfaces on pub.dev. - README pinned-install snippet bumped from
^0.0.1to^0.0.2so 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 11ext.telescope.*VM Service extensions plus 9telescope_*MCP tools are byte-identical to0.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:loggingLogger calls captured to thelogsring buffer.ExceptionWatcher:FlutterError.onError+PlatformDispatcher.instance.onError, chain-preserve previous handlers.DumpWatcher:debugPrintcapture (vanilla Flutter); debug-only.MagicHttpFacadeAdapter: MagicHttpfacade interceptor.MagicModelWatcher:ModelCreated/ModelSaved/ModelDeletedevents from Magic.MagicCacheWatcher:CacheHit/CacheMiss/CachePut/CacheForget/CacheFlushevents.MagicEventWatcher: curated event subscription (auth, db connection, gate-define).MagicGateWatcher:GateAccessCheckedevent after everyGate.allows/Gate.denies.MagicQueryWatcher:QueryExecutedevent 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:namegetter +install()+uninstall().TelescopeHttpAdapter: same 3-method shape + optionalpendingCountgetter (default 0).McpToolDescriptor: const-constructible; shape owned byfluttersdk_artisan.
TelescopeStore extension surface #
pendingHttpCountgetter sumsTelescopeHttpAdapter.pendingCountacross every registered adapter; consumed byext.dusk.wait_for_network_idlefor 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 officialdart-lang/setup-dart/.github/workflows/publish.yml@v1reusable workflow with OIDC authentication + github-release job auto-extracting CHANGELOG entry..github/dependabot.yml: weekly pub root + weekly github-actions bumps.
Documentation #
README.mdtwo-path Quick Start (one-shot self-bootstrap viadart run fluttersdk_telescope telescope:install; manual wiring for consumers who prefer to drive the artisan dispatcher by hand). After install, the consumer's./bin/fsanative AOT launcher is the recommended entry point for every subsequent telescope command.doc/tree:getting-started/,watchers/,mcp/.llms.txtat 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
kDebugModegate). - 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%).