@parity-app/cli 0.1.0 → 0.2.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parity-app/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Drive the Parity app from a terminal or an agent: read state, run edits, take screenshots, serve MCP.",
@@ -34,6 +34,11 @@
34
34
  "dist",
35
35
  "skills"
36
36
  ],
37
+ "scripts": {
38
+ "build": "vp pack",
39
+ "prepack": "vp pack",
40
+ "try-packed": "node scripts/try-packed.ts"
41
+ },
37
42
  "publishConfig": {
38
43
  "access": "public"
39
44
  },
@@ -42,12 +47,9 @@
42
47
  "typescript": "6.0.3"
43
48
  },
44
49
  "devDependencies": {
50
+ "@parity-app/agent": "0.0.0",
45
51
  "@parity-app/core": "0.0.0",
46
52
  "@parity-app/libgen": "0.0.0",
47
53
  "@parity-app/registry": "0.0.0"
48
- },
49
- "scripts": {
50
- "build": "vp pack",
51
- "try-packed": "node scripts/try-packed.ts"
52
54
  }
53
- }
55
+ }
package/skills/SKILL.md CHANGED
@@ -22,25 +22,62 @@ it.
22
22
 
23
23
  ## Tools
24
24
 
25
- The same tools are MCP tools and `parity` commands in a terminal. They act on a tab, which is one open project; the
26
- active tab is the default.
25
+ The same tools are MCP tools and `parity` commands in a terminal. They act on a tab, which is one open project. Without
26
+ `tab`, they act on the open project your working folder is in, and otherwise on `active`: what the person is looking at,
27
+ the active tab of the Parity window they used last. When the person says "this" or "what I've selected" without naming a
28
+ project, pass `tab: "active"` (`--tab active`) and read `get_app_state` for the selection.
27
29
 
28
30
  | MCP | CLI | What it does |
29
31
  | --------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
32
  | `get_app_state` | `parity state` | Designs, the selection, modes, components and token names, and what Parity detected (builder and config, package manager, installed versions). Start here. |
31
- | `execute` | `parity exec` | Runs a JavaScript snippet against the designs. All changes land as one undo step. |
33
+ | `get_status` | `parity status` | The revision, whether anything is unsaved and which files and tokens, the last save, and undo and redo; `summary` names them. |
34
+ | `get_modes` | `parity modes [design]` | Viewports (the ids `Responsive` takes), modes (the ids renders take) and what each design shows; a design adds its frames. |
35
+ | `execute` | `parity exec` | Runs a JavaScript snippet against the designs. Its changes land as one undo step, merged per design with edits made meanwhile. |
32
36
  | `screenshot` | `parity screenshot <design>` | Renders a design in its states and modes, offscreen, and returns the images. |
33
37
  | `find_problems` | `parity problems <design>` | Render errors, overflow, clipping and unknown components, per frame. |
34
38
  | `check_accessibility` | `parity a11y <design>` | axe-core against WCAG 2.2 AA, per frame, on the layers that break each rule. |
35
39
  | `get_console` | `parity console` | What the project's own code wrote to the console, on the canvas and offscreen. |
40
+ | `get_tokens` | `parity tokens` | Tokens filtered by category, source or name, with values in every theme. |
41
+ | `get_components` | `parity components` | Components, libraries, icon sets and exports left out or failing to render. |
42
+ | `get_props` | `parity props <Component>` | Props from source; `all` includes passed-through attributes. |
36
43
  | `get_tabs` | `parity tabs` | The open projects. |
37
44
  | `open_project` | `parity open <folder>` | Opens a project in a new tab. |
38
45
  | `read_skill` | `parity skill [file]` | This skill. |
39
46
 
40
- In a terminal, `parity tokens`, `parity components` and `parity props <Component>` read what designs can use without
41
- writing a snippet. `parity components` also says which of the app's exports aren't components and why, which components
42
- fail to draw on the canvas and why (often a missing provider, which a decorator adds), and what the person listed or
43
- took off the list by hand.
47
+ The token, component and prop tools read what designs can use without writing a snippet in either transport.
48
+ `parity components` also says which of the app's exports aren't components and why, which components fail to draw on the
49
+ canvas and why (often a missing provider, which a decorator adds), and what the person listed or took off the list by
50
+ hand.
51
+
52
+ CLI commands and MCP tools share validation and defaults. Screenshot scale defaults to 1 and accepts 0.25 to 3. Audits
53
+ check every state and shown mode unless narrowed; a single-element selector list is equivalent to a string. Screenshot
54
+ failures and audit findings exit with 1 in the CLI and set MCP `isError`, with details retained. `get_console` defaults
55
+ `clear` to false; `get_props` defaults `all` to false. MCP project paths must be absolute; the CLI resolves relative
56
+ folders from its working directory. Skill reading is local and needs no running app.
57
+
58
+ Several copies of Parity can run at once, each with its own projects open. Each call goes to the copy that has its
59
+ project open, so naming the project is enough; `get_tabs` lists every copy's projects. A project open in two copies goes
60
+ to the one that has been running longest. A copy kept apart with `PARITY_AGENT_FILE` is reached only through that file:
61
+
62
+ ```sh
63
+ parity tabs # every project open in any copy
64
+ parity state --tab active # what the person is looking at, and their selection
65
+ PARITY_AGENT_FILE=copy/agent.json pnpm dev & # a copy of Parity kept apart
66
+ PARITY_AGENT_FILE=copy/agent.json parity tabs # just that copy
67
+ ```
68
+
69
+ Every tool returns its result as data too: MCP `structuredContent`, or `--json` in the CLI. Read that rather than the
70
+ text. A failure has `error.code` to branch on, `retryable` (send it again unchanged), `path` (the input at fault) and
71
+ `alternatives` (valid ids for one it didn't know), so a mistyped id is fixed in one retry:
72
+
73
+ ```sh
74
+ parity problems button --state loading --json
75
+ # {"error": {"code": "unknown_state", "path": "/state", "alternatives": ["default", "outline", "destructive", "disabled"], ...}}
76
+ parity problems button --state disabled --json
77
+ ```
78
+
79
+ `get_tokens` returns the first 100 tokens and `get_console` the newest 50 messages, with `total` and `truncated`; narrow
80
+ the read or raise `limit` when `truncated` is true.
44
81
 
45
82
  When a library's components, tokens or icons don't show, `parity lib <package> --root <project>` describes the package
46
83
  in a `.lib.par`: its components, tokens and the providers its frames need. It writes `./<id>.lib.par` in the current
@@ -54,7 +91,8 @@ keeping the designs there, so ask before running that in someone's project.
54
91
  2. **Look before you change.** Read the design with `Get("design-id", { depth: 3 })` in `execute`, and take a screenshot
55
92
  of what's there. Find the components you need with `Components()` and their props with `Props()`.
56
93
  3. **Change it in one snippet.** Build or edit the design with `Insert`, `Update`, `Replace`, `Move` and the rest. A
57
- snippet either applies whole, as one step the person can undo, or changes nothing. Name the layouts you make.
94
+ snippet either applies whole, as one step the person can undo, or changes nothing. Name the layouts you make. If it
95
+ fails because the person changed a design it changes, run it again by its `editId`.
58
96
  4. **Cover every state.** A screen that loads has a loading state, a list has an empty one, a form has an error one. Add
59
97
  them with `AddState`, as changes to the base design, not as copies.
60
98
  5. **Check every frame.** Screenshot the design in all its states and the modes it shows (`state: "all"`,
@@ -65,7 +103,15 @@ keeping the designs there, so ask before running that in someone's project.
65
103
 
66
104
  Your changes to designs are on the canvas but not saved: the person saves (Ctrl+S) or undoes them, as with their own
67
105
  edits. Don't say they're saved. Token values from `SetTokens` are the same: they show at once, and go into the project's
68
- stylesheet when the person saves.
106
+ stylesheet when the person saves. `get_status` says what's saved and what isn't, and its `revision` (also in
107
+ `get_app_state`) stays the same until something changes, so compare it before reading the board again:
108
+
109
+ ```sh
110
+ parity status --summary
111
+ # shadcn-vite is at revision 7c612c04:1.
112
+ # 1 file isn't saved. They wait for the person to save; agents can't.
113
+ # shadcn-vite.par: changed button
114
+ ```
69
115
 
70
116
  ## Rules
71
117
 
@@ -86,11 +132,12 @@ stylesheet when the person saves.
86
132
  - **Reuse what repeats.** When a row, card or list item appears more than twice, make it a component design with
87
133
  `ExtractComponent`, so each copy is an instance with its own content.
88
134
  - **Leave the canvas tidy.** New designs go into empty space (`FindEmptySpace`); `Overlaps()` tells you if boards
89
- collide.
135
+ collide, sizing boards the canvas hasn't drawn yet from their frames (`estimated: true`).
90
136
 
91
137
  ## When something fails
92
138
 
93
- `execute` returns an `editId` when a snippet throws, and nothing changes. Fix it with `editId` and `edits` (exact find
94
- and replace on the snippet) instead of sending it all again. The error names the line. Common causes: a node path that
95
- matches two designs (write it as `design/node`), a node property that doesn't exist, and a state change to something
96
- states can't change (only `hidden`, `text`, `props` and `style`).
139
+ `execute` returns an `editId` when a snippet throws, and nothing changes; `error.code` says why, and
140
+ `error.alternatives` names the valid ids when a design, state, mode or component wasn't found. Fix it with `editId` and
141
+ `edits` (exact find and replace on the snippet) instead of sending it all again. The error names the line. Common
142
+ causes: a node path that matches two designs (write it as `design/node`), a node property that doesn't exist, and a
143
+ state change to something states can't change (only `hidden`, `text`, `props` and `style`).
@@ -154,7 +154,8 @@ or hides the node. `ExtractComponent` sets all of this up from the occurrences i
154
154
  A **variant** is a param whose `options` each patch the component design, the way a state does: a size or a tone the
155
155
  instance chooses. Its `default` is an option's id, else the first option. An instance picks one in its `args`, and
156
156
  `view.variants` picks which one the component's own board shows. Patches apply in this order: the viewport's, the
157
- variant options', the state's, then the instance's overrides.
157
+ variant options', the state's, then the instance's overrides. `Get` returns the stored node, with the text it was given;
158
+ `Get(path, { state })` returns it as the board shows it, the param's value filled in and `bind` kept (see execute.md).
158
159
 
159
160
  ```json
160
161
  "params": {
package/skills/execute.md CHANGED
@@ -4,12 +4,30 @@
4
4
  `await` works at the top level. When the snippet finishes, every change lands on the canvas as one step the person can
5
5
  undo; when it throws, nothing changes and you get an `editId` to fix it with.
6
6
 
7
- - Pass `description`: 3 to 8 words the person sees while it runs, such as "Add a pricing section".
7
+ The person, and other agents, can change the tab while your snippet runs. Only the designs your snippet changed, created
8
+ or deleted are applied, over what's there by then, so their edits to other designs, even in the same file, stay. If a
9
+ design you changed was changed or deleted in the meantime, or the modes were and you set them, nothing is applied and
10
+ the call fails naming it:
11
+
12
+ ```text
13
+ Failed, nothing changed: Not applied: since this call read the tab, "Pricing" (app.par) was changed in the editor, and
14
+ this call changes it too. Run the call again to work from what's there now.
15
+ ```
16
+
17
+ Run it again with its `editId` and no edits (`parity exec --edit-id <id>`, or `execute` with just `editId`): the snippet
18
+ reads the tab afresh, so it works from their change. Read the design again first if your snippet depends on what it
19
+ held.
20
+
21
+ - Prefer a `description` of 3 to 8 words, such as "Add a pricing section". CLI `exec` calls it `--label`. Both default
22
+ to "Edit designs" if omitted.
8
23
  - `Print(...)` (or `console.log`) is how you see values. Objects print as JSON.
9
24
  - Values assigned without `const` or `let` carry over to your next snippet in the same tab.
10
25
  - A snippet has 5 seconds of computing time. Rendering (screenshots and checks) doesn't count against it.
11
26
  - To retry a failed snippet, send `editId` with `edits`, each an exact `{ find, replace }` on the snippet text (`all` to
12
- replace every match).
27
+ replace every match). Omitted edits default to an empty list. Do not send a new snippet with `editId`.
28
+ - Both CLI and MCP validate the same request: a nonblank snippet or `editId`, nonblank label and tab when supplied, and
29
+ patches with a nonempty `find`, string `replace` (which can be empty), and optional boolean `all`. Invalid requests
30
+ are rejected before reaching the app.
13
31
 
14
32
  ## API
15
33
 
@@ -17,14 +35,15 @@ undo; when it throws, nothing changes and you get an `editId` to fix it with.
17
35
  // Reading
18
36
  Designs(): DesignSummary[] // every design: id, name, kind, states, what it shows, size
19
37
  GetDesign(design): Design // a whole design, as in document.md
20
- Get(path, { depth?, state? }?): Node // a design's root or a node; depth cuts children off with "…"
38
+ Get(path, { depth?, state?, mode? }?): Node // a design's root or a node; depth cuts children off with "…"
21
39
  Get(path?, visit): any[] // walks nodes, collecting what visit returns
40
+ Get(path, visit, { state?, mode? }): any[] // walks nodes as a frame draws them
22
41
  Components(): string[] // the components designs can use
23
42
  Props(component, { all? }?): PropsInfo // a component's props, from its source
24
43
  Icons(): IconSet[] // the project's icon sets, with their styles
25
44
  Icons(set, { search?, style?, limit? }): Icon[] // async: a set's icons, as component nodes name them
26
45
  GetTokens({ category?, source?, search? }?): Token[]
27
- GetModes(): { settings, modes } // the project's modes, and each resolved
46
+ GetModes(design?): { settings, variables, viewports, modes, designs } // modes resolved; what each design shows
28
47
  Selection(): { design?, node?, mode? } // what the person has selected
29
48
 
30
49
  // Designs
@@ -82,11 +101,27 @@ canvas, the main mode and the modes beside it. Start here to find ids.
82
101
  ### Get
83
102
 
84
103
  `Get("dashboard", { depth: 2 })` returns the design's root with two levels of children; deeper ones show as `"…"`. Read
85
- a branch with a node path, as in `Get("dashboard/Stats", { depth: 2 })`. With `state`, the result shows the nodes as
86
- that state changes them.
104
+ a branch with a node path, as in `Get("dashboard/Stats", { depth: 2 })`. Without `state` or `mode`, Get returns the
105
+ nodes as stored, which is what `Update` and the other edits change.
106
+
107
+ With `state`, `mode` or both, Get returns the nodes as that frame of the board draws them: the design's variant options
108
+ (`view.variants`), its params' defaults filled into bound values, its responsive patches for the mode, then the state's
109
+ patches. `mode` is a mode id from `GetModes()`, as Screenshot takes. A state alone is read in the design's main mode,
110
+ and a mode alone in the state the design shows. A bound value comes back filled in, with its `bind` kept so you can tell
111
+ it comes from a param. An instance comes back as its own node with the args it's drawn with; read what's inside it by
112
+ its path, `design/instance/inner`, which only works with `state` or `mode`.
113
+
114
+ ```js
115
+ // A component whose title is bound to its "title" param: stored, then as the board shows it.
116
+ Print(Get("card/title").text) // "Title"
117
+ Print(Get("card/title", { state: "base" }).text) // "Card title", the param's default
118
+ // A copy of the card on a screen, inside the instance, in a state and on mobile.
119
+ Print(Get("profile/person/title", { state: "selected", mode: "mobile-light" }).text) // "Selected"
120
+ ```
87
121
 
88
122
  `Get(path, visit)` walks the nodes under `path`, or every design without one, and returns what `visit` returns for each.
89
- `visit(node, { design, path, depth, index, parent, skipChildren })`. Return `undefined` to collect nothing.
123
+ `visit(node, { design, path, depth, index, parent, skipChildren })`. Return `undefined` to collect nothing. Given a
124
+ path, `Get(path, visit, { state, mode })` visits the nodes as that frame draws them, as above.
90
125
 
91
126
  ```js
92
127
  // Every Button in the project, with where it is.
@@ -239,6 +274,18 @@ ExtractComponent("team-members", rows.nodes, { name: "Member row", params: { [ro
239
274
  `SetModes(settings)` replaces the project's modes: take `GetModes().settings`, change it, and pass it back. Modes are
240
275
  shared by every design.
241
276
 
277
+ `GetModes()` also gives `viewports`, the ids `Responsive` takes, each with its width and window height; `modes`, the ids
278
+ `Screenshot` and the checks take, each with its `values` (one option per variable), width, window height, classes and
279
+ attributes; and `designs`, each design's `state` (or `states` when it shows every state), `main` mode and `beside`
280
+ modes. `GetModes("dashboard")` gives just that design, with each frame's width, fixed `height` if it has one, and
281
+ `window`.
282
+
283
+ ```js
284
+ const { viewports, designs } = GetModes("dashboard")
285
+ Print(viewports.map((viewport) => viewport.id)) // ["desktop", "tablet", "mobile"]: for Responsive
286
+ Print(designs[0].frames.map((frame) => `${frame.mode} ${frame.window.width}x${frame.window.height}`))
287
+ ```
288
+
242
289
  `SetTokens` changes token values in the project's stylesheets: `SetTokens({ primary: "oklch(0.55 0.2 260)" })` for every
243
290
  theme, or `SetTokens({ primary: { default: "…", dark: "…" } })` per theme. Every frame shows the new values at once, and
244
291
  saving writes them into the project's code, so do it only when the person asks to change the design system, not to style
@@ -250,15 +297,34 @@ one design. They're undone with the call's other changes.
250
297
  result. `state` and `mode` take an id, a list, or `"all"`; `mode` also takes `"shown"`, the modes the design shows. The
251
298
  defaults are the state on the canvas and the main mode.
252
299
 
300
+ Frames draw as the canvas does: instances of a library's component designs (from its `.lib.par`) draw that design, and
301
+ token values not yet saved show, the person's edits and this call's own `SetTokens` alike, so
302
+ `SetTokens({ primary: "red" }); await Screenshot(id)` shows the new value in the same call.
303
+
253
304
  `Measure` lays out one frame and returns its size, whether it overflows sideways, and every node's box (`clipped` when a
254
305
  parent cuts it off). `Problems` checks frames for render errors, overflow, clipping and unknown components.
255
306
  `Accessibility` returns axe-core's findings per node and frame, with the rule, its impact and what's wrong.
256
307
 
308
+ These frames are drawn in the same window as the canvas's: as wide as the frame, and as tall as the design's fixed
309
+ height for the viewport, else the viewport's screen height, else the device nearest its width. So `100vh` and window
310
+ size checks measure the same in `Measure` and `Screenshot` as on the canvas, even when the frame grows past that height.
311
+
257
312
  ## The canvas
258
313
 
259
314
  `FindEmptySpace({ width, height })` finds room for a new board, to the right of the others unless `direction: "below"`,
260
315
  or next to the design named in `near`. `Overlaps()` lists boards that collide, titles included; move one with
261
- `UpdateDesign(design, { x, y })`.
316
+ `UpdateDesign(design, { x, y })`. A board is as big as the canvas last drew it. One it hasn't drawn yet is sized from
317
+ its frames, laid out as the canvas lays them out: each shown mode's frame size (`frames` on the design, else the mode's
318
+ width), companions shrunk to 640 wide, and a frame without a fixed height as tall as its window. Those overlaps say
319
+ `estimated: true`.
320
+
321
+ ```js
322
+ // With the default modes: a 1280 main frame, the dark one beside it at half size and the 390 phone, 2505 wide in all.
323
+ const dashboard = CreateDesign({ name: "Dashboard", x: 0, y: 0 })
324
+ UpdateDesign(dashboard, { showModes: ["desktop-dark", "mobile-light"] })
325
+ CreateDesign({ name: "Settings", x: 2200, y: 0 })
326
+ Print(Overlaps(dashboard)) // Dashboard and Settings, 305 across, estimated: true
327
+ ```
262
328
 
263
329
  ## Examples
264
330
 
@@ -30,9 +30,11 @@ Modes are the project's, shared by every design. Each mode picks one option of e
30
30
  - **Theme:** a class on the frame, such as `dark`, that switches the project's tokens.
31
31
  - **Locale:** `lang` and `dir` attributes, for languages and right-to-left scripts.
32
32
 
33
- `GetModes()` lists them. A design shows its main mode and, beside it, any others you choose with
34
- `UpdateDesign(design, { showModes: ["desktop-dark", "mobile-light"] })`. Show the modes that matter for the design: a
35
- dashboard in desktop and mobile, a component in light and dark.
33
+ `get_modes` (`parity modes`) lists them, and `get_modes` with a design gives its frames: run it before a responsive
34
+ change, since the ids `Responsive` takes are viewports (`mobile`) and the ids screenshots take are modes
35
+ (`mobile-light`). `GetModes()` gives the same in a snippet. A design shows its main mode and, beside it, any others you
36
+ choose with `UpdateDesign(design, { showModes: ["desktop-dark", "mobile-light"] })`. Show the modes that matter for the
37
+ design: a dashboard in desktop and mobile, a component in light and dark.
36
38
 
37
39
  ## Viewports and responsive patches
38
40
 
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Andrew Craswell
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.
@@ -1,286 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { randomUUID } from "node:crypto";
4
- import { homedir } from "node:os";
5
- // src/caller.ts
6
- /** Who is driving Parity, named the way people know it, for the agent's cursor on the canvas. */
7
- /** Known clients, by the name they give MCP's `initialize`, most specific first. */
8
- const clients = [
9
- [/claude[-_ ]?code/i, "Claude Code"],
10
- [/^claude/i, "Claude"],
11
- [/cursor/i, "Cursor"],
12
- [/codex/i, "Codex"],
13
- [/gemini/i, "Gemini CLI"],
14
- [/windsurf|codeium/i, "Windsurf"],
15
- [/copilot|visual studio code|^vscode/i, "Copilot"],
16
- [/zed/i, "Zed"],
17
- [/cline/i, "Cline"],
18
- [/goose/i, "Goose"]
19
- ];
20
- /**
21
- * An MCP client's name for people: a known agent's own name, else the title it gives, else its name with its
22
- * separators made spaces and its words capitalized ("my-agent" becomes "My Agent").
23
- */
24
- function mcpCallerName(clientInfo) {
25
- const name = typeof clientInfo?.name === "string" ? clientInfo.name.trim() : "";
26
- const title = typeof clientInfo?.title === "string" ? clientInfo.title.trim() : "";
27
- for (const [pattern, known] of clients) if (pattern.test(name) || pattern.test(title)) return known;
28
- if (title) return title;
29
- if (!name) return void 0;
30
- return name.replace(/[-_]+(mcp[-_]?)?client$/i, "").split(/[-_\s]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ");
31
- }
32
- /**
33
- * Who runs the CLI, from what agents put in their shell's environment. `PARITY_AGENT_NAME` names it outright; with
34
- * no sign of an agent, it's someone at a terminal.
35
- */
36
- function cliCallerName(env) {
37
- const named = env.PARITY_AGENT_NAME?.trim();
38
- if (named) return named;
39
- if (env.CLAUDECODE === "1") return "Claude Code";
40
- if (env.GEMINI_CLI === "1") return "Gemini CLI";
41
- if (Object.keys(env).some((key) => key.startsWith("CODEX_"))) return "Codex";
42
- return "Terminal";
43
- }
44
- // src/client.ts
45
- /** Where the running app writes its address and token. */
46
- const agentFile = process.env.PARITY_AGENT_FILE ?? join(homedir(), ".parity", "agent.json");
47
- let caller = cliCallerName(process.env);
48
- /** Tells callers with the same name apart. A terminal's calls are separate processes, so they share their name. */
49
- let callerId = process.env.PARITY_AGENT_ID?.trim() || caller;
50
- /**
51
- * Names who's calling in later calls, so the canvas can say who's at work. The MCP server learns the name from its
52
- * client, and is one session: its calls share an id, apart from any other session of the same agent.
53
- */
54
- function setCaller(name, session = false) {
55
- caller = name ?? caller;
56
- callerId = session ? `${caller}#${randomUUID().slice(0, 8)}` : caller;
57
- }
58
- /** Calls the running Parity app. Fails with a clear message when it isn't running. */
59
- async function call(method, params = {}) {
60
- let connection;
61
- try {
62
- connection = JSON.parse(await readFile(agentFile, "utf8"));
63
- } catch {
64
- throw new Error(`Parity isn't running (no ${agentFile}). Start the app, then try again.`);
65
- }
66
- let response;
67
- try {
68
- response = await fetch(`${connection.url}/rpc`, {
69
- method: "POST",
70
- headers: {
71
- "content-type": "application/json",
72
- authorization: `Bearer ${connection.token}`
73
- },
74
- body: JSON.stringify({
75
- method,
76
- params,
77
- agent: caller,
78
- agentId: callerId
79
- })
80
- });
81
- } catch {
82
- throw new Error(`Parity isn't answering at ${connection.url}. Is the app still running?`);
83
- }
84
- const body = await response.json();
85
- if (body.error !== void 0) throw new Error(body.error);
86
- return body.result;
87
- }
88
- /** A readable account of an exec call, for terminals and for agents. */
89
- function describeExec(result) {
90
- const lines = [];
91
- if (result.ok) lines.push(result.applied ? "Done. The changes are on the canvas as one undoable step." : "Done. Nothing changed.");
92
- else {
93
- lines.push(`Failed, nothing changed: ${result.error}`);
94
- lines.push(`Fix it by retrying with editId "${result.editId}" and edits ({find, replace}) to the snippet.`);
95
- }
96
- if (result.output.length > 0) lines.push("", "Output:", ...result.output);
97
- const created = Object.entries(result.created);
98
- if (created.length > 0) lines.push("", "Created:", ...created.map(([name, id]) => ` ${name} = ${id}`));
99
- if (result.warnings.length > 0) lines.push("", "Warnings (fix these next):", ...result.warnings.map((warning) => ` ${warning}`));
100
- if (result.screenshots.length > 0) lines.push("", "Screenshots:", ...result.screenshots.map((shot) => ` ${shot.design} / ${shot.state} / ${shot.mode}: ${shot.path}`));
101
- return lines.join("\n");
102
- }
103
- const impactOrder = [
104
- "critical",
105
- "serious",
106
- "moderate",
107
- "minor"
108
- ];
109
- /**
110
- * A readable account of an accessibility check: each rule broken, worst first, with the layers that break it, the
111
- * frames they break it in and what's wrong. Alike layers, such as the rows of a list, share a line.
112
- */
113
- function describeAccessibility(result) {
114
- const { design, findings, frames } = result;
115
- const checked = `${design.name} (${design.id}), ${frames === 1 ? "1 frame" : `${frames} frames`}`;
116
- const rules = /* @__PURE__ */ new Map();
117
- for (const finding of findings) rules.set(finding.rule, [...rules.get(finding.rule) ?? [], finding]);
118
- const lines = [rules.size === 0 ? `No issues found in ${checked}, with axe-core against WCAG 2.2 AA.` : `${rules.size === 1 ? "1 rule" : `${rules.size} rules`} broken in ${checked}, with axe-core against WCAG 2.2 AA.`];
119
- const groups = [...rules.values()].sort((a, b) => impactOrder.indexOf(a[0].impact) - impactOrder.indexOf(b[0].impact));
120
- for (const group of groups) {
121
- const { impact, rule, help, helpUrl } = group[0];
122
- lines.push("", `${impact}: ${help} (${rule})`);
123
- const byLayer = /* @__PURE__ */ new Map();
124
- for (const finding of group) {
125
- const key = `${finding.node ?? ""}\n${finding.detail}`;
126
- const layer = byLayer.get(key) ?? {
127
- name: finding.name ?? "The design",
128
- nodes: finding.node ? [finding.node] : [],
129
- detail: finding.detail,
130
- frames: []
131
- };
132
- layer.frames.push({
133
- state: finding.state,
134
- mode: finding.mode
135
- });
136
- byLayer.set(key, layer);
137
- }
138
- const alike = /* @__PURE__ */ new Map();
139
- for (const layer of byLayer.values()) {
140
- const same = JSON.stringify([
141
- layer.name,
142
- layer.detail,
143
- layer.frames
144
- ]);
145
- const existing = alike.get(same);
146
- if (existing) existing.nodes.push(...layer.nodes);
147
- else alike.set(same, layer);
148
- }
149
- for (const layer of alike.values()) {
150
- const where = layer.frames.length === frames && frames > 1 ? "every frame" : frameList(layer.frames);
151
- const ids = layer.nodes.length > 0 ? ` (${layer.nodes.join(", ")})` : "";
152
- lines.push(` ${layer.name}${ids}, in ${where}: ${layer.detail}`);
153
- }
154
- lines.push(` How to fix it: ${helpUrl}`);
155
- }
156
- if (result.warnings.length > 0) lines.push("", "Warnings:", ...result.warnings.map((warning) => ` ${warning}`));
157
- lines.push("", "Automated checks find some issues, not all. Try the design with a keyboard and a screen reader too.");
158
- return lines.join("\n");
159
- }
160
- /** A readable account of the layout problems in a design's frames, each once with the frames it's in. */
161
- function describeProblems(result) {
162
- const { design, problems, frames } = result;
163
- const checked = `${design.name} (${design.id}), ${frames === 1 ? "1 frame" : `${frames} frames`}`;
164
- const grouped = /* @__PURE__ */ new Map();
165
- for (const { problem, node, state, mode } of problems) {
166
- const key = `${node ?? ""}\n${problem}`;
167
- const entry = grouped.get(key) ?? {
168
- problem,
169
- ...node ? { node } : {},
170
- frames: []
171
- };
172
- entry.frames.push({
173
- state,
174
- mode
175
- });
176
- grouped.set(key, entry);
177
- }
178
- const count = grouped.size === 1 ? "1 problem" : `${grouped.size} problems`;
179
- const lines = [grouped.size === 0 ? `No problems found in ${checked}.` : `${count} in ${checked}:`];
180
- for (const { problem, node, frames: where } of grouped.values()) {
181
- const every = where.length === frames && frames > 1 ? "every frame" : frameList(where);
182
- lines.push(` ${node ? `${node}: ` : ""}${problem}, in ${every}`);
183
- }
184
- if (result.warnings.length > 0) lines.push("", "Warnings:", ...result.warnings.map((warning) => ` ${warning}`));
185
- return lines.join("\n");
186
- }
187
- /** Tokens by category, each with its value in every theme that sets one. */
188
- function describeTokens(tokens) {
189
- if (tokens.length === 0) return "No tokens match.";
190
- const lines = [`${tokens.length === 1 ? "1 token" : `${tokens.length} tokens`}. Name them in styles without "--".`];
191
- const categories = /* @__PURE__ */ new Map();
192
- for (const token of tokens) {
193
- const key = `${token.category}, ${token.source}`;
194
- categories.set(key, [...categories.get(key) ?? [], token]);
195
- }
196
- for (const [category, list] of categories) {
197
- lines.push("", `${category}:`);
198
- for (const token of list) {
199
- const values = Object.entries(token.values).sort(([a], [b]) => first("default", a, b)).map(([theme, value]) => `${theme} ${value}`);
200
- lines.push(` ${token.name}: ${values.join(", ")}`);
201
- }
202
- }
203
- return lines.join("\n");
204
- }
205
- /**
206
- * The project's own components, then each library's, then the icon libraries, then the exports left out and why, the
207
- * components failing to draw and why, and what the person corrected by hand.
208
- */
209
- function describeComponents({ components, iconLibraries = [], leftOut = [], drawn = 0, failing = [], corrections = {} }) {
210
- const groups = /* @__PURE__ */ new Map();
211
- for (const component of components) {
212
- const slash = component.indexOf("/");
213
- const group = slash > 0 ? component.slice(0, slash) : "";
214
- groups.set(group, [...groups.get(group) ?? [], slash > 0 ? component.slice(slash + 1) : component]);
215
- }
216
- const lines = [];
217
- for (const [group, names] of [...groups].sort(([a], [b]) => first("", a, b))) lines.push(group ? `${group}, as ${group}/Name:` : "The project's own:", ` ${names.join(", ")}`, "");
218
- if (iconLibraries.length > 0) lines.push(`Icons: ${iconLibraries.map((library) => `${library}, as ${library}/Name`).join("; ")}`, "");
219
- if (leftOut.length > 0) {
220
- lines.push("Not components:");
221
- for (const entry of leftOut) lines.push(` ${entry.name}, in ${entry.path}: ${entry.undecided ? "can't tell, " : ""}${entry.reason}`);
222
- lines.push("");
223
- }
224
- if (failing.length > 0) {
225
- lines.push(`Failing to draw on the canvas, ${failing.length} of ${failing.length + drawn} drawn so far:`);
226
- for (const entry of failing) lines.push(` ${entry.id}: ${entry.reason}`);
227
- lines.push("");
228
- }
229
- const { include = [], exclude = [] } = corrections;
230
- if (include.length > 0 || exclude.length > 0) {
231
- const name = (ref) => ref.export === "default" ? "the default export" : ref.export;
232
- lines.push("Corrected by hand in the project file:");
233
- for (const ref of include) lines.push(` listed: ${name(ref)}, in ${ref.path}`);
234
- for (const ref of exclude) lines.push(` taken off: ${name(ref)}, in ${ref.path}`);
235
- }
236
- return lines.join("\n").trimEnd();
237
- }
238
- /** A component's props, with each variant's options and default. */
239
- function describeProps(result) {
240
- if (!result.known) return `${result.component}'s props haven't been read from its source yet. Try again in a moment.`;
241
- const imported = result.import && result.import !== result.path ? `, imported from "${result.import}"` : "";
242
- const lines = [`${result.component}, from ${result.path}${imported}:`];
243
- for (const prop of result.props) {
244
- const options = prop.options ? `: ${prop.options.map((option) => JSON.stringify(option)).join(", ")}` : "";
245
- const fallback = prop.default === void 0 ? "" : `, default ${JSON.stringify(prop.default)}`;
246
- lines.push(` ${prop.name} (${prop.kind}${prop.required ? ", required" : ""})${options}${fallback}`);
247
- }
248
- if (result.props.length === 0) lines.push(" No props of its own. --all lists the attributes it passes on.");
249
- return lines.join("\n");
250
- }
251
- /** Frames as state and mode ids, naming a mode once when they share it: "default, error in desktop-light". */
252
- function frameList(frames) {
253
- const modes = [...new Set(frames.map((frame) => frame.mode))];
254
- if (modes.length === 1) return `${frames.map((frame) => frame.state).join(", ")} in ${modes[0]}`;
255
- return frames.map((frame) => `${frame.state} in ${frame.mode}`).join(", ");
256
- }
257
- /** A readable account of what a project's code wrote to the console, oldest first, with where each came from. */
258
- function describeConsole(result) {
259
- const { tab, entries } = result;
260
- if (entries.length === 0) return `${tab.name}'s code hasn't written anything to the console.`;
261
- const lines = [`${entries.length === 1 ? "1 message" : `${entries.length} messages`} from ${tab.name}'s code, oldest first:`];
262
- for (const entry of entries) {
263
- const time = new Date(entry.last).toLocaleTimeString([], { hour12: false });
264
- const repeats = entry.count > 1 ? ` (${entry.count} times)` : "";
265
- const [first = "", ...rest] = entry.text.split("\n");
266
- lines.push("", `${time} ${entry.level}${repeats}: ${first}`, ...rest.map((line) => ` ${line}`));
267
- const offscreen = entry.where === "offscreen" ? ", in an offscreen render" : "";
268
- lines.push(` at ${scriptPath(entry.source)}:${entry.line}${offscreen}`);
269
- }
270
- return lines.join("\n");
271
- }
272
- /** Compares two names alphabetically, with `name` before every other. */
273
- function first(name, a, b) {
274
- if (a === name) return -1;
275
- if (b === name) return 1;
276
- return a.localeCompare(b);
277
- }
278
- /** A script's path on its server, without the cache-busting query Vite adds. */
279
- function scriptPath(url) {
280
- try {
281
- return new URL(url).pathname;
282
- } catch {
283
- return url || "an unknown script";
284
- }
285
- }
286
- export { describeConsole as a, describeProps as c, mcpCallerName as d, describeComponents as i, describeTokens as l, call as n, describeExec as o, describeAccessibility as r, describeProblems as s, agentFile as t, setCaller as u };