erdraft 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -2
  3. package/dist/chunk-IQ5YYSCP.js +2464 -0
  4. package/dist/chunk-VSN3XJPO.js +99 -0
  5. package/dist/cli.js +758 -0
  6. package/dist/inspect/index.js +373 -0
  7. package/dist/inspect/logger.js +64 -0
  8. package/dist/read-schema-child.js +238 -0
  9. package/dist/studio-config-TCMRAVMR.js +32 -0
  10. package/dist/types/core/fields.d.ts +39 -0
  11. package/dist/types/inspect/field-of.d.ts +3 -0
  12. package/dist/types/inspect/holds-value.d.ts +2 -0
  13. package/dist/types/inspect/inspect-logger.d.ts +2 -0
  14. package/dist/types/inspect/inspector.d.ts +5 -0
  15. package/dist/types/inspect/listen-on-document.d.ts +5 -0
  16. package/dist/types/inspect/read-write.d.ts +5 -0
  17. package/dist/types/inspect/use-field-changes.d.ts +6 -0
  18. package/dist/types/inspect/watch-storage.d.ts +8 -0
  19. package/dist/window/apple-touch-icon.png +0 -0
  20. package/dist/window/assets/index-BCt7ERad.css +2 -0
  21. package/dist/window/assets/index-v2x3QhC-.js +132 -0
  22. package/dist/window/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  23. package/dist/window/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  24. package/dist/window/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  25. package/dist/window/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  26. package/dist/window/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  27. package/dist/window/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  28. package/dist/window/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  29. package/dist/window/favicon.ico +0 -0
  30. package/dist/window/favicon.png +0 -0
  31. package/dist/window/favicon.svg +1 -0
  32. package/dist/window/index.html +23 -0
  33. package/package.json +97 -3
  34. package/skill/SKILL.md +47 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erdraft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,222 @@
1
- # Temporary Holding Version
1
+ # Erdraft
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A project's data model, drafted with an agent while a person watches. The agent reads the schema and proposes changes through Erdraft's MCP (or its CLI); each proposal is a **draft**, drawn at once in a window beside the schema the code has, its tables and columns marked added, changed or removed. Asked for ideas, the agent proposes two or three **alternatives** in one group, compared a tab each, an arrow at each end of the tabs going to the one before or after (the tabs scroll where there are more than the card holds). The person **adopts** one; only then does the agent write it in the code. Erdraft never changes the code nor the database.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install --save-dev erdraft
9
+ ```
10
+
11
+ It needs Node 22 or later and a project whose schema is written with [Drizzle](https://orm.drizzle.team) for Postgres (`drizzle-orm` 0.44 or later); the inspector, React 19.2 or later. The window's assistant runs [Claude Code](https://claude.com/claude-code), when it is installed; everything else works without it. It has been tried on macOS; Linux and Windows not yet.
12
+
13
+ With pnpm, allow esbuild's install script when it asks (`pnpm approve-builds`): tsx, which reads the schema's TypeScript, needs it.
14
+
15
+ ## Set it up
16
+
17
+ From the project's root:
18
+
19
+ ```sh
20
+ npx erdraft init # .erdraft.json, Erdraft's MCP in .mcp.json, the skill for the project's agents
21
+ npx erdraft studio # the window, at http://localhost:9369 (the schema read from the code as it opens)
22
+ ```
23
+
24
+ `init` finds the schema in the module drizzle-kit's config names (`drizzle.config.ts` and its kin, at the root or in a package of `packages/` or `apps/`), else where schemas usually are (`src/db/schema.ts`, `src/server/db/schema.ts`...), and names the project after its `package.json`, so every checkout of it shares its drafts whatever its folder is called. It reads the schema before it ends, to show it is one Erdraft reads, and says what to do next. Give it the module when it finds none, or when drizzle-kit's config names several or a glob (Erdraft reads one module that exports every table), and another name: `npx erdraft init src/db/index.ts --name Acme`. Run again, it keeps what is there; `--force` writes `.erdraft.json` and the skill over.
25
+
26
+ What it writes is a `.erdraft.json`, which says where the schema is and the project's name, its drafts kept by it:
27
+
28
+ ```json
29
+ {
30
+ "name": "Acme",
31
+ "schema": "src/db/schema.ts"
32
+ }
33
+ ```
34
+
35
+ and Erdraft's server beside the others in Claude Code's `.mcp.json`; any agent that speaks MCP over stdio runs `erdraft mcp` the same way:
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "erdraft": { "command": "npx", "args": ["-y", "erdraft", "mcp"] }
41
+ }
42
+ }
43
+ ```
44
+
45
+ Keep it in the project (`npm install --save-dev erdraft`, or the project's package manager's), so `npx erdraft` runs the version installed rather than the latest; `npx erdraft --help` says every command.
46
+
47
+ ## Staying up to date
48
+
49
+ `npx erdraft` runs the version the project installed: `npx erdraft --version` says which. Update it as any other dependency (`npm install --save-dev erdraft@latest`), or run the latest without installing it (`npx erdraft@latest studio`).
50
+
51
+ ## Ask it
52
+
53
+ Ask in your own words, *propose two alternatives for tagging projects*, from either side:
54
+
55
+ - **The window's assistant**: its sparkles, at the top bar's end (AI Assistant under the pointer), open a panel at the right. What the window shows (the draft open, else the table in focus) goes with the words, and the conversation goes on from message to message. It runs your own Claude Code headless (`claude -p`), with Erdraft's MCP as its only tools and none of Claude Code's own: it reads and proposes, never edits a file nor runs a command.
56
+ - **The bell**, at the rail's foot over the theme, allows the browser's notifications (the browser asks once): a draft the agent proposes while the window is not in front is told in a notification, its id and title, and the window opens on it when it is clicked. The bell turns them off and on after, kept; refused, it says where the browser's settings allow them again.
57
+ - **Your agent's session**: with the MCP in the project's `.mcp.json`, a Claude Code session started after it was added offers its tools once approved.
58
+
59
+ ## Share it
60
+
61
+ **Share**, at the top bar's end before the assistant, gives a link to the window over the internet, for someone on another machine to follow the schema and the drafts as they change: **Create a link**, then **Copy link**, and **Stop sharing** when done (the window's process ending stops it too). The link opens on what the window shows as it is copied (the draft open, the table in focus, the relationships drawn: the address's query). Whoever opens the link only reads: the window says *View only*, with no Share, no assistant, no Adopt or Set aside, no draft's bin, no database's menu (its name alone), no Connect an agent (the MCP runs on this machine alone) and no Read the schema again, and what it is shown through refuses whatever would change something.
62
+
63
+ The link goes through a tunnel provider, ngrok for now (`tunnel` in `.erdraft.json`), through its SDK in Erdraft's process, with no ngrok program to install: add the SDK to the project (`npm install --save-dev @ngrok/ngrok`, an optional peer of Erdraft's). The first time, the panel asks for an ngrok account's authtoken (a free account's, from [its dashboard](https://dashboard.ngrok.com/get-started/your-authtoken), *Get one*), and keeps it in the system's keychain once a link has opened with it (macOS's Keychain, Windows' Credential Manager, Linux's Secret Service; never in a file), so it is not asked again; *Forget it* takes it out. An authtoken ngrok refuses is said and not kept; where the keychain cannot keep one, it is kept until Erdraft stops. `NGROK_AUTHTOKEN`, when set, goes before the one kept, and the panel says so. On ngrok's free plan, whoever opens the link is shown ngrok's warning page once before the window.
64
+
65
+ ## Commands
66
+
67
+ ```text
68
+ erdraft: a project's data model, drafted with an agent while a person watches
69
+
70
+ Usage: erdraft <command> [options] [--database <id>]
71
+ erdraft --version
72
+
73
+ init [<schema>] [--name <name>] [--force]
74
+ Set the project up: .erdraft.json (its Drizzle schema found, or the one given), the MCP
75
+ in .mcp.json, the skill for its agents
76
+ studio Open the window (the diagram, live)
77
+ mcp Serve the MCP on stdin and stdout, for an agent
78
+ read Read the schema again from the code
79
+ tables [--draft <id>] List the tables
80
+ table <name> [--draft <id>] Describe a table
81
+ drafts List the drafts
82
+ draft <id> Show a draft and what it changes
83
+ propose <file|-> --title <title> [--summary <text>] [--group <name>]
84
+ Propose a draft from a JSON array of operations
85
+ revise <id> [<file|->] [--replace] [--title <title>] [--summary <text>] [--group <name>]
86
+ Add operations to a draft (or put them in its place)
87
+ show [<table>] [--related] [--draft <id> | --schema]
88
+ Show it in the window: a table in focus, only it and the tables it joins, a draft
89
+ wait <id> [--seconds <n>] Wait until the person adopts a draft or sets it aside (30 s at most when not said)
90
+ adopt <id> Adopt a draft (the others of its group are set aside)
91
+ discard <id> Set a draft aside
92
+ delete <id> Delete a draft for good
93
+ databases List the project's databases, * the one worked on
94
+ install-skill [--force] Write Erdraft's skill into the project (.claude/skills/erdraft), for its agents
95
+ use <id> Work on another database (the window switches too)
96
+
97
+ A command works on the database chosen last (use), or on the one --database names. The nearest .erdraft.json says
98
+ where the databases' schemas and their drafts are.
99
+ ```
100
+
101
+ ## Configuration
102
+
103
+ `.erdraft.json`, found from the folder a command runs in upwards, says what Erdraft reads and where it keeps its drafts:
104
+
105
+ | Key | Example | What it is |
106
+ | --- | --- | --- |
107
+ | `name` | `Acme` | The project's name, its folder in the store; none given, the config's folder's. Two projects of one name share their drafts |
108
+ | `schema` | `src/db/schema.ts` | The project's one database: the Drizzle module that exports every table and enum (drizzle-kit's `schema`) |
109
+ | `databases` | | Or its databases, each `{ "name", "schema" }`: the window's menu switches between them (Add a database… says how), the CLI with `use <id>` or `--database <id>`; each has its own drafts in `databases/<id>/`, and the workspace keeps which it works on |
110
+ | `store` | `~/.erdraft/acme` | The project's folder: in `databases/<id>/`, each database's drafts (`drafts.json`), cards moved (`layouts.json`), last draft's number (`sequence.json`) and the app's fields the inspector traced (`fields.json`), shared by the CLI, the MCP, the window and every workspace of the project on this machine, outside the repository; each workspace's own schema read (`base.json`) and view asked (`view.json`) in `workspaces/<id>/`, its branch's never another's. `~` is the home folder; none given, `~/.erdraft/<name>` |
111
+ | `port` | `9369` | The window's port, on 127.0.0.1: none given, 9369, unassigned by IANA and outside every system's ephemeral range (decisions/0006) |
112
+ | `claude` | `claude` | The Claude Code the assistant runs |
113
+ | `model` | `sonnet` | The assistant's model, an alias or a model's full name |
114
+ | `tunnel` | `ngrok` | The provider the window is shared through (Share it); none given, ngrok, the one there is so far |
115
+
116
+ ## Where a field keeps its value
117
+
118
+ The **inspector** says, over the project's own app in development, where a field keeps what is typed in it: a table's column, a key of the browser's storage, or nowhere (kept on the page only, gone with a reload). The app is given two pieces, both in development alone: a field is traced by its value as it is saved, not by reading the code, which loses it through spreads and renames and says nothing of a value kept in the browser or nowhere.
119
+
120
+ - **The logger**, on its server: `inspectLogger(url)` (`erdraft/inspect/logger`) as its Drizzle client's `logger`, which sends each row a write changes (`update`, `insert`) to the window's server, kept there in memory for 15 minutes, never in the store nor off the machine. Nothing it does fails a query: a statement it cannot read, a value JSON has no type for (a BigInt is sent by its digits) and a window not open are let go.
121
+ - **The inspector**, on its pages: `<Inspector url={url} />` (`erdraft/inspect`), drawn over every page. It watches each field a person changes (a box, a list, a group of radios) and each key the page writes in its storage, from the page's mount.
122
+
123
+ Change a field and save it as anyone would, then **⌥⇧D** (Alt+Shift+D) and click the field: its last value is looked for in the rows written since it changed (trimmed, in other capitals, its digits alone, inside a JSON document) and in the keys the page wrote, the column first. Found in a column, the window focuses its table and lights the column (`?table=projects&column=postal_code`), and the assistant takes it as "this field". A field not changed is looked for since the page opened; one changed and found nowhere is said kept nowhere. Each field traced is kept in the store (`fields.json`, by page and label), shown under the pointer on the next visit and listed to an agent by `list_fields`. Esc stops it.
124
+
125
+ In a Next.js app, both in development alone, the window's address in one variable (`NEXT_PUBLIC_ERDRAFT_URL=http://localhost:9369`):
126
+
127
+ ```ts
128
+ // The server's Drizzle client
129
+ import { drizzle } from 'drizzle-orm/node-postgres'
130
+ import inspectLogger from 'erdraft/inspect/logger'
131
+
132
+ const erdraft = process.env.NODE_ENV === 'development' ? process.env.NEXT_PUBLIC_ERDRAFT_URL : undefined
133
+
134
+ export const db = drizzle(process.env.DATABASE_URL, {
135
+ logger: erdraft ? inspectLogger(erdraft) : undefined,
136
+ })
137
+ ```
138
+
139
+ ```tsx
140
+ // Drawn once, in the root layout
141
+ 'use client'
142
+
143
+ import dynamic from 'next/dynamic'
144
+
145
+ const Inspector = dynamic(() => import('erdraft/inspect'), { ssr: false })
146
+
147
+ export default function ErdraftInspector() {
148
+ const url = process.env.NODE_ENV === 'development' ? process.env.NEXT_PUBLIC_ERDRAFT_URL : undefined
149
+
150
+ return url ? <Inspector url={url} /> : null
151
+ }
152
+ ```
153
+
154
+ A field is known by its label (its `aria-label`, the text it is labelled by, its `<label>`), so two fields of one page with one label are one; a switch or a box ticked is not traced, its value too short to be found by, nor a password, never sent to the window.
155
+
156
+ ## How it works
157
+
158
+ - **The base** is the schema as the code has it, read again from the top bar's arrows after the page's name (Read the schema again, `POST /api/read`, every window and the shared link drawing it at once, no page loaded again; a module that cannot be read said beside them), never as the module is saved (a file saved half written would break the read): the Drizzle module read by tsx in a process of its own each time (a table changed since is read as it is now; what the module prints never reaches the MCP), by the paths of the `tsconfig.json` nearest it, an ES module's or a CommonJS project's alike, its tables (one in a schema of its own, `pgSchema('auth')`, named `auth.users`), columns, keys, indexes (a unique constraint of several columns among them), checks and enums written to `base.json` (`read`, `read_schema`, and each time the window opens). A module with no Postgres table is refused, a MySQL or SQLite one among them.
159
+ - **A draft** is a list of operations on the base (`add_table`, `drop_table`, `rename_table`, `add_column`, `drop_column`, `alter_column`, `add_foreign_key`, `drop_foreign_key`, `add_index`, `drop_index`, `add_check`, `drop_check`, `add_enum`, `set_enum_values`, `drop_enum`), applied in order. One that cannot be applied is refused, naming its step; a base read again keeps every draft on it, and a draft that no longer applies says why in the window (one set aside says nothing of it), unless the base holds what it comes to (adopted, then written in the code: the draft laid over the code's schema, each table and enum it touches the same there as in the code, its columns and checks by name, its foreign keys by what they join, its indexes by their columns, an enum's values; what its own steps added and took away again gone from both, what the code holds more kept; `src/core/written-in.ts`), which the window, `show_draft` and `erdraft draft` say instead, the diagram the schema as the code has it with what the draft did marked on it, as a draft still proposed is marked (the tables and columns it added, those it changed and named again, the keys, indexes and checks it added; `src/core/mark-written.ts`).
160
+ - **The store** is changed by one process at a time, every other waiting its turn (a `.lock` file in it): the CLI, the MCP and the window read a file, change it and write it whole, so an agent proposing three alternatives at once keeps all three, each its own number.
161
+ - **The window's server** listens on 127.0.0.1 and answers its own page alone: a request to its API from another site's page, or to another host than this machine (a name turned to 127.0.0.1), is refused, so a page the person has open can neither read the schema nor adopt a draft nor start the assistant. The inspector's routes answer any page of this machine, the project's app.
162
+ - **Sharing** (`POST /api/share`, the credential typed in its body; `DELETE` to stop; `DELETE /api/share/credential` to forget the one kept; the link in the state, and where the credential comes from, never the credential): a second server on a port the system gives, on 127.0.0.1 (`src/share/serve-shared.ts`), is what the tunnel reaches. The link's token (24 random bytes, `?share=`) is kept in an HttpOnly cookie and taken out of the address, and every request without it refused; with it, GET and HEAD alone are passed on to the window's own server as a page of this machine asks them (of `localhost`, with no `Origin` and no cookie), of the API only `/api/state` and `/api/events`. A provider is a `TunnelProvider` (`src/share/tunnel-provider.ts`: its name, the credential it opens with, its name, where one is got and its environment variable, and `open(port, credential)` its address and `close`), named in `src/share/tunnels.ts`, its credential kept by the keychain under `erdraft` and the provider's id (`src/share/keychain.ts`, `@napi-rs/keyring`, loaded when a credential is first asked for); it must carry server-sent events, or the shared window never changes (decisions/0008). Run from Erdraft's source, the window is Vite's, and the link reaches what Vite serves.
163
+ - **The diff** sets the draft beside the base, a table or column renamed matched to its old name, a foreign key matched by what it joins.
164
+ - **The window** (Vite, React, React Flow) keeps what it shows in its address (`?draft=d3&table=users&relations=users`), so a reload or a link opens it again (a draft's card closed from its X, the schema shown again where the view stood, never framed whole, the table in focus and the relationships drawn kept), and Back and Forward go from screen to screen (another draft or other relationships a step; the focus alone none). It fetches the state and hears the store's folder change through server-sent events: a draft proposed by the agent is shown within a second, and selected. Its tables keep the base's places, a table a draft adds at the nearest free place beside a table it joins; the base laid out as the person arranges it from the controls (Arrange tables, kept in the browser for every view): top to bottom (dagre, the default), left to right (dagre's ranks across), or square (columns as many as make the diagram as wide as it is high, each card under the shortest, in the order the ranks read; `positionsOf`, `lib/graph.ts`), every card going where the arrangement lays it as a step undone like a drag, the cards moved by hand in the view shown too. The diagram opens already framed (on the table in focus, else on what a draft changes, else whole), never zoomed to as it appears; panned away, the controls' target (Center the tables in focus, beside the whole diagram's) brings those tables back to the middle of the view, and the draft card's own, at its bottom left (Center its tables under the pointer), the tables the draft changes (adopted and written in the code, or no longer applying, the tables its operations touch that the code has, `src/core/touched-tables.ts`), the table in focus let go. The lines run under the cards, each in a tone of its own (five hues, its hue halfway to its foreground so each reads on the canvas light or dark), those meeting at a card each another, so a person presenting the diagram can say which they mean (a line a draft adds or takes away in the colour of what it does), each a lane apart from the others sharing a card (`TURN` and `LANE`, `lib/graph.ts`), never one over another, and running across in the gap between its two cards where they stand now, never behind one (`LaneEdge`, `lib/route.ts`); a line clicked lights the two tables it joins and the columns it joins, frames them and fades the rest, let go with a click on the pane or a table. A card the person drags stays where they leave it, kept in the store's `layouts.json` (shared by every workspace) by what the window showed (the schema, a draft, a table's relationships in either), for every window and the next visit; Reset layout, in the controls, sets it back to the diagram's own. Each drag (several cards dragged together one) is undone and done again from the controls' arrows or with ⌘Z and ⇧⌘Z (Ctrl+Z, Ctrl+Y), never while writing in a field.
165
+ - **The assistant** (`POST /api/assistant`) runs `claude -p` from the project's folder, the person's words on its stdin (never among its arguments), with `--strict-mcp-config` (Erdraft's MCP alone), `--tools ""` and `--allowedTools mcp__erdraft`, its stream (`--output-format stream-json --include-partial-messages`) read into the window's events: words as they are written, each tool as a step, the session to `--resume`. Stopping it or closing the panel stops the agent.
166
+ - **The window follows the agent**: asked to see a table, its relationships or a draft, the agent calls `show_in_window` (the CLI, `show`), which writes the workspace's `view.json` in the store; the window, hearing the folder change, opens the draft, focuses the table and, for its relationships, draws only it and the tables it joins by a foreign key either way, laid out on their own, under the card the way back (Show all); the person asks for a draft's tables' from its card, beside Center (the tables joined by commas in the address, `?relations=wiki_pages,wiki_page_stars`), and for a table's own from its card's header (Show its relationships beside its count, Show all tables there while they are drawn), and goes along a foreign key from its column's link, an arrow under the pointer (Go to its table): the table it points to focused, the column it points to lit, every table drawn again when it is out of the relationships drawn. An ask made before the window opened is not applied.
167
+ - **Deleting** is the person's too: a draft's bin in the column, said again in a dialog, or `erdraft delete <id>`; the draft goes for good with its layouts, and its id is never given again (`sequence.json`). The agent only sets a draft aside.
168
+ - **Adopting** is the person's, in the window or the CLI (`adopt`), never an MCP tool's; adopting a draft sets the others of its group still proposed aside. The agent waits for it (`wait_for_decision`, the CLI's `wait`): the store read every second until the draft is adopted or set aside, the time is up (30 s unless asked, 120 at most through the MCP) or the person stops the agent, and told what to do next, so the person never has to say they adopted. The wait is short and called again, as the agent's turn is held while it waits: what the person writes to the agent meanwhile (a column to change, a question) is read between two waits, answered, and the draft revised.
169
+ - **A table's rules** are drawn under its columns, past a rule, a row each: a unique index (its fingerprint) or an index (its sorted list) by its columns, a check (its shield) by its SQL, its name under the pointer; a draft marks each added or taken away, an index matched by its columns and whether it is unique, a check by its name and its SQL.
170
+ - **A json column's shape** is a draft's while it is proposed: the column's TypeScript type, as the code's `$type<...>()` writes it, given by the agent with the column (`shape`, taken off by `alter_column` with null). Once written it is read from the code: the module exports the column's zod 4 schema by its table's and its column's words in camelCase, then `Shape` (`websites.theme` is `websitesThemeShape`), turned into JSON Schema by zod's `toJSONSchema` and said as the TypeScript type it describes; a `$type` alone is gone when the module runs, and draws nothing. Either way its row shows braces before its type, which open its fields a line each in a panel beside the card (decisions/0005).
171
+
172
+ ## The skill
173
+
174
+ `erdraft install-skill` writes Erdraft's skill into the project, at `.claude/skills/erdraft/SKILL.md` beside its `.erdraft.json`, so an agent there that has never heard of Erdraft knows how to work with it when asked to design or change the schema: read the project's own conventions and the tables touched, propose the whole design in one draft (its indexes and checks as operations), alternatives in a group, wait for the person's choice, write it by the project's conventions, and check the window says it is written. It names no project: what is a project's (its naming, its migrations, its tests) the skill sends the agent to read there. Written once; run again it leaves a skill that is the same, and refuses one that differs (changed by hand, or of another version) unless `--force` writes it over. The skill's source is the package's `skill/SKILL.md`.
175
+
176
+ ## The MCP's tools
177
+
178
+ | Tool | Does |
179
+ | --- | --- |
180
+ | `read_schema` | Reads the schema again from the code |
181
+ | `list_tables`, `describe_table` | The tables, a table's columns and keys and the keys that point to it (after ←); with a draft, as the draft has them |
182
+ | `list_databases` | The project's databases, * the one the person works on, which every other tool works on |
183
+ | `list_drafts`, `show_draft` | The drafts and where they stand; one in full |
184
+ | `propose_draft` | A new draft, drawn at once in the window |
185
+ | `revise_draft` | Operations added to a draft (or put in their place), drawn as they come |
186
+ | `show_in_window` | What the window shows: a table in focus, only it and the tables it joins, a draft, the whole diagram |
187
+ | `wait_for_decision` | Waits until the person adopts a draft or sets it aside, or the time is up, and says what to do next |
188
+ | `list_fields` | The app's fields the inspector traced, page by page, each with where it keeps its value |
189
+ | `discard_draft` | A draft set aside |
190
+
191
+ ## Developing
192
+
193
+ ```text
194
+ bin/erdraft.ts The CLI's entry: the MCP on stdio, the window built (dist/window) or, from the source, by Vite
195
+ skill/SKILL.md The skill install-skill writes into a project (src/core/install-skill.ts)
196
+ src/core/ The schema read from Drizzle (in a process of its own, read-schema-child.ts), the operations, the diff, the store and its lock, the config, the text the CLI and the MCP answer with
197
+ src/cli/run.ts The CLI's commands
198
+ src/mcp/create-server.ts The MCP server and its tools
199
+ src/server/ The window's API (state, events, adopt, discard, the schema read again, the share, the assistant, the inspector's) and what it refuses (refusal-of.ts), Claude Code's stream read, the built window served (serve-window.ts), its Vite config
200
+ src/share/ The window shared over the internet (decisions/0008): the providers (tunnel-provider.ts, tunnels.ts, providers/ngrok.ts), their credentials in the system's keychain (keychain.ts), the read-only server the tunnel reaches (serve-shared.ts), one link at a time (create-share.ts)
201
+ src/inspect/ The inspector: the Drizzle logger and the rows it reads from a statement, the value matched, and the overlay a project's app draws (its fields, their changes, the browser's storage)
202
+ src/ui/ The window: app.tsx, its hooks, its components (the assistant's panel and composer among them), its contexts (contexts/relations.ts, what a table's card reaches of the diagram), the diagram's layout (lib/graph.ts), the conversation's messages (lib/assistant-message.ts)
203
+ src/test-utils/ A small Drizzle schema and the fakes the tests use
204
+ ```
205
+
206
+ ```sh
207
+ pnpm dev # the window from the source, by Vite, the page changing as it is written
208
+ pnpm erdraft <command> # the CLI from the source, through tsx
209
+ pnpm test:coverage # every file at 100%, the window's React layer in happy-dom
210
+ pnpm bundle # dist/: the CLI and the inspector (tsup), the window (Vite), the inspector's types (tsc)
211
+ ```
212
+
213
+ - **A window open keeps the core it started with.** Vite brings a change to `src/ui/` into it at once, but its server applies the drafts with the `src/core/` it loaded: a change to the core (a column's fields, the diff, how a draft is read) reaches it only once `studio` is started again, and an agent's MCP only once it is connected again. The CLI runs the source afresh each time.
214
+ - **A change is looked at in a window of its own,** never by stopping the one a person is using: `studio` started from a folder whose `.erdraft.json` gives another `port` and the same `schema` and `store` (absolute paths), so it shows the same drafts; photographed in a headless browser, then stopped (2026-11-03, a json column's shape on port 4101).
215
+ - **A control added to the window says what a shared window does with it.** Whoever opens a shared link only reads: nothing that changes something (adopt, set aside, delete, the database switched or added, the assistant, Share, Read the schema again) nor that belongs to the developer's machine (Connect an agent, its MCP on stdio) is drawn there, `viewOnly()` (`src/ui/lib/view-only.ts`) deciding, and the share's server refusing it whatever the page shows (decisions/0008; 2026-11-04, the owner's correction: Add a database in a shared window).
216
+ - **A shared window is looked at without a tunnel:** `serveShared(<the test window's port>, <a token>)` (`src/share/serve-shared.ts`) in a script, and Chromium launched with `--host-resolver-rules=MAP shared.test 127.0.0.1`, the share's cookie (`erdraft_share`, the token) added to its context before the page is opened, as a `Secure` cookie is not kept over http; a host that is not this machine's is what turns the window to View only (2026-11-04, port 9398).
217
+
218
+ The source runs as TypeScript through tsx; Node does not strip the types of a file inside `node_modules`, so the package ships it built (`prepack`): `dist/cli.js`, the inspector's two entries with their types, and the window Vite built, which the CLI serves with its API beside it, where from the source Vite serves it. It is published with pnpm (`pnpm publish`), which writes `publishConfig`'s `bin` and `exports`, the built files', over the source's; `npm publish` is refused (`prepublishOnly`), as it would publish a package whose command is not in it.
219
+
220
+ ## License
221
+
222
+ MIT