1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
//! Generates the AI guide — bat-cli-owned markdown that teaches an AI assistant how to drive
//! bat-cli without reading `--help` — plus the routers that point an assistant at it.
//!
//! Everything here is **machine-global**, under `<config_dir>/ai_context/` (so
//! `~/.config/bat-cli/ai_context/`, honouring `XDG_CONFIG_HOME` / `BAT_CLI_CONFIG_DIR`).
//! rover keeps its guide per project because it owns the folder it writes into; bat-cli does
//! not — its `Bat.toml` sits at the root of somebody else's repository, and dropping four
//! generated markdown files next to it is litter in a tree the auditor did not ask us to
//! touch. The guide also describes the *binary*, not the project, so one copy per machine is
//! the honest granularity: there is nothing per project to say.
//!
//! Regenerated idempotently on every command, so it always documents the installed binary.
//! These are generated files, never the auditor's own content — overwriting them is safe.
//!
//! The split within it is deliberate:
//!
//! - **The guide is version-stamped.** Written by the running binary, so it cannot drift from
//! the code, and an assistant can see which version it is reading.
//! - **The routers are version-agnostic and byte-stable.** They carry no instructions: they
//! only say where the guide lives. Because they never change, upgrading bat-cli never
//! rewrites them, so a running assistant session never needs another restart.
use Colorize;
use fs;
use ;
use ;
;
pub type GuideResult<T> = ;
/// The guide files, written under `<config_dir>/ai_context/`.
const FILES: & = &;
/// Markers delimiting bat-cli's **managed block** inside a foreign context file. Only the
/// text between them is ever rewritten; everything the user wrote outside is preserved.
const AGENTS_BEGIN: &str = "<!-- bat-cli:agents:begin -->";
const AGENTS_END: &str = "<!-- bat-cli:agents:end -->";
/// Writes `body` to `path` only when it differs from what is on disk, to avoid churn in the
/// audited repository's git tree. Returns whether it changed the file.
/// The one place the guide lives: `<config_dir>/ai_context/`, next to the machine-wide
/// preferences and credentials rather than inside any audited repository.
/// (Re)writes the AI guide, stamping the running version into each file. Only writes a file
/// when its bytes change. Returns whether anything did.
/// Writes bat-cli's managed block into `path`: replaces it in place when the markers are
/// there, appends it to an existing foreign file otherwise (keeping everything the user
/// wrote), or creates the file. Returns whether it changed anything.
/// Installs the **global**, version-agnostic AI-assistant routers, so every bat-cli project
/// is understood with no per-project setup and — after one first-time restart — no further
/// ones:
///
/// - **Claude Code**: `~/.claude/skills/bat-cli/SKILL.md`, auto-invoked via its `description`.
/// - **Codex**: `~/.agents/skills/bat-cli/SKILL.md`, a standalone skill rather than an
/// always-on `AGENTS.md` block, so bat-cli is selected by its description.
/// - **Gemini CLI**: a managed block in `~/.gemini/GEMINI.md`, appended to whatever is there.
///
/// Best-effort and idempotent. Returns `true` only the first time the global Claude skills
/// directory is created — the one case needing a one-off `claude --continue` to watch it.
/// Refresh step run by every command: regenerate the global guide, make sure the routers
/// exist, and stamp the running version into `Bat.toml` when there is one here.
///
/// Best-effort throughout — none of this may break a command that otherwise worked, so a
/// failure only logs. Prints the one-time restart hint when the global Claude skills
/// directory had to be created.
/// Print where the AI assistant integration lives and the one line a user can say
/// to their assistant to start driving bat-cli. Shown by `bat-cli refresh-ai-guide`
/// so, right after `cargo install`, the setup is discoverable in one command.
/// Stamps the running version into `Bat.toml` when one exists here and it differs from what
/// is stored. The guide itself is global, so this is not what tells an assistant the guide
/// moved — it records **which binary last scanned this project**, which is what says whether
/// `BatMetadata.json` came from the parser you are running now. Best-effort: no `Bat.toml`,
/// or a write failure, is silently nothing. Returns whether the stamp moved.
const README: &str = r##"<!-- Generated by bat-cli {BAT_CLI_VERSION} — do not edit; regenerated on every bat-cli command. -->
# bat-cli — guide for AI assistants
You are helping a security auditor on a **bat-cli** project. bat-cli parses the smart-contract
codebase in this repository into `BatMetadata.json`, and draws a function's call graph onto a
Miro board — every function rendered as a syntax-highlighted screenshot, laid out, uploaded
already positioned, with every arrow landing on the exact line that makes the call.
The auditor drives it by talking to you ("rescan the code", "deploy `Vault.deposit` to the
board", "which external functions have no access control?") rather than by reading `--help`.
Two files at the root of the audited repository are the whole project:
| file | holds |
|---|---|
| `Bat.toml` | project type, program/`src` paths, the Miro board URL, `bat_cli_version` |
| `BatMetadata.json` | the parsed codebase, and what has already been deployed |
Screenshots are rendered to the system temp directory and deleted after upload. bat-cli
creates **no branches and no commits**: version control is the auditor's business.
Open the guide that matches the task:
- `workflow.md` — the commands, their flags, what is interactive, and the failure modes.
- `metadata.md` — the `BatMetadata.json` schema and the `jq` recipes to query it.
- `changelog.md` — what is NEW per bat-cli version; read it when the version rises.
## This guide is machine-global, not per project
It lives once per machine, in `ai_context/` next to bat-cli's own config — NOT inside the
audited repository. That is deliberate: `Bat.toml` sits at the root of a repo the auditor does
not own, and these docs describe the **binary**, not the project, so there is nothing per
project for them to say. Every bat-cli command regenerates them, so they always document the
version that is installed right now.
The per-project stamp is a different thing: `Bat.toml`'s `bat_cli_version` records **which
binary last scanned this project**, i.e. whether `BatMetadata.json` came from the parser you
are running today. If it is behind `bat-cli --version`, the scan predates your binary — run
`bat-cli sonar` before trusting the metadata, since a newer parser can extract things the old
scan simply does not contain.
## Read this guide once; re-read it ONLY when bat-cli updates
These docs only change when bat-cli's version changes — so read them once and do not keep
re-reading them:
- Note the version in this file's header comment when you first load the guide.
- Before acting later, `bat-cli --version` is a cheap check. If it is HIGHER than the version
stamped in these docs — bat-cli was updated and the next command will regenerate them —
**read `changelog.md` FIRST**. Each version entry lists what is new AND a `Re-read:` line
naming the docs that actually changed: re-open ONLY those (the union across every version
above the one you last saw), not everything. If the version is the SAME, the guide is
current — do NOT re-read it.
## Golden rules
1. **Run from the root of the audited repository**, where `Bat.toml` lives — never from the
bat-cli source checkout.
2. **A stale scan lies.** `BatMetadata.json` carries line numbers; if the source changed since
the last scan they point at the wrong lines. Re-run `bat-cli sonar` before trusting them,
and always before a deploy.
3. **You cannot answer an interactive prompt.** `init`, `login`, `config --edit` and a bare
`deploy` open `dialoguer` pickers. Hand those to the auditor (`! bat-cli init`) instead of
launching them and hanging. See `workflow.md` for what is safe to run unattended.
4. **Prefer `--dry-run` while you are checking that a graph resolves.** It computes and prints
the layout without contacting Miro, needs no login, and puts nothing on the board.
5. **Deploying is not free.** Every deploy uploads dozens of images and connectors to a shared
board; Miro slows down past about a thousand objects. Deploy what the auditor asked for,
one function at a time, and never pass `--all` on your own initiative.
6. **Query the metadata before grepping the source.** `metadata.md`'s recipes answer most
structural questions (entry points, access control, storage, the call graph) in one read.
"##;
const WORKFLOW: &str = r##"<!-- Generated by bat-cli {BAT_CLI_VERSION} — do not edit; regenerated on every bat-cli command. -->
# bat-cli — commands and workflow
## The loop
```
init ──▶ sonar ──▶ deploy
(once) (after every source change) (per function, on demand)
```
`init` scans once as its last step, so a fresh project is ready to deploy. After that,
`sonar` is what keeps `BatMetadata.json` in step with the code, and `deploy` reads it.
Check the state before acting:
```bash
ls Bat.toml BatMetadata.json 2>/dev/null; bat-cli --version
```
- no `Bat.toml` → not initialized; `init` is the first step (the auditor runs it)
- `Bat.toml` but no `BatMetadata.json` → run `bat-cli sonar`
- source changed since the last scan → `bat-cli sonar` before anything else
## Which stack is this?
`Bat.toml`'s `project_type` is one of `Anchor`, `Pinocchio`, `VanillaSolana`, `Foundry`,
`GenericRust`, and it decides what is possible:
- **`Foundry`** (Solidity/EVM) — the complete path: scan, query, deploy.
- **the SVM types** (Anchor, Pinocchio, vanilla Rust) — `init` and `sonar` work and fill
`BatMetadata.json`, but **`deploy` has no SVM path** and will fail. Say so plainly rather
than trying flags; the metadata is still worth querying.
## Commands
| command | what it does | interactive? |
|---|---|---|
| `bat-cli init` | detect the framework, write `Bat.toml`, create/pick the Miro board, then scan | **yes** |
| `bat-cli sonar` | rescan the source, rebuild `BatMetadata.json` | no |
| `bat-cli deploy` | render a function's call graph and upload it to Miro | yes unless `--entry-point` |
| `bat-cli login` / `logout` | machine-wide Miro OAuth (`--setup`, `--status`, `--force`) | **yes** (browser) |
| `bat-cli config` | show the machine preferences (`--edit` re-answers them) | only with `--edit` |
| `bat-cli update` | install the latest crates.io version (`--check`, `--force`) | no |
Machine-wide state lives in `~/.config/bat-cli/` — `config.toml` (auditor name, code editor),
`miro.toml` (the OAuth credentials, `0600`) and `ai_context/` (this guide). Override the
directory with `XDG_CONFIG_HOME` or `BAT_CLI_CONFIG_DIR`. **Authorization is per machine, not
per project**: one `bat-cli login` covers every audit on the box, and only the board URL
belongs to the project.
**When this guide appears.** Not at `cargo install` — cargo runs nothing after it builds. The
first bat-cli command you run publishes it, along with the assistant skills, and every command
after that re-checks them. `bat-cli update` publishes the new version's guide itself, by asking
the binary it just installed to do it (the updating process is the outgoing version, so it
could not). `bat-cli refresh-ai-guide` forces the same thing on demand; it needs no project.
A **project** does not catch up on its own: after an update its `BatMetadata.json` still comes
from the old parser until `bat-cli sonar` runs inside it, and `Bat.toml`'s `bat_cli_version` is
what tells you so.
`-v` / `-vv` raise the `env_logger` level (logs go to stderr); `RUST_LOG` works too.
## Interactive prompts — you cannot answer them
`login`, `config --edit`, a bare `deploy`, and a bare `init` use `dialoguer` prompts (select,
multiselect, fuzzy-select, yes/no). Do not launch those and hope. Ask the auditor to run them
in-session with the `!` prefix:
> Run `! bat-cli login` and press Accept in the browser (it also prints the URL to paste).
**`init --yes` runs with NO prompt — you can run it yourself.** The project name is the folder
name, and the Miro board is resolved without asking: it reuses an existing board with the same
name as the folder, or creates one, or attaches `--board-url <URL>`. (A Foundry project already
derives `src` from `foundry.toml`; only a bare `init` on an SVM project still asks which folders
to scan.) So the AI-drivable setup is: the auditor runs `! bat-cli login` ONCE, then you run
`bat-cli init --yes` → `bat-cli sonar` → `bat-cli deploy --entry-point <X>`.
A bare `deploy` shows a fuzzy list: entry points first and marked `[entry point]`, then every
other function, with `(deployed)` on what is already on the board — pass `--entry-point` to skip
it. If that entry point is ALREADY on the board, `deploy` asks a yes/no before drawing a second
frame — pass `--yes` to answer it and redeploy non-interactively.
Safe to run unattended: `init --yes`, `sonar`, `config` (no flag), `update --check`,
`login --status`, `deploy --entry-point <X>` (add `--yes` to redeploy, or `--dry-run`).
## deploy
```bash
bat-cli deploy # fuzzy-pick (interactive)
bat-cli deploy --entry-point Vault.deposit # Contract.function, or a bare function name
bat-cli deploy --entry-point Vault.deposit --dry-run # print the layout, contact nothing
bat-cli deploy --entry-point Vault.deposit --preview /tmp/frame.png
```
| flag | |
|---|---|
| `--entry-point <name>` | `Contract.function` or bare `function`; omit to pick from a list |
| `--dry-run` | compute and print the layout, never touch Miro (no login needed) |
| `--preview <path>` | compose the frame locally as a PNG |
| `--max-depth <n>` / `--max-nodes <n>` | bound the graph by hand; unset draws all of it |
| `--include-external` | include contracts coming from `lib/` |
| `--stroke-width <1-24>` | connector thickness in dp (default 8) |
| `--all` | every entry point at once — **discouraged**; it warns and asks first |
| `--yes` | skip the "already on the board — deploy again?" confirmation (redeploy non-interactively; builds a second frame) |
**Cross-contract resolution loop.** `deploy` follows the tree into other contracts, but a call on
an interface-typed receiver (`$.borrowerOps.adjustPosition`) has a runtime-bound target it can't
pin. So it STOPS and lists them (each with `[InterfaceType]` and in-scope candidates) rather than
silently dropping them. To include those downstream functions (and their storage markers): read
the wiring, pick the real contract, `bat-cli resolve <INTERFACE> <CONTRACT>`, and deploy again.
Each round follows what you resolved and surfaces the next layer, until the tree is complete —
resolutions live in the metadata and persist across `sonar`. `bat-cli resolve --list` shows them;
`--allow-unresolved` draws the partial graph without stopping. Standard-token interfaces
(`IERC20.balanceOf`, …) surface as name-collision candidates — those are reads, safe to skip with
`--allow-unresolved` once the real state-changing hops are resolved.
**Any function can be deployed, not just an entry point.** A shared helper needs a frame of
its own for anything else to point at, and is worth reading on its own terms.
**One frame per function, board-wide.** A function already on the board is pointed at rather
than redrawn — that is what lets several diagrams share a helper's frame. Asked for directly,
though, it is the auditor's call: a redeploy prompts yes/no, so `--entry-point` on an
already-deployed function still blocks. Add `--dry-run`, or hand the command over.
What lands on the board: one frame per entry point, one image per function already positioned,
and one connector per call site anchored to the exact line — past the end of the line when it
makes one call, on the called token itself when it makes several, since then the column is the
only thing telling them apart. The entry point sits in the top-left, layers run downward, and
no arrow points backwards.
**Storage-write markers.** Every node whose function mutates contract storage is drawn inside a
hollow red rectangle, so state-changing functions stand out at a glance. This is derived from
each function's `storage_writes` in the metadata (see `metadata.md`); nothing to configure.
Inside the rectangle, a **translucent red band** covers each exact line that writes storage
(from `storage_write_sites`), so you see WHICH state changes, statement by statement.
**External-boundary markers.** A **dashed amber band** covers a line that calls an external
contract with no in-scope source — an interface-typed receiver nothing in the repo implements
(e.g. an ERC-20 by address), via a non-view method (from `unknown_external_calls`). It means the
flow leaves the audited code and the callee MIGHT mutate its own state — unverified, so it is
deliberately distinct from the solid-red proven write. `view`/`pure` calls are never flagged.
**Frame recycling.** Redeploying a function reuses its existing frame (same id and position),
wiping and redrawing the contents — so a diagram that links to the frame by URL keeps working,
and no duplicate frames pile up. Interface/abstract stubs (bodyless declarations, empty
`virtual {}`) are never drawn on their own: a stub is redirected to its concrete override.
## Failure modes
| symptom | cause / fix |
|---|---|
| `this is already a bat project` | `Bat.toml` exists — the auditor wanted `sonar`, not `init` |
| `no entry point matched; run bat-cli sonar first` | metadata missing or stale, or the name is wrong — check the names with `jq '.entry_points[].name' BatMetadata.json` |
| deploy fails on an Anchor/Pinocchio project | expected: no SVM deploy path, only `init`/`sonar` |
| `not logged in to Miro` | the auditor runs `! bat-cli login` once per machine; `--dry-run` and `--preview` work without it |
| board creation failed during `init` | Miro's free plan caps team boards at three; `init` keeps going, set the board later |
| `No .sol files found in src/` | `init` ran outside the Foundry repository root |
| the command hangs with no output | it hit an interactive prompt — kill it and hand it to the auditor |
"##;
const METADATA: &str = r##"<!-- Generated by bat-cli {BAT_CLI_VERSION} — do not edit; regenerated on every bat-cli command. -->
# bat-cli — querying `BatMetadata.json`
The scan is the fastest way to answer a structural question about the codebase. Reach for it
before grepping `.sol` files: one `jq` read replaces a search that has to guess at naming.
**It is only as fresh as the last `bat-cli sonar`.** Every record carries line numbers; if the
source moved since the scan they point at the wrong lines. When a `line` does not match what
is in the file, rescan rather than working around it.
## Shape (Foundry / EVM)
Top level: `contracts`, `entry_points`, `function_dependencies`, `interfaces`, `file_items`,
`miro`.
- **`contracts[]`** — `metadata_id`, `name`, `file_path`, `contract_type`
(`Contract` | `Interface` | `Abstract` | `Library`), `base_contracts`, `line`, `external`,
and nested `functions`, `state_variables`, `events`, `modifiers`.
- `functions[]` — `metadata_id`, `name`, `contract_name`, `visibility`, `mutability`,
`modifiers`, `params`, `returns`, `line`, `end_line`, `is_constructor`, `is_stub` (bodyless
declaration or empty `virtual {}` — nothing to draw; redirected to its override on deploy),
`storage_writes`, `storage_write_sites`, `unresolved_calls`, `unknown_external_calls`.
- `state_variables[]` — `name`, `type_name`, `visibility`, `is_constant`, `is_immutable`, `line`.
- **`entry_points[]`** — the public/external surface: `name` (stored as `Contract.function`),
`contract_name`, `function_metadata_id`, `access_control`, `storage_reads`, `storage_writes`,
`external_calls`, `events_emitted`, `modifiers`, `dependencies`.
- **`function_dependencies[]`** — the call graph, as `function_metadata_id` → `callees[]`.
- **`interfaces[]`** — `name`, `implemented_by`, `functions`.
**`storage_writes`** is populated (empty = writes no storage) on BOTH `entry_points[]` and every
`contracts[].functions[]`. It lists the written storage locations as readable paths — a state
var (`totalSupply`), an index/mapping (`balances[]`), a storage-pointer field (`$.reserveStable`),
or an accessor path (`_s().paused`). The Miro deploy rings storage-writing nodes in red from this.
Match the exact string (the recipe below finds writers of one var). **`storage_write_sites`** is
the same writes with the FILE `line` each sits on (`{name, line}`), driving the per-line red band.
**`unknown_external_calls`** (on every `contracts[].functions[]`) lists calls on an interface-typed
receiver with NO in-scope implementer — the callee's source is not in the repo (e.g. an ERC-20 by
address), so its storage effect is unknowable. Each is `{receiver, method, inferred_type}`. The
deploy flags non-view ones with a dashed amber band; unlike `unresolved_calls`, these have no
in-scope target to `resolve`.
**`unresolved_calls`** (on every `contracts[].functions[]`) is the AI-resolution work-list for
FULL cross-contract storage coverage. Static analysis marks a function's own `storage_writes`
exactly, and follows calls to concrete contracts — but a call on an **interface-typed** receiver
(`$.borrowerOps.adjustPosition(…)`) has a target that is only bound at runtime, so it cannot be
pinned statically. Each entry is `{receiver, method, inferred_type, candidates}` — `candidates`
are the in-scope concrete contracts that plausibly implement it. **To answer "what does entry
point X change?" completely:** (1) collect `storage_writes` across X's statically-resolved call
tree, then (2) for each `unresolved_calls` entry on the way, read the WIRING (where `receiver` is
assigned — the constructor, config setter, factory, deploy script) to pick the real contract from
`candidates`, look up that method there, and recurse into ITS `storage_writes` / `unresolved_calls`.
That's the only irreducibly-dynamic step, and it's yours: static analysis narrows it to a short
candidate list; you decide the actual target from the evidence. To surface it visually, deploy the
resolved writer (`bat-cli deploy --entry-point <Contract.method>`) — its node is ringed red.
- **`miro`** — what is already on the board; `miro.auto.frames[]` holds `entry_point` and
`frame_url` per deployed frame.
Two rules that decide most queries:
- **Cross-references are by `metadata_id`** — a random 30-character string — **never by name.**
Join on the id; two contracts can define the same function name.
- **`external: true` marks anything from `lib/`.** Exclude it for scope questions: a finding in
a vendored dependency is usually out of scope.
`access_control` values: `OnlyOwner`, `{"RoleBased": {"role": …}}`,
`{"RequireMsgSender": {"compared_to": …}}`, `{"CustomModifier": {"name": …}}`, `None`.
## Recipes
```bash
# every entry point with its access control
jq -r '.entry_points[] | "\(.name) \(.access_control | tostring)"' BatMetadata.json
# entry points with NO access control — the first thing to look at
jq -r '.entry_points[] | select((.access_control | length) == 0 or .access_control == ["None"]) | .name' BatMetadata.json
# in-scope contracts only (drop lib/)
jq -r '.contracts[] | select(.external | not) | "\(.name)\t\(.file_path)"' BatMetadata.json
# one contract's functions, with visibility, mutability and line span
jq -r '.contracts[] | select(.name == "Vault") | .functions[]
| "\(.name) \(.visibility) \(.mutability) L\(.line)-\(.end_line)"' BatMetadata.json
# one contract's storage layout
jq -r '.contracts[] | select(.name == "Vault") | .state_variables[]
| "\(.type_name) \(.name) \(.visibility)"' BatMetadata.json
# which entry points write a given storage variable
jq -r --arg v "totalSupply" '.entry_points[] | select(.storage_writes | index($v)) | .name' BatMetadata.json
# entry points that make an external call (reentrancy surface)
jq -r '.entry_points[] | select((.external_calls | length) > 0)
| "\(.name): \(.external_calls | join(", "))"' BatMetadata.json
# resolve a function name to its metadata_id
jq -r '.contracts[] | .functions[] | select(.name == "deposit")
| "\(.contract_name).\(.name) \(.metadata_id)"' BatMetadata.json
# who a function calls, by name (join callees back through the contracts)
jq -r --arg id "<metadata_id>" '
. as $m
| ($m.contracts[] | .functions[] | {(.metadata_id): "\(.contract_name).\(.name)"}) as $names
| $m.function_dependencies[] | select(.function_metadata_id == $id) | .callees[]' BatMetadata.json
# what is already deployed to the board
jq -r '.miro.auto.frames[] | "\(.entry_point)\t\(.frame_url)"' BatMetadata.json
```
## SVM projects
An Anchor/Pinocchio/vanilla-Rust scan writes a different shape — `source_code`, `entry_points`,
`function_dependencies`, `traits`, `context_accounts`, `miro` — with the same `metadata_id`
discipline. There is no deploy path for it, but the same querying approach applies; inspect the
top-level keys with `jq 'keys' BatMetadata.json` before writing a recipe.
"##;
const CHANGELOG: &str = r##"<!-- Generated by bat-cli {BAT_CLI_VERSION} — do not edit; regenerated on every bat-cli command. -->
# bat-cli changelog — what's new (read this to spot new capabilities)
New bat-cli capabilities **by version, newest first**. You are running bat-cli
**{BAT_CLI_VERSION}** — everything listed at `{BAT_CLI_VERSION}` and below is available to you.
When `Bat.toml`'s `bat_cli_version` rises above the value you last saw, **read THIS file
first**: each entry lists exactly what changed AND which guide docs to re-read (`Re-read:`),
so you re-open only the docs that actually changed — not everything.
## 0.22.0
- **Exact storage-write lines on the board.** A red frame border said a function mutates state but
not WHICH. Each `contracts[].functions[]` now carries **`storage_write_sites`** — every write
with the `name` (lvalue path) and the FILE `line` it happens on. On the board, `deploy` draws a
translucent red band over each of those exact lines, so you read off precisely which state a
function changes, statement by statement. Regenerate with `sonar`, then `deploy`.
_Re-read: metadata.md, workflow.md._
- **External-boundary markers — calls to contracts whose source you don't have.** A call on an
interface-typed receiver that NOTHING in the repo implements (an ERC-20 passed by address, say)
reaches a contract with no in-scope source, so its storage effect is unknowable. Each function
now carries **`unknown_external_calls`** (interface-typed receivers only), and `deploy` marks
each such line with a DASHED AMBER band — visually distinct from the solid-red proven write: it
means "unverified external state-change boundary", not a fact. A `view`/`pure` method is never
flagged (the compiler guarantees no mutation). _Re-read: metadata.md, workflow.md._
- **No more duplicate interface/abstract screenshots.** A call resolving to a bodyless interface
declaration or an empty `virtual {}` stub was drawn as its own node next to the real one. Now a
stub is redirected to its single concrete override (`is_stub` on each function); a pure
declaration with no in-scope override is drawn as nothing. _Re-read: workflow.md._
- **Redeploy recycles the frame in place.** Deploying a function again wipes its contents and
reflows them into the SAME frame (same id/position), instead of leaving a duplicate — so links
that point at a frame by URL keep working. _Re-read: workflow.md._
## 0.21.0
- **Cross-contract storage coverage — deploy an entry point, see EVERY storage change it causes,
across contracts.** A call on an interface-typed receiver (`$.borrowerOps.adjustPosition`) has a
concrete target bound at runtime that static analysis can't pin, so those hops used to be
dropped and their downstream writes invisible. Now:
- Each `contracts[].functions[]` carries **`unresolved_calls`** — the interface hops that need
resolving, each with `inferred_type` (the receiver's interface, resolved through struct-field,
local, parameter and accessor-return types, following field chains of any depth like
`_s().CORE.owner`), in-scope `candidates`, and `assigned_in` (the functions that WRITE the
receiver — where its address is wired, so you know which candidate is real). The list is
pruned to only the hops that can actually reach a storage write.
- **`deploy` walks the whole tree and STOPS** listing the unresolved hops (transitively — the
entire tree at once) instead of drawing a partial graph. Record each with
`bat-cli resolve <INTERFACE> <CONTRACT>` (stored in the metadata's `resolutions`, preserved
across `sonar` like `miro`); deploy follows them, drawing the concrete downstream functions
with their red storage markers. `--allow-unresolved` draws the partial graph as-is.
- **Storage-write recall fix.** Functions with a MULTI-LINE signature were parsed from the wrong
line and silently lost their writes/calls; the whole function is parsed now, so their storage
writes (and everything above) are detected. Also a large internal speedup — one parse per
function instead of several. _Re-read: metadata.md, workflow.md._
## 0.19.1
- **Storage-write detection now covers writes through `storage` PARAMETERS** — a library that
takes the storage struct as a reference (`execute(CvammStorage storage $, …)` then
`$.reserveStable += …`) is the common EVM pattern and was previously missed. The red
storage-write marker on the board is also thicker/more prominent. _Re-read: nothing (metadata
shape unchanged; just more complete)._
## 0.19.0
- **Storage-write detection + diagram markers.** Each function now records the contract storage
it writes in `storage_writes` (state vars, `mapping[k]=`, storage-pointer `$.x`, accessor
`_s().x`, `++`/`--`, `delete`, `.push`/`.pop`; inherited state vars resolved). On the Miro
board, every node whose function mutates storage is drawn inside a hollow red rectangle, so
state changes stand out. Regenerate with `sonar`, then `deploy`. _Re-read: metadata.md, workflow.md._
- **`init --yes` is fully non-interactive** (for scripts / AI): project name = folder name, and
the Miro board is resolved with no prompt — it reuses an existing board named after the folder,
else creates one, else `--board-url <URL>` attaches a specific board. The AI-drivable setup is
now `! bat-cli login` (once, human) → `bat-cli init --yes` → `sonar` → `deploy --entry-point`.
_Re-read: workflow.md._
## 0.18.1
- The generated docs' header said they are regenerated on `init`/`sonar`/`deploy`; every
bat-cli command regenerates them. Wording only. _Re-read: nothing._
## 0.18.0
- **bat-cli now generates its own AI guide, once per machine.** Every command regenerates
`~/.config/bat-cli/ai_context/` (this file, `README.md`, `workflow.md`, `metadata.md`) from
the running binary, so the guide always documents the version installed right now and never
lands inside the repository being audited. It also installs a version-agnostic router —
`~/.claude/skills/bat-cli/SKILL.md`, `~/.agents/skills/bat-cli/SKILL.md`, and a managed block
in `~/.gemini/GEMINI.md` — that only says where the guide lives, so upgrading never rewrites
it and no assistant session needs a second restart.
- **`Bat.toml` carries `bat_cli_version`**, stamped by `init`/`sonar`/`deploy`: which binary
last scanned this project, and therefore whether `BatMetadata.json` came from the parser you
are running today.
- **`bat-cli update` now publishes the new version's guide immediately**, by asking the binary
it just installed to regenerate it — the updating process is the outgoing version, so the
guide used to describe the replaced version until you happened to run something else.
`bat-cli refresh-ai-guide` does the same on demand and needs no project.
_Re-read: README.md, workflow.md, metadata.md._
"##;
/// The global Claude Code / Codex skill. Byte-stable and version-agnostic on purpose: it is a
/// router, not the instructions, so upgrading bat-cli never rewrites it and a running session
/// never needs another restart.
const GLOBAL_SKILL_MD: &str = r##"---
name: bat-cli
description: Drive bat-cli, the Blockchain Auditor Toolkit that parses a smart-contract codebase into BatMetadata.json and draws its call graph onto a Miro board. Use whenever the user mentions bat-cli, or wants to "init the bat project", "run sonar", "rescan the metadata", "deploy this function/entry point to Miro", "draw the call graph", "preview the diagram", or wants to query the parsed codebase (entry points, call graph, storage, access control) in a folder that contains a Bat.toml.
---
bat-cli parses the smart-contract codebase of an audited repository into `BatMetadata.json`,
and draws a function's call graph onto a Miro board — every function a syntax-highlighted
screenshot, laid out and uploaded already positioned, with every arrow landing on the exact
line that makes the call.
**You are in a bat-cli project when the current folder — or any parent — contains a
`Bat.toml`.** If there is none, this is not a bat-cli project; do nothing bat-cli-specific
(beyond telling the user `bat-cli init` is what creates one). Run every bat-cli command from
the directory holding `Bat.toml`.
**The authoritative instructions live in one machine-global guide**, written by the installed
binary — not in the audited repository. Read it once, then stay current cheaply; do NOT
re-read everything before every action:
1. The guide is `~/.config/bat-cli/ai_context/` (or `$XDG_CONFIG_HOME/bat-cli/ai_context/`, or
`$BAT_CLI_CONFIG_DIR/ai_context/` when either is set). Read it and follow `README.md`'s
"read once" rules from then on:
- `README.md` — what bat-cli is, and the golden rules
- `workflow.md` — the commands, their flags, what is interactive, failure modes
- `metadata.md` — the `BatMetadata.json` schema and `jq` recipes
- `changelog.md` — what is NEW per version; your source of truth for new capabilities
2. **`changelog.md` is how you learn what changed.** The docs carry the version that generated
them in their header. If `bat-cli --version` is HIGHER, bat-cli was updated: read
`changelog.md` FIRST — each entry lists the new capabilities AND a `Re-read:` line naming
exactly which docs changed, so you re-open only those. If it matches, the guide is current.
3. `Bat.toml`'s `bat_cli_version` is a different signal: it says which binary last scanned
THIS project. Behind `bat-cli --version` means `BatMetadata.json` predates your parser —
run `bat-cli sonar` before trusting it.
If the guide directory does not exist, no bat-cli command has run since it was installed:
run `bat-cli refresh-ai-guide` (harmless, needs no project) to publish it, then read it.
Non-negotiables: run from the project root; never answer an interactive `dialoguer` prompt by
guessing — hand those commands to the user; prefer `--dry-run` while checking a graph, since a
real deploy puts dozens of objects on a shared board.
"##;
/// Body of the managed block in `~/.gemini/GEMINI.md`. Same router, same byte-stability.
const GLOBAL_AGENTS_BODY: &str = r##"## bat-cli projects — instructions for AI coding agents
bat-cli parses the smart-contract codebase of an audited repository into `BatMetadata.json`,
and draws a function's call graph onto a Miro board.
**You are in a bat-cli project when the current folder — or any parent — contains a
`Bat.toml`.** If there is none, ignore this section. Run every bat-cli command from the
directory holding `Bat.toml`.
**The authoritative instructions live in one machine-global guide**, written by the installed
binary — not in the audited repository. Read it once, then stay current cheaply:
1. The guide is `~/.config/bat-cli/ai_context/` (or under `$XDG_CONFIG_HOME/bat-cli` /
`$BAT_CLI_CONFIG_DIR` when either is set): `README.md`, `workflow.md`, `metadata.md`,
`changelog.md`. Follow README's "read once" rules thereafter. If the directory does not
exist, run `bat-cli refresh-ai-guide` once to publish it.
2. **`changelog.md` is how you learn what changed.** The docs carry the version that generated
them. If `bat-cli --version` is HIGHER, read `changelog.md` FIRST — each entry lists the new
capabilities AND a `Re-read:` line naming exactly which docs changed, so you re-open only
those.
3. `Bat.toml`'s `bat_cli_version` says which binary last scanned THIS project; behind
`bat-cli --version` means the metadata predates your parser — run `bat-cli sonar`.
Non-negotiables: run from the project root; never guess at an interactive prompt — hand those
commands to the user; prefer `--dry-run` while checking a graph.
"##;