npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

canopy-engine

v0.1.18

Published

Canopy: a map of what a JavaScript / TypeScript codebase does - entry points, call flows and services, drawn from the code itself.

Readme

Canopy

Beta. JS/TS only. Canopy is an early release for testers. It reads JavaScript and TypeScript projects; other languages are not read.

Canopy reads a JavaScript or TypeScript codebase and draws a map of what it does: where execution starts (routes, event listeners, socket handlers, the app's main file), what each of those calls, in order, and how the parts of a project (services) fit together. The map is built from the code itself.

JavaScript and TypeScript only. Exactly which files are read is listed in "Which files are analysed" below.

Install (needs Node 18 or newer)

1. Try it, always the latest, nothing to install. Run this inside your project folder:

npx -y canopy-engine@latest   # needs the network

2. Install for regular use. Once, then use the short name:

npm install -g canopy-engine   # needs the network
canopy

Without installing anything, npx canopy-engine runs the same program (it downloads it the first time). Do not type npx install -g canopy-engine: npx install is not a command. The install command is npm install -g canopy-engine, and then you run canopy.

(The two lines marked "needs the network" download from the npm registry. If you were given a file instead, canopy-engine-<version>.tgz, install that: npm install -g ./canopy-engine-<version>.tgz.)

If the canopy command clashes with another tool on your computer, use canopy-engine instead. It is the same program under a second name: canopy-engine --version, canopy-engine doctor, and so on.

What canopy does

canopy does these steps in order and prints a plain line for each; if one fails it stops with one line saying what happened and what to do:

  1. Checks this computer - Node 18+, a folder you can write to, free disk space (300 MB), git, a free port (the same checks as canopy doctor).
  2. Looks at your project - counts the JavaScript / TypeScript files. A folder with none (for example a Python-only project) stops here: "Canopy currently supports JavaScript and TypeScript".
  3. Prepares its output folder - outside your project, in your user cache folder (see "What is in the store" below). Nothing is written inside your project and .gitignore is not touched. A .canopy/ folder left by an older Canopy is copied once and listed in .git/info/exclude; Canopy never deletes it.
  4. Plans auto-update on commit - never asks. After the map opens, Canopy adds its block to your git post-commit and post-merge hooks in the background (10 s limit; your own hook content is kept). Skip it with --no-hook or CANOPY_NO_HOOK=1. See "Hook on and off" below.
  5. Builds the map - the first time takes seconds to minutes depending on size (a project of ten thousand files takes some minutes); a later run on an unchanged project answers "up to date" at once, and after an edit it re-reads only the changed files and links the whole project again. It ends with a short summary: services, workflows, files inside a service, files with parse errors.
  6. Opens the map - starts a viewer on a free local port (127.0.0.1 only) and opens your browser. Ctrl+C stops it.

Then it prints "Next time just run canopy." and the feedback link. Options: -y / --yes (no questions; installs the hook), --no-hook (no hook, no question), --no-open (build the map, do not start the viewer; for scripts), --port=N, --for=SECONDS (stop the viewer by itself), --full (ignore everything remembered and analyse from scratch), --quiet (print only problems and the address), --no-color (or set NO_COLOR=1; also used automatically when the output is not a terminal), and a folder to look at instead of the current one: canopy path/to/project.

Supported languages: JavaScript and TypeScript only (see "Which files are analysed" below).

Several folders at once

canopy <folderA> <folderB> [<folderC> ...] analyses each folder on its own, exactly as canopy <folder> does (each keeps its own stored versions and cache, and a folder whose files did not change is not analysed again), and then records the links between the folders: a call in one folder (a fetch, axios and similar call with a method and a path) matched to a route of another folder by method and path. The folders do not need a common parent folder or git.

  • Links carry the usual tags described under "Links between services": exact when the call and the route are written out in full, and inferred when, for example, the base address is an environment variable nothing names.
  • The result is a workspace file in Canopy's store folder (workspaces/<id>/workspace.json), printed at the end of the run with the links. Run it again and, if no folder changed, the links are reused and it finishes much faster. The maps are still opened one folder at a time (canopy open <folder>); there is no combined viewer yet.
  • A folder given twice is counted once (with a note); a folder inside another is refused (give only the outer one); a folder that does not exist is named. Up to 8 folders.
  • Your AI tool can ask canopy_workspace (through canopy mcp) for the folders and links of the workspace a project belongs to.

Which files are analysed

Read from the source of the engine, not guessed:

  • Parsed as JavaScript: .js .jsx .mjs .cjs. Parsed as TypeScript: .ts .mts .cts (and .tsx with the TSX grammar).
  • Never skipped: no source file is left out because of its size, a very long line, a name like *.min.js or *.bundle.js, or because it is a declaration file (.d.ts). A file that cannot be parsed completely is still a file on the map and is listed in the coverage summary as partly analysed.
  • Folders that are not read: everything inside node_modules, .git, dist, build, out, coverage, vendor, .next, .nuxt, .turbo, .cache, .yarn, .pnp, bower_components and anything your .gitignore lists. The source files inside them are counted in the coverage summary.
  • .html is read only to find the page's <script src> (the start of a browser app); its other contents are not analysed.
  • .vue and .svelte files are skipped, not parsed: they are counted ("not read yet: N files in languages Canopy does not support"), they do not fail the run, and their code does not appear on the map.
  • Every other file type (.py, .go, .css, .json, ...) is ignored.

Large projects, memory and what happens when it still fails

  • A whole analysis runs in a second Node process that is given about 75% of this computer's memory (at least 2 GB, at most 8 GB). Set CANOPY_MAX_MEMORY_MB=6144 (any number from 256) to choose it yourself; an existing --max-old-space-size in NODE_OPTIONS is respected. CANOPY_DEBUG_MEM=1 prints the memory use at each stage (to the terminal only).
  • Big projects. The number of helpers that read files is max(1, min(cores - 1, free memory in MB * 0.6 / 800)) (at most 8); the first lines of a run say how many and why, and CANOPY_WORKERS=<n> overrides it. Every stage logs its time and memory, a line appears every 30 seconds while a long stage runs, and canopy sync --profile also saves stage-timings.json in the store. Two limits exist and are always disclosed (a list at the end of the run, in the store's meta.json under limits, and a small dismissible banner in the viewer): CANOPY_MAX_FANOUT=<n> (off unless you set it) keeps only the first n outgoing links of a function, and a run whose memory passes the smaller of 4 GB and 70% of this computer's memory stops with a plain message and exit code 3, saving nothing (CANOPY_MAX_RSS_MB; 0 turns it off).
  • If Canopy still runs out of memory, or the process ends abnormally, it says so in plain words with what to try next (analyse one package or app at a time with canopy <folder>, give it more memory, close other programs) and exits with a non-zero code. It never ends silently. last-error.txt in the project's store folder then holds the time, the Canopy version, the stage it was in, the exit code and the memory setting, and nothing else (no code, no secrets).
  • Nothing is lost by a crash: every file already read is remembered, so the next run starts where the last one stopped. A run that was stopped leaves no stale lock behind (a lock whose process has ended is removed, and the run says so).

What Canopy remembers

In the project's store folder (in your user cache folder, not inside your project), on this computer only: the versions of your code it has analysed, which files it has already read, and which version you last looked at. That is what lets the Changes view start with "Since you last opened Canopy, 2 days ago (12 commits)" instead of only the last commit. The "last looked at" mark moves only after the page has loaded and stayed open for a minute, or when you stop Canopy normally; a crash does not move it. Delete the project's store folder (see "What is in the store") to start over; a damaged store is moved aside (state.corrupt-<time>) and rebuilt, and Canopy says so. If you run canopy again in the same project while its viewer is open, it opens the running one instead of starting a second.

Updating

canopy update --check   # needs the network
  • canopy update --check says whether a newer version is on npm and changes nothing: "up to date (x.y.z)" or "x.y.z available (you have a.b.c). Run: canopy update".
  • canopy update installs the newest version with npm install -g canopy-engine@latest, shows npm's output, then runs the new canopy --version to confirm it. If anything goes wrong it prints the exact command to run yourself. If you run Canopy with npx -y canopy-engine@latest, you always get the newest version and there is nothing to update.
  • A one-line notice. When a newer version is known, an interactive command prints one line to stderr before its output: "A newer Canopy is available: 0.1.8 -> 0.1.9. Update with: canopy update". It is read from a small file in your user config folder (update-check.json); when that file is more than a day old, Canopy refreshes it in the background (2-second limit, silent on any failure) by asking registry.npmjs.org for the public package metadata of canopy-engine, sending no identifiers. Nothing is printed with --quiet, in CI, when output is not a terminal, from a source checkout, or by canopy mcp and the commit hook. CANOPY_NO_UPDATE_CHECK=1 turns the check and the notice off. canopy --version shows 0.1.9 (latest: x.y.z) from the same file.
  • No silent updates, ever. Canopy never downloads, installs or changes itself on its own: canopy update shows the current and the newest version, asks you to confirm (--yes skips the question), then runs npm.
  • When the map opens in your browser, the page makes one request to registry.npmjs.org (the same version number, nothing about you or your project) and, if a newer version exists, shows a small notice at the bottom left: "Canopy x.y.z is available. In your terminal run: canopy update". You can close it; it stays closed for the rest of that browser session. To turn the request off completely, start Canopy with the environment variable CANOPY_NO_UPDATE_CHECK=1 (the page then gets no address to ask and makes no request).

Commands

canopy --version
canopy --help
canopy doctor
canopy sync .
canopy history
canopy diff
canopy open --for=3
canopy hook install
canopy hook uninstall
  • canopy update [--check] - see "Updating" above.
  • canopy [path] - the one-command journey above.
  • canopy sync [path] - analyze, and store a new version only if the code changed.
  • canopy open [path] - serve the viewer on the latest output. --port=N picks the port; --for=SECONDS stops by itself after that time (for scripts).
  • canopy doctor [path] - OK / FAIL per check (Node version, grammars, queries, write access, git, viewer, hook), each with the fix.
  • canopy history, canopy diff [from] [to] [--json] - the stored versions and what changed between two of them.
  • canopy hook install / canopy hook uninstall - see below.
  • canopy ai [path] - which Claude Code the Ask AI panel would use (found automatically). Claude Code is the supported provider. The panel also lists Codex CLI (marked experimental), local model servers on your own computer such as Ollama (experimental, off until you turn them on under Change in the panel; they answer only from Canopy's search results), and Antigravity CLI and Gemini CLI, which are listed but stay off because they cannot be limited to Canopy's read-only tools for a single run.
  • canopy telemetry on|essentials|off|status|show|test - usage statistics you choose at a terminal; they carry a random install ID, so they are pseudonymous, not anonymous (see Privacy). essentials = a daily ping, on = Essentials plus advanced counts, off = nothing is sent or stored, status = your state and queue, show = the last payloads exactly as sent, test = one marked test event.
  • canopy mcp [path] - a read-only server your own AI tool can use (see "Search, and help from your own AI tool").
  • canopy install [path] [--yes] / canopy uninstall [path] - connect your AI tool to it, and remove that again.

Search, and help from your own AI tool

Search (the box at the top of the viewer; press /). It answers from an index kept in the store's search/ folder, the same way every time, with nothing sent anywhere. Try:

| type | finds | |---|---| | post v1 events trigger | the route POST /events/trigger (a leading v1 or api is optional: frameworks add it) | | GET /api/users/:id or get users {id} | that route (:id, {id} and [id] are the same thing) | | sendEmail or send email | the function (camelCase, snake_case and acronyms are split into words) | | "hooks.example.com" | a quoted phrase must appear exactly: in a string, URL, queue name or comment | | create order | also finds post, add, send (a short fixed list of synonyms), one typo in a longer word (evnts), and the start of a word (trig) | | STRIPE_KEY | the file that reads that environment variable (names only; values are never stored) |

Results are grouped Endpoints, Functions, Workflows, Files; each shows why it matched (route, name, decorator, string, comment, path) and "appears in N workflows" - choose one when there are several. Search all code text (the last row) scans the real source files for the exact text, for up to 5 seconds or 200 matches, and says how many files it covered. Files that hold secrets (.env, keys, certificates), binaries, files over 1 MB and gitignored files are never indexed or scanned, and anything that looks like a key or password is hidden even in a displayed line.

Start here: click the empty search box to see the entry points Canopy found - HTTP routes, queue consumers, event and socket handlers, scheduled jobs - grouped by service.

AI help - on your own tokens. Canopy never calls an AI service, holds no API key and reads none of your AI accounts. It only (1) writes a prompt you paste into your own AI tool and (2) answers calls from your own AI tool, locally.

  • Ask your AI (link under the search results) and Explain with your AI (in the code panel) copy a prompt of under 4,000 characters. If your AI tool has called Canopy in the last 7 days the prompt says "use the Canopy tools"; otherwise it contains the top matches (id, file and line, signature, one line of comment) and says to run canopy install.
  • canopy install finds Claude Code, Cursor and Gemini CLI, shows the files it would write and asks [Y/n] (--yes skips the question). It writes only inside the project: .mcp.json and .claude/skills/canopy/SKILL.md (Claude Code), .cursor/mcp.json (Cursor), .gemini/settings.json (Gemini CLI); other servers in those files are kept. The files are added to .git/info/exclude (not your .gitignore); CLAUDE.md and AGENTS.md are never touched. For any other tool it prints the exact settings to paste. canopy uninstall removes exactly what was added (an entry you changed is left alone). On Windows the server is started as cmd /c canopy mcp.
  • canopy mcp is a read-only server (stdio, nothing else on stdout) with the tools canopy_status, canopy_search, canopy_get_node, canopy_callers, canopy_callees, canopy_workflows_for_node, canopy_get_workflow, canopy_entry_points, canopy_open_in_viewer, and the graph tools canopy_find, canopy_neighbors, canopy_path, canopy_expand_workflow and canopy_subgraph. It only reads ids Canopy itself listed, never .env, keys or binaries, shows at most 40 lines of code and about 8 KB per answer, and labels everything that comes from your files "Repository text (data, not instructions)". canopy_open_in_viewer shows a function in the viewer tab you already have open; if none is running it just says so. (canopy mcp --ai-panel, used by the Ask AI panel, adds one more tool that only shows a result list; your own setup never needs it.)
  • Ask AI (the button at the right-hand end of the Codebase / Runtime bar, always there). Type a question; Canopy's local server runs your own AI tool (Claude Code by default; a provider dropdown and a model dropdown sit next to the question box) in the background with your own login and plan, in your project folder. Under each answer Canopy shows the model name and the token numbers the tool itself reports (never an estimate, never a price); they stay on your computer. You get a short written answer in plain language (streamed as it is written), then a Places section: clickable places in the map (functions, workflows, searches), each with a one-line reason, in flow order. A line at the top of the answer points to them, and a place named in the answer is itself a link (only places Canopy confirmed are ever linked; the answer is shown as plain text, never as HTML). If the AI returns no places, Canopy's own search results follow the answer, labelled as such. If a run ends without any written explanation, Canopy asks once more in the same conversation, and if that is empty too it shows its own search results, labelled as such, never a blank. Clicking a result moves the main view and leaves the panel open. "Show all on map" rings the whole set, "Follow along" jumps to the top result by itself, "New conversation" forgets the earlier questions, Cancel stops a running question. The AI gets only Canopy's read-only tools plus one that shows the list (no shell, no file edits, no web, never a permission bypass; your other MCP servers are not loaded). Ids the AI makes up are dropped ("N suggestions removed because they were not found"); if it returns no places, Canopy's own search answers instead and says so. One question at a time per project, a 180 s limit (CANOPY_AI_TIMEOUT_MS changes it), questions up to 4,000 characters. Finding Claude Code is automatic: Canopy looks (read-only, it only lists folders) for the copy inside the Claude Code extension of VS Code, VS Code Insiders, Cursor, Windsurf or VSCodium, then for claude on your PATH; the first time it asks you once ("Found Claude Code from VS Code (v2.1.304). Use it for Ask AI? [Allow] [Not now]") and remembers the path in prefs.json in the store folder (a path only). The panel footer says which copy is in use, with a "Change" link to pick another or paste a path; canopy ai prints the same line. Canopy never calls an AI service itself and never reads a credential: the command line is built as an array, your question travels on stdin, and only the id of the Claude session is kept (ai-session.json in the store folder) so follow-up questions remember earlier ones. If no Claude Code is found (or it is too old) the panel says so, links to the install page and offers "Copy prompt instead". CANOPY_CLAUDE_BIN (a full path) overrides everything. If Claude is not signed in, sign in to Claude Code in VS Code and try again. Claude Code is the default. Codex CLI and a local model server (an address you type) are experimental and off until you turn them on; Antigravity CLI and Gemini CLI are only detected and stay off, because Canopy could not confirm that a run can be limited to its read-only tools.
  • Experimental deep links. Put {"experimentalDeepLinks": true} in prefs.json in the store folder to add two buttons to the Ask panel, "Open in Claude Code" and "Open in VS Code" (they open your own tool with the prompt filled in). They are labelled "experimental, may not work on every setup" and do nothing Canopy controls.

Links between services

When a project has more than one service (a frontend and a backend, a server and a realtime server), Canopy links the calls that cross between them:

  • HTTP: a fetch, axios (also axios.create({ baseURL }) instances), ky, got or XMLHttpRequest call with a method and a path is matched to a route of another service. Paths with parameters (/users/${id} and /users/:id), a base URL in a variable (${API_URL}/users) and wrapper functions (request("POST", "/friends/request"), where the link starts at the function that calls the wrapper) are followed.
  • WebSocket: new WebSocket(url) / io(url) is linked to the service that runs a WebSocket server (a socket.io client only to a socket.io server), a socket.send(...) whose message is built at run time to that server's message listener, and, the other way round, what the server sends over the socket to the message handler of the client that connects to it (always "inferred": the message itself is not read).

Every link says how sure Canopy is: exact means the client URL is written out in full (no variable base URL, no parameters), the route is written out in full, and the method and path are equal on both sides; resolved means the base URL was followed to the service that owns it, using the project's own configuration files (an environment variable read from .env, a docker-compose service name or port, a dev-server proxy, or a server that listens on that port), and a route there matches; the link says which variable and which file; inferred is everything else (a base URL it had to guess, parameters matched loosely, a leading /api ignored, only the path matched).

Canopy reads .env and deploy-config files locally to resolve service URLs; it never stores or transmits secret values. The files are .env (and .env.local, .env.development, .env.production, .env.example), docker-compose*.yml, render.yaml, fly.toml, railway.json / railway.toml, vercel.json (rewrites and redirects), netlify.toml (redirects), app.json (Heroku), Dockerfile (EXPOSE and ENV), the dev-server proxy of vite.config / next.config, and the proxy field of package.json. They are read as plain text and never run. Only URL-like and port-like values are looked at (anything else is dropped as it is read), and only the variable name, the file name and the address without user name, password or query string can appear in the output. The links are in output/systemGraph.js in the store folder (crossServiceEdges, with confidence, method, url, reason, and for resolved links resolvedFrom), the service-to-service counts in serviceLinks (exact / resolved / inferred), and the steps that cross carry crossesInto in the viewer.

In the viewer, a step that calls into another service is joined by an edge to a small box in that service (what is called, which function receives it). The edge is solid for exact and resolved links and dashed for inferred ones; hover it for the tag and the reason (for a resolved link, the variable and the file). "View in {service}" on the box opens the receiving workflow, and the start of that workflow shows "Called from {service}: {file}" (click to go back to the caller). Back returns to the step you left.

A package.json bin or main that points at build output (dist/cli.js) now starts the map at the source file it was built from, when the repository itself says which one (a sourcemap, tsconfig outDir/rootDir, a dist -> src layout, or a build script that names the source such as esbuild src/cli.js).

What breaks if I change this?

Open the code panel of a function (the </> button on its card) and press What breaks if I change this? Canopy walks up the callers, up to three levels, lights up the functions it reaches on the map and dims the rest. A bar at the bottom says how many there are (direct, indirect, in other services) and has a Clear button.

  • Direct calls the function itself; indirect reaches it through other functions; cross-service crosses a service boundary (an HTTP call, a queue). Every link keeps its confidence tag: exact, resolved or inferred.
  • It lists at least these: calls made at run time (dynamic dispatch, callbacks, events) may be missed.
  • Your AI tool can ask the same question through canopy mcp: canopy_blast_radius takes a function id, or an environment variable name (env: every function that reads process.env.NAME or import.meta.env.NAME, then their callers), and an optional depth (1 to 8, default 3). The answer is grouped by service, lists ids, names, file and line, and stops at 500 nodes ("+N more").

Calls Canopy could not link, and functions nobody calls

Open the code panel of a function (the </> button on its card). Under the code, a short section says how many of its calls Canopy linked and how many it could not, for example "3 calls linked, 1 not resolved". Show lists the calls it could not link, each with a reason: dynamic property call, computed member (handlers[name]()), callback parameter, unknown import, external library, or other. Ask AI to trace this opens Ask AI with those calls written into the question; if your AI tool is set up, the question is asked at once, otherwise it waits in the box.

  • A function that nothing in the project calls and that is not a route, listener or other entry point is shown with "No caller found, likely called at runtime". In the saved data it is tied to its own file by a separate kind of link (definedIn); it is not a call, and it is not counted among the call links.
  • Your AI tool can ask the same through canopy mcp: canopy_unresolved takes a function id (node) or a workflow (workflow, like workflow:3) and answers in a few lines.
  • The data is in unresolved-calls.json in the viewer's output folder (it is new; no existing file changed). Set CANOPY_NO_UNRESOLVED=1 to skip it.

Realtime links (socket events)

Canopy already links a fetch or axios call to the route it reaches. It now also links realtime messages across a client and a server when the event name is written out as a string: a client's socket.emit("chat:send", ...) is linked to the server function that handles "chat:send", and the server's io.to(room).emit("chat:broadcast", ...) or socket.broadcast.emit(...) to the client function that handles it.

  • Detected: socket.io (server and client), ws (a ws.send(JSON.stringify({ type: "x" })) and a handler that branches on msg.type / msg.event, only when the value is a literal), and Node EventEmitter emit / on inside the project.
  • In the map the link is a dashed edge labelled emits 'chat:send' between the two services (best seen in the Runtime view of a workflow); the receiving side is described as handles 'chat:send' in the hover text. Links are inferred (matched by the event name only), unless the same emitter object is used on both sides, which is resolved.
  • Built-in names (connect, disconnect, error, close, data, end, open, message without a literal type, and similar) never create links. A name built at run time (a variable) is not linked; it is counted as unresolved in realtime-links.json.
  • At most 50 links are kept per event name (CANOPY_MAX_EVENT_LINKS changes this); the file says how many were not shown. A handler written inline is linked as the handler node Canopy already makes for it, or else through the function that registers it.
  • The details of every link (also those inside one service) are in realtime-links.json in the viewer's output folder. CANOPY_NO_REALTIME=1 turns the linking off.

Hook on and off

The hook starts canopy sync in the background after every commit and merge so the map stays current. It never blocks or fails a commit and prints nothing when it works. When it runs it looks for Canopy in this order: canopy on your PATH; else npx --yes canopy-engine@<the version you installed>; else it prints one line, canopy not found; skipping map update, and carries on.

  • Turn it on: it is set up automatically in the background after the map opens, or run canopy hook install (safe to repeat; your own hook content is kept).
  • Turn it off: canopy hook remove (removes only Canopy's block). --no-hook or CANOPY_NO_HOOK=1 skips the automatic set-up.

When it doesn't work

Run canopy doctor and send its output with your feedback. Healthy output (an example; the version, folders and counts differ on your computer; the hook line is not critical):

canopy 0.1.5 doctor (C:\work\my-project)

OK    Node version >= 18 - v22.12.0
OK    tree-sitter grammars (wasm) - 5 files
OK    query files (.scm) - 17 files
OK    write access to the Canopy cache folder - %LOCALAPPDATA%\canopy\projects\6fa0d25bdca29d7a
OK    git available - git version 2.45.1.windows.1
OK    map viewer present
FAIL  post-commit hook installed
      fix: canopy hook install "C:\work\my-project"  (not critical)

all critical checks passed

A critical problem looks like this, and canopy doctor then exits with 1:

FAIL  write access to the Canopy cache folder - EEXIST: file already exists, mkdir '%LOCALAPPDATA%\canopy'
      fix: make the Canopy cache folder writable for your user, or set CANOPY_HOME to a folder you own

1 critical problem(s) - fix them and run canopy doctor again

Exit codes of canopy: 0 ok, 1 a problem (the message says what to do), 3 no JavaScript / TypeScript files.

Queue links are inferred. When one function adds a job to a queue (queue.add, BullMQ or Bull, @InjectQueue, or a wrapper class) and another processes the same queue (new Worker, queue.process, a NestJS @Processor class), Canopy draws a dashed link, "queue (inferred)", between them - but only when the queue's name is known for sure (a string, a constant, an enum member). A name built at run time gives no link. These links do not change any workflow.

What is in the store

The store is one folder per project, outside the project: %LOCALAPPDATA%\canopy\projects\<hash> on Windows, ~/Library/Caches/canopy/projects/<hash> on macOS, $XDG_CACHE_HOME/canopy/projects/<hash> (else ~/.cache/canopy/projects/<hash>) on Linux. <hash> comes from the project's full path, and project.json in the folder says which project it is. CANOPY_HOME moves the whole canopy folder. Moving or renaming a project folder starts a new store (the old one stays where it was).

| file | what it is | |---|---| | output/workflows.js | every traced workflow with its steps, calls and source - what the viewer reads (workflows.json is the same data as plain JSON when it fits under 25 MB) | | output/systemGraph.js | links between workflows and between services | | output/services.js | the services found (frontend, backend, ...) with counts | | output/meta.js, meta.json | repo root and the coverage report (files analyzed, skipped, with parse errors) | | output/history.js | the version list and recent diffs shown in the viewer's Changes panel | | history/*.json.gz | one compressed snapshot per version (total size capped at 200 MB, CANOPY_SNAPSHOT_MAX_MB) | | sync.log | log of background syncs started by the hook | | meta.json | the project's random id, when it was first set up, which version you last looked at (checksummed) | | files.json | size, time and hash of each analysed file, so an unchanged project is recognised without re-reading it | | server.json | the address of the viewer while it runs (so a second canopy reuses it), and a key only canopy mcp can use to ask it to show something | | search/ | the search index (index.json, rebuilt after every sync; files/ holds one small cache entry per file) | | ai-session.json | the id of the Claude Code conversation the Ask AI panel continues (an id only; "New conversation" deletes it) | | install.json, mcp-seen.json | what canopy install wrote (so uninstall can remove exactly that); when your AI tool last called canopy mcp | | last-error.txt | only after a crash: time, version, stage, exit code, memory setting |

The viewer in five bullets

  • First screen: the services of the project and the links between them; click one to list its workflows.
  • A workflow opens as a chain of cards: the entry point (a route, a listener, the main file) and the functions it calls, in source order, with branches (if, switch, try) shown side by side.
  • Click a call to open it as its own card; Trace from root opens everything beneath the entry point (it pauses at 3,000 rows and offers Continue).
  • The </> button on any card shows the exact source lines.
  • The Changes panel starts with what changed since you last opened Canopy, and can also step through one commit at a time; click a changed function for its line-by-line diff.
  • Runtime (predicted) draws a workflow end to end across the services it calls, one band per service.

Mental Model: join two parts of your code and draw how they connect

The Mental Model button (next to Ask AI) draws a flowchart of how one part of your code reaches another. No AI is involved. Search for a function or route and choose Start and End (or Add several nodes), then Build diagram. Canopy joins them with the links it already found (calls, routes, queues; the shortest path of up to 8 links, or the real links among a set of nodes) and draws them as a Mermaid flowchart.

  • Click a box to open that node in the normal code panel. A dashed box stands for several nodes (it is used when a diagram would have more than about 25 boxes): click it to list the nodes inside, open any of them, or Show all on the map.
  • Copy as Mermaid copies the diagram text. The same input always gives the same text.
  • A saved diagram holds only the node ids and any links you added yourself, never copies of your code. It is drawn again from the current map each time it opens, so a renamed or deleted function shows up as changed or missing.
  • The drawing library (Mermaid) loads only when you first draw a diagram, runs in strict mode, and every label is escaped. If no path of real links exists, the two nodes are drawn alone and Canopy says so; it never invents a link.

Mental Model: ask your AI to compose one, and see which links are proven

In the Mental Model panel you can also type a question ("what happens after an order is placed?") and choose Compose with AI. It needs the same AI tool as Ask AI; if none is installed or signed in, the panel says so and the start-and-end Diagram keeps working.

The AI never draws anything. It names parts of your code (by the ids Canopy gives it) and how they connect, as plain data. Canopy checks that every part exists, draws the diagram itself, and sorts every link into one of three kinds:

  • Solid: Canopy found the link in your code on its own (directly, or as a path of up to 6 steps).
  • Thick blue, "Connected by AI, checked in the code": Canopy had no link, so it showed the code of both parts to the AI in one batch and asked it to quote the exact lines that make the second part run (a call, an event name, a route or URL, an import). Canopy then checked on its own that the quote really is at that line in the first part and really points at the second. The quote is shown under the diagram.
  • Faint dashed, "AI says connected, not confirmed": the AI was unsure, said no, or its quote did not check out. A switch hides these. A link is never dropped just because Canopy has no link of its own: the code check decides.

If the code of either part changes later, the link is listed as not confirmed ("the code changed") until you check again. At most 12 links without a Canopy link are checked per diagram, with at most 60 lines of each part shown to the AI. Verified results are kept on your computer, and a short log of verified links stays in the project's Canopy folder; nothing is sent anywhere by this feature.

Mental Model: a bigger picture and plainer words

The Mental Model is easier to read now.

  • Expand (or a click on the picture) opens it nearly full screen. Drag to move, use the mouse wheel, a trackpad pinch or the + and - buttons to zoom, Fit to see it all, double-click to start over, Esc or Close to leave. A click on a box opens the code panel.
  • Boxes have short everyday names (at most 6 words) and links have at most 4. The real function name, file and line are in the code panel, not on the picture.
  • One row under the picture says what the lines mean, with a count for each: solid = found in your code, thick blue = connected by AI and checked in the code, faint dashed = AI says connected but it is not confirmed, dotted = added by you. Details about links that are not confirmed are folded away until you open them.
  • When the AI says two parts are connected, Canopy first looks at its own links: a direct link, or a path of up to 6 steps, is drawn as a normal solid link ("in 3 steps" when it is more than one). Only the rest is checked against the code the AI quotes.
  • A line under the picture says Click any box to see it in your code, and a small pointer shows it once; a click on a box takes you to that part of your code on the map, zoomed so it is readable, and outlines it (a ring that pulses for about 2 seconds, then a steady outline). If Runtime does not show it, Canopy says so and shows it in Codebase.
  • Nothing is loaded from the internet: the drawing library ships with Canopy and is loaded only when you first open a Mental Model.

Privacy

Canopy runs on your computer and Canopy itself never sends your source code anywhere. The network features are: the update check (CANOPY_NO_UPDATE_CHECK=1 turns it off); and usage statistics that you choose (pseudonymous: they carry a random install ID). The first time you run Canopy at a terminal it asks one question: [1] Essentials (the default: one ping a day with the Canopy version, your operating system family, the Node version, a random install ID and the local date) or [2] Yes (Essentials plus feature-usage counts, coarse timing buckets and error categories; never code, paths, repository or project names, searches, prompts or answers). Without a terminal (a script, a pipe, CI, the background sync after a commit) Canopy never asks, and it sends something only if your choice is already stored. canopy telemetry off, CANOPY_TELEMETRY=0 and DO_NOT_TRACK=1 stop everything and never ask. canopy telemetry show prints the last payloads exactly as sent, status shows the state and queue, and test sends one marked test event. Each send is one HTTPS request with a 3-second limit made in the background (Canopy never waits more than 300 ms for it at start or exit); a failed send waits in a local retry queue (at most 50 events, 7 days) for the next launch, and every event sent or queued is also written to a local log you can read. Your choice and install ID are in config.json in your user config folder (%APPDATA%\canopy\ on Windows, ~/.config/canopy/ elsewhere), the log and queue under telemetry/ in the Canopy home folder. If you use Ask AI, the AI tool you choose runs on your computer under your own login, and the parts of your code that Canopy's tools return may be processed by that tool's provider under your own account and that provider's terms. See PRIVACY.md for the exact list.

Known limits

General limits of this version:

  • JavaScript and TypeScript only; other languages are silently skipped (a Python-only folder gives an empty map).
  • Links between services are found for HTTP calls with a readable method and path and for WebSocket connections; links are "resolved" only when the project's own files say which service a base URL belongs to, otherwise "inferred" (the base URL is a variable). Calls whose URL is built at run time are not linked; a message a server sends to a browser is linked to the client's message handler only when that handler calls into the project, and the link is always "inferred".
  • Method calls on objects mostly stay unresolved; plain function calls and imports resolve better.
  • Flow type annotations are not supported: files that use them show syntax errors and are only partly analysed.
  • Anonymous exports have no node, so calls into them are not followed.
  • Very large repositories (thousands of files) take minutes the first time.
  • Vue and Svelte files are skipped (see "Which files are analysed").

Known issues

  • Mainly tested on Windows; macOS and Linux are expected to work but have had far less testing.
  • Ask AI: Claude Code is supported; other providers are not yet.
  • Code edits by AI are not part of this version.
  • JavaScript and TypeScript only; Vue and Svelte files are skipped.
  • Accuracy figures (how many links are found and how many are right) are not yet published.
  • Mental Model with AI is new and has been tried on few projects.

Troubleshooting

  • canopy doctor first. It exits non-zero only when a critical check fails (Node, grammars, queries, write access).
  • "no map yet": run canopy sync <path> (or just canopy) before canopy open <path>.
  • A commit prints canopy not found; skipping map update: neither canopy nor npx is on the PATH git uses; install Canopy globally (npm i -g canopy-engine) or run canopy hook uninstall.
  • Parse errors in a few files are normal; the coverage block printed by canopy sync lists them and the rest of each file is still analyzed.
  • The viewer answers "forbidden" (403): it only accepts a 127.0.0.1 or localhost address. A forwarded port opened under another host name (Codespaces, dev containers, ssh -L with a custom name) is refused on purpose; open http://127.0.0.1:<port>/ instead.

License

Canopy is free for personal, educational and evaluation use on your own computer; commercial use (inside a company or for paid work) needs a separate written agreement. The full terms are in the LICENSE file in this package.

Feedback

The viewer has a small Send feedback link, and canopy sync ends with a one-line hint: both open a mailto: link in your own mail program with the subject "Canopy feedback (v, )" and an empty message. Canopy collects and sends nothing; you decide whether to send. (--quiet leaves the hint out.)

For a longer report please use FEEDBACK.md (about 20 minutes) and send it back, with the output of canopy doctor, to [email protected]. canopy also prints the feedback link from canopyFeedbackUrl in package.json.