@parity-app/cli 0.1.0 → 0.3.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/dist/bin.mjs CHANGED
@@ -1,9 +1,283 @@
1
1
  #!/usr/bin/env node
2
- import { a as describeConsole, c as describeProps, d as mcpCallerName, i as describeComponents, l as describeTokens, n as call, o as describeExec, r as describeAccessibility, s as describeProblems, u as setCaller } from "./client-DsTLQ2lT.mjs";
2
+ import { _ as errorDetails, a as describeExec, c as describeProps, f as setCaller, g as errorCodes, h as closest, i as describeConsole, l as describeStatus, m as CodedError, n as describeAccessibility, o as describeModes, p as mcpCallerName, r as describeComponents, s as describeProblems, t as call, u as describeTokens, v as readErrorDetails } from "./client-Cf66-n3R.mjs";
3
3
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, normalize, relative, resolve, sep } from "node:path";
5
5
  import { parseArgs } from "node:util";
6
6
  import { createInterface } from "node:readline";
7
+ // src/errors.ts
8
+ /** A request field that isn't valid, rejected before anything reaches the app. */
9
+ function invalidInput(field, message) {
10
+ return new CodedError(message, {
11
+ code: "invalid_input",
12
+ ...field ? { field } : {}
13
+ });
14
+ }
15
+ /**
16
+ * A thrown value as an operation error. Invalid input always points at the request; a field the app names becomes a
17
+ * path only when the operation has that input: an unknown design inside a snippet has no `design` input to point at.
18
+ */
19
+ function operationError(error, fields) {
20
+ const message = error instanceof Error ? error.message : String(error);
21
+ const details = errorDetails(error) ?? {
22
+ code: "app_error",
23
+ retryable: false
24
+ };
25
+ const field = details.field;
26
+ const known = field !== void 0 && (details.code === "invalid_input" || !fields || fields.includes(field.split("/")[0]));
27
+ return {
28
+ code: details.code,
29
+ message,
30
+ retryable: details.retryable,
31
+ ...known ? { path: `/${field}` } : {},
32
+ ...details.alternatives?.length ? { alternatives: details.alternatives } : {}
33
+ };
34
+ }
35
+ /** Renames the top-level field a path points at, for an adapter whose names differ from the contract's. */
36
+ function renamePath(error, names) {
37
+ if (!error.path) return error;
38
+ const [, first = "", ...rest] = error.path.split("/");
39
+ if (!Object.hasOwn(names, first)) return error;
40
+ return {
41
+ ...error,
42
+ path: [
43
+ "",
44
+ names[first],
45
+ ...rest
46
+ ].join("/")
47
+ };
48
+ }
49
+ /** The error's schema. What each field means is in the CLI's README and the skill, to keep every tool's schema small. */
50
+ const operationErrorSchema = {
51
+ type: "object",
52
+ properties: {
53
+ code: {
54
+ type: "string",
55
+ enum: [...errorCodes]
56
+ },
57
+ message: { type: "string" },
58
+ retryable: { type: "boolean" },
59
+ path: { type: "string" },
60
+ alternatives: {
61
+ type: "array",
62
+ items: { type: "string" }
63
+ },
64
+ editId: { type: "string" }
65
+ },
66
+ required: [
67
+ "code",
68
+ "message",
69
+ "retryable"
70
+ ]
71
+ };
72
+ // src/execute.ts
73
+ /** The same request fields in both adapters' paths, before MCP renames label and code. */
74
+ const executeFields = [
75
+ "tab",
76
+ "label",
77
+ "code",
78
+ "editId",
79
+ "edits"
80
+ ];
81
+ /** A reply as the canonical result: the failure's message, code and retry id together in one error. */
82
+ function execResponse(reply, fields = executeFields) {
83
+ const { error, details, editId, ...rest } = reply;
84
+ if (reply.ok || error === void 0) return rest;
85
+ const coded = readErrorDetails(details);
86
+ const failure = operationError(coded ? new CodedError(error, coded) : new CodedError(error, { code: "execution_failed" }), fields);
87
+ return {
88
+ ...rest,
89
+ error: {
90
+ ...failure,
91
+ ...editId ? { editId } : {}
92
+ }
93
+ };
94
+ }
95
+ /** The result as structured data: everything but image data, which MCP sends as image blocks. */
96
+ function structuredExec(result) {
97
+ return {
98
+ ...result,
99
+ screenshots: result.screenshots.map(({ png: _png, ...shot }) => shot)
100
+ };
101
+ }
102
+ /** Execute's and screenshot's structured result. */
103
+ const execOutputSchema = {
104
+ type: "object",
105
+ properties: {
106
+ ok: { type: "boolean" },
107
+ applied: {
108
+ type: "boolean",
109
+ description: "Changes landed on the canvas as one undoable step."
110
+ },
111
+ output: {
112
+ type: "array",
113
+ items: { type: "string" },
114
+ description: "Lines from Print."
115
+ },
116
+ warnings: {
117
+ type: "array",
118
+ items: { type: "string" }
119
+ },
120
+ created: {
121
+ type: "object",
122
+ additionalProperties: { type: "string" },
123
+ description: "Names of designs and nodes created, mapped to their ids."
124
+ },
125
+ screenshots: {
126
+ type: "array",
127
+ items: {
128
+ type: "object",
129
+ properties: {
130
+ path: {
131
+ type: "string",
132
+ description: "Where the PNG was saved."
133
+ },
134
+ design: { type: "string" },
135
+ state: { type: "string" },
136
+ mode: { type: "string" }
137
+ },
138
+ required: [
139
+ "path",
140
+ "design",
141
+ "state",
142
+ "mode"
143
+ ]
144
+ }
145
+ },
146
+ error: operationErrorSchema
147
+ },
148
+ required: [
149
+ "ok",
150
+ "applied",
151
+ "output",
152
+ "warnings",
153
+ "created",
154
+ "screenshots"
155
+ ]
156
+ };
157
+ const defaultLabel = "Edit designs";
158
+ /** What every call's tab field says, the same in the CLI's help, MCP tools and the skill (PAR-514). */
159
+ const tabDescription = "Tab id, project root or name, or \"active\" for what the person is looking at. Defaults to the open project the current folder is in, else \"active\".";
160
+ /** Canonical properties, also used by MCP discovery after renaming label and code. */
161
+ const executeProperties = {
162
+ tab: {
163
+ type: "string",
164
+ minLength: 1,
165
+ description: tabDescription
166
+ },
167
+ label: {
168
+ type: "string",
169
+ minLength: 1,
170
+ default: defaultLabel,
171
+ description: "Shown while it runs. Prefer 3 to 8 words, e.g. \"Add pricing tiers\"."
172
+ },
173
+ code: {
174
+ type: "string",
175
+ minLength: 1,
176
+ description: "The snippet. Omit when retrying with editId."
177
+ },
178
+ editId: {
179
+ type: "string",
180
+ minLength: 1,
181
+ description: "The failed call's editId, to retry with edits."
182
+ },
183
+ edits: {
184
+ type: "array",
185
+ description: "Exact find and replace edits to the failed snippet, applied in order.",
186
+ items: {
187
+ type: "object",
188
+ additionalProperties: false,
189
+ properties: {
190
+ find: {
191
+ type: "string",
192
+ minLength: 1
193
+ },
194
+ replace: { type: "string" },
195
+ all: { type: "boolean" }
196
+ },
197
+ required: ["find", "replace"]
198
+ }
199
+ }
200
+ };
201
+ function optionalText(value, field) {
202
+ if (value === void 0) return void 0;
203
+ if (typeof value !== "string" || !value.trim()) throw invalidInput(field, `${field} must be a nonblank string`);
204
+ return value;
205
+ }
206
+ function edit(value, index) {
207
+ const at = `edits/${index}`;
208
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidInput(at, "Each edit must be an object");
209
+ if (!("find" in value) || typeof value.find !== "string" || !value.find.length) throw invalidInput(`${at}/find`, "Each edit needs a nonempty find string");
210
+ if (!("replace" in value) || typeof value.replace !== "string") throw invalidInput(`${at}/replace`, "Each edit needs a replace string");
211
+ if ("all" in value && typeof value.all !== "boolean") throw invalidInput(`${at}/all`, "Edit all must be a boolean");
212
+ const unknown = Object.keys(value).find((key) => ![
213
+ "find",
214
+ "replace",
215
+ "all"
216
+ ].includes(key));
217
+ if (unknown) throw invalidInput(`${at}/${unknown}`, "Unknown edit field");
218
+ return {
219
+ find: value.find,
220
+ replace: value.replace,
221
+ ..."all" in value && typeof value.all === "boolean" ? { all: value.all } : {}
222
+ };
223
+ }
224
+ /** Validate once, before either transport sends a request to the desktop. Never rewrite snippet or patch text. */
225
+ function executeRequest(input) {
226
+ const tab = optionalText(input.tab, "tab");
227
+ const label = optionalText(input.label, "label") ?? defaultLabel;
228
+ const code = optionalText(input.code, "code");
229
+ const editId = optionalText(input.editId, "editId");
230
+ if (!code && !editId) throw invalidInput("code", "Nothing to run: pass a snippet, or an editId with edits");
231
+ if (code !== void 0 && editId !== void 0) throw invalidInput("editId", "Pass a snippet or an editId, not both");
232
+ if (input.edits !== void 0 && !Array.isArray(input.edits)) throw invalidInput("edits", "edits must be an array");
233
+ if (input.edits !== void 0 && !editId) throw invalidInput("edits", "edits requires an editId");
234
+ const edits = Array.isArray(input.edits) ? input.edits.map(edit) : void 0;
235
+ return {
236
+ label,
237
+ ...tab === void 0 ? {} : { tab },
238
+ ...code === void 0 ? {} : { code },
239
+ ...editId === void 0 ? {} : {
240
+ editId,
241
+ edits: edits ?? []
242
+ }
243
+ };
244
+ }
245
+ /** Execution failures have the same meaning for CLI exits and MCP tool errors. */
246
+ function executionFailed(result) {
247
+ return !result.ok;
248
+ }
249
+ /** One handler for both adapters. Caller identity stays in the transport's connection context. */
250
+ const executeOperation = {
251
+ failed: executionFailed,
252
+ fields: executeFields,
253
+ outputSchema: execOutputSchema,
254
+ structured: structuredExec,
255
+ /** Discovery uses the same contract, with just the two historical MCP field aliases. */
256
+ inputSchema(names = {
257
+ label: "label",
258
+ code: "code"
259
+ }) {
260
+ const { label, code, ...rest } = executeProperties;
261
+ return {
262
+ type: "object",
263
+ properties: {
264
+ ...rest,
265
+ [names.label]: label,
266
+ [names.code]: code
267
+ },
268
+ oneOf: [{
269
+ required: [names.code],
270
+ not: { anyOf: [{ required: ["editId"] }, { required: ["edits"] }] }
271
+ }, {
272
+ required: ["editId"],
273
+ not: { required: [names.code] }
274
+ }]
275
+ };
276
+ },
277
+ async run(input, send) {
278
+ return execResponse(await send("exec", { ...executeRequest(input) }));
279
+ }
280
+ };
7
281
  // src/check.ts
8
282
  /**
9
283
  * `parity lib check <library>`: inserts every component of a registry library file into the running app, once on its
@@ -13,8 +287,8 @@ import { createInterface } from "node:readline";
13
287
  */
14
288
  async function runCheck(library, options) {
15
289
  if (!library) throw new Error("parity lib check <library> [--tab t] [--mode m]");
16
- const { applyLibraryEdits, componentNode, generateId, librarySource, parseLibraryEdits, withLibraryContext } = await import("./src-FF8eY6EI.mjs");
17
- const { registryDir, registryEntries } = await import("./src-DYi23Ypb.mjs");
290
+ const { applyLibraryEdits, componentNode, generateId, librarySource, parseLibraryEdits, withLibraryContext } = await import("./src-DZKi9702.mjs");
291
+ const { registryDir, registryEntries } = await import("./src-BGF7szJh.mjs");
18
292
  const entry = (await registryEntries()).find((item) => item.manifest.id === library);
19
293
  if (!entry) throw new Error(`The registry has no library "${library}"`);
20
294
  let manifest = entry.manifest;
@@ -31,12 +305,12 @@ async function runCheck(library, options) {
31
305
  }]));
32
306
  const mode = options.mode ?? "desktop-light";
33
307
  const exec = async (label, code) => {
34
- const result = await call("exec", {
308
+ const result = await executeOperation.run({
35
309
  tab: options.tab,
36
310
  label,
37
311
  code
38
- });
39
- if (!result.ok) throw new Error(result.error ?? `${label} failed`);
312
+ }, call);
313
+ if (executeOperation.failed(result)) throw new Error(result.error?.message ?? `${label} failed`);
40
314
  const line = result.output.find((item) => item.startsWith("{") || item.startsWith("["));
41
315
  if (!line) throw new Error(`${label} printed nothing`);
42
316
  return line;
@@ -142,8 +416,8 @@ function describeCheck(report) {
142
416
  * because it reads the project's files and its own copy of TypeScript.
143
417
  */
144
418
  async function runLib(packageName, options) {
145
- const { appManifestPath, generateLibrary, manifestText, mergeManifest, reviseLibrary } = await import("./src-8MhDV_sA.mjs");
146
- const { covers, registryDir, registryEntries } = await import("./src-DYi23Ypb.mjs");
419
+ const { appManifestPath, generateLibrary, manifestText, mergeManifest, reviseLibrary } = await import("./src-Aoey5Uq0.mjs");
420
+ const { covers, registryDir, registryEntries } = await import("./src-BGF7szJh.mjs");
147
421
  const spec = packageName === void 0 ? void 0 : packageSpec(packageName);
148
422
  let root = resolve(options.root ?? ".");
149
423
  if (spec && (spec.version !== void 0 || options.registry && !options.root)) {
@@ -154,7 +428,7 @@ async function runLib(packageName, options) {
154
428
  console.error(`Installing ${spec.name} ${version} on its own…`);
155
429
  root = await installedVersion(spec.name, version);
156
430
  }
157
- const { parseLibraryEdits } = await import("./src-8MhDV_sA.mjs");
431
+ const { parseLibraryEdits } = await import("./src-Aoey5Uq0.mjs");
158
432
  const trialEdits = options.edits ? parseLibraryEdits(JSON.parse(await readFile(resolve(options.edits), "utf8"))) : void 0;
159
433
  const recordedId = options.registry && spec ? options.id ?? (await registryEntries()).find((entry) => entry.manifest.package?.name === spec.name)?.manifest.id : void 0;
160
434
  const appErrors = recordedId ? await readAppErrors(registryDir(), recordedId) : void 0;
@@ -304,17 +578,693 @@ function describeReport(report) {
304
578
  for (const note of report.notes) lines.push(`Note: ${note}`);
305
579
  return lines.join("\n");
306
580
  }
581
+ // src/operations.ts
582
+ function text$1(description, required = false, fallback) {
583
+ return {
584
+ schema: {
585
+ type: "string",
586
+ minLength: 1,
587
+ pattern: "\\S",
588
+ description,
589
+ ...fallback === void 0 ? {} : { default: fallback }
590
+ },
591
+ required,
592
+ read(value, name) {
593
+ if (value === void 0 && !required) return fallback;
594
+ if (typeof value !== "string" || !value.trim()) throw invalidInput(name, `${name} must be a nonblank string`);
595
+ return value;
596
+ }
597
+ };
598
+ }
599
+ function boolean(description) {
600
+ return {
601
+ schema: {
602
+ type: "boolean",
603
+ default: false,
604
+ description
605
+ },
606
+ read(value, name) {
607
+ if (value === void 0) return false;
608
+ if (typeof value !== "boolean") throw invalidInput(name, `${name} must be a boolean`);
609
+ return value;
610
+ }
611
+ };
612
+ }
613
+ function selector(description, fallback) {
614
+ const item = {
615
+ type: "string",
616
+ minLength: 1,
617
+ pattern: "\\S"
618
+ };
619
+ return {
620
+ schema: {
621
+ oneOf: [item, {
622
+ type: "array",
623
+ minItems: 1,
624
+ items: item
625
+ }],
626
+ description,
627
+ ...fallback ? { default: fallback } : {}
628
+ },
629
+ read(value, name) {
630
+ if (value === void 0) return fallback;
631
+ if (typeof value === "string" && value.trim()) return value;
632
+ if (Array.isArray(value) && value.length && value.every((item) => typeof item === "string" && item.trim())) return value.length === 1 ? value[0] : value;
633
+ throw invalidInput(name, `${name} must be a nonblank string or a nonempty list of nonblank strings`);
634
+ }
635
+ };
636
+ }
637
+ /** How many items a list read returns at most: a default that keeps a reply small, and a ceiling. */
638
+ function limit(description, fallback, most) {
639
+ return {
640
+ schema: {
641
+ type: "integer",
642
+ minimum: 1,
643
+ maximum: most,
644
+ default: fallback,
645
+ description
646
+ },
647
+ read(value, name) {
648
+ if (value === void 0) return fallback;
649
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > most) throw invalidInput(name, `${name} must be a whole number from 1 to ${most}`);
650
+ return value;
651
+ }
652
+ };
653
+ }
654
+ const tab = text$1(tabDescription);
655
+ const design = text$1("Design id or name.", true);
656
+ const scale = {
657
+ schema: {
658
+ type: "number",
659
+ minimum: .25,
660
+ maximum: 3,
661
+ default: 1,
662
+ description: "Pixel density."
663
+ },
664
+ read(value) {
665
+ if (value === void 0) return 1;
666
+ if (typeof value !== "number" || !Number.isFinite(value) || value < .25 || value > 3) throw invalidInput("scale", "scale must be a finite number from 0.25 to 3");
667
+ return value;
668
+ }
669
+ };
670
+ const rootText = text$1("Absolute project folder path.", true);
671
+ const absoluteRoot = /^(?:\/|[A-Za-z]:[\\/]|\\\\)/;
672
+ const root$1 = {
673
+ ...rootText,
674
+ schema: {
675
+ ...rootText.schema,
676
+ pattern: absoluteRoot.source
677
+ },
678
+ read(value) {
679
+ const path = rootText.read(value, "root");
680
+ if (typeof path !== "string" || !absoluteRoot.test(path)) throw invalidInput("root", "root must be an absolute project folder path");
681
+ return path;
682
+ }
683
+ };
684
+ function fieldsContract(fields) {
685
+ return {
686
+ inputSchema: {
687
+ type: "object",
688
+ additionalProperties: false,
689
+ properties: Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, field.schema])),
690
+ required: Object.entries(fields).filter(([, field]) => field.required).map(([key]) => key)
691
+ },
692
+ request(input) {
693
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw invalidInput(void 0, "Operation input must be an object");
694
+ for (const key of Object.keys(input)) if (!Object.hasOwn(fields, key)) throw invalidInput(key, `Unknown input field ${key}`);
695
+ const result = {};
696
+ for (const [key, field] of Object.entries(fields)) {
697
+ const value = field.read(input[key], key);
698
+ if (value !== void 0) result[key] = value;
699
+ }
700
+ return result;
701
+ }
702
+ };
703
+ }
704
+ /** An operation whose canonical result is built from the app's reply, given the validated request. */
705
+ function mapped(method, fields, outputSchema, result, options = {}) {
706
+ const contract = fieldsContract(fields);
707
+ const routing = routingFor(fields);
708
+ return {
709
+ inputSchema: contract.inputSchema,
710
+ outputSchema,
711
+ fields: Object.keys(fields),
712
+ routing,
713
+ async run(input, send) {
714
+ const request = contract.request(input);
715
+ return result(await send(method, request, routing), request);
716
+ },
717
+ failed: options.failed ?? (() => false),
718
+ structured: options.structured ?? ((value) => Object.fromEntries(Object.entries(value)))
719
+ };
720
+ }
721
+ /** Which copy a call goes to, from its fields: see `Operation.routing`. */
722
+ function routingFor(fields) {
723
+ if ("tab" in fields) return "tab";
724
+ if ("root" in fields) return "open";
725
+ return "copies";
726
+ }
727
+ /** An operation whose canonical result is the app's reply. */
728
+ function operation(method, fields, outputSchema, options = {}) {
729
+ return mapped(method, fields, outputSchema, (reply) => reply, options);
730
+ }
731
+ /** The first `count` items, with how many there were, so a reply says when it left some out. */
732
+ function bounded(items, count, keep = "first") {
733
+ const kept = keep === "first" ? items.slice(0, count) : items.slice(Math.max(0, items.length - count));
734
+ return {
735
+ items: kept,
736
+ total: items.length,
737
+ truncated: kept.length < items.length
738
+ };
739
+ }
740
+ const string = { type: "string" };
741
+ const strings = {
742
+ type: "array",
743
+ items: string
744
+ };
745
+ const integer = { type: "integer" };
746
+ const boolean_ = { type: "boolean" };
747
+ function object(properties, optional = []) {
748
+ return {
749
+ type: "object",
750
+ properties,
751
+ required: Object.keys(properties).filter((key) => !optional.includes(key))
752
+ };
753
+ }
754
+ const ref = object({
755
+ id: string,
756
+ name: string
757
+ });
758
+ const tabSchema = object({
759
+ id: string,
760
+ name: string,
761
+ root: string,
762
+ active: boolean_,
763
+ status: {
764
+ type: "string",
765
+ enum: [
766
+ "loading",
767
+ "ready",
768
+ "error"
769
+ ]
770
+ },
771
+ error: string,
772
+ designs: {
773
+ ...integer,
774
+ description: "How many designs the project has."
775
+ }
776
+ }, ["error"]);
777
+ const frameSchema = {
778
+ state: string,
779
+ mode: string
780
+ };
781
+ const designSchema = object({
782
+ id: string,
783
+ name: string,
784
+ kind: string,
785
+ file: string,
786
+ x: { type: "number" },
787
+ y: { type: "number" },
788
+ width: { type: "number" },
789
+ height: { type: "number" },
790
+ states: {
791
+ type: "array",
792
+ items: ref
793
+ },
794
+ showing: object({
795
+ state: string,
796
+ mode: string,
797
+ beside: strings
798
+ }),
799
+ nodes: integer
800
+ }, ["width", "height"]);
801
+ const stateSchema = object({
802
+ tab: tabSchema,
803
+ selection: object({
804
+ design: string,
805
+ node: string,
806
+ mode: string
807
+ }, [
808
+ "design",
809
+ "node",
810
+ "mode"
811
+ ]),
812
+ designs: {
813
+ type: "array",
814
+ items: designSchema
815
+ },
816
+ overlaps: {
817
+ type: "array",
818
+ items: object({
819
+ designs: strings,
820
+ names: strings
821
+ })
822
+ },
823
+ modes: {
824
+ type: "array",
825
+ items: object({
826
+ id: string,
827
+ name: string,
828
+ width: { type: "number" },
829
+ primary: boolean_
830
+ })
831
+ },
832
+ components: strings,
833
+ tokens: object({
834
+ count: integer,
835
+ semantic: strings,
836
+ themeCategories: strings
837
+ }),
838
+ viewport: { type: "object" },
839
+ detection: { type: "object" }
840
+ }, ["viewport", "detection"]);
841
+ const truncation = {
842
+ total: {
843
+ ...integer,
844
+ description: "How many there were before the limit."
845
+ },
846
+ truncated: {
847
+ ...boolean_,
848
+ description: "Some were left out; narrow the read or raise limit."
849
+ }
850
+ };
851
+ const tokensSchema = object({
852
+ tokens: {
853
+ type: "array",
854
+ items: object({
855
+ name: string,
856
+ category: string,
857
+ source: string,
858
+ values: {
859
+ type: "object",
860
+ additionalProperties: string,
861
+ description: "Value per theme."
862
+ }
863
+ })
864
+ },
865
+ ...truncation
866
+ });
867
+ const consoleSchema = object({
868
+ tab: ref,
869
+ entries: {
870
+ type: "array",
871
+ items: object({
872
+ level: {
873
+ type: "string",
874
+ enum: [
875
+ "info",
876
+ "warning",
877
+ "error",
878
+ "debug"
879
+ ]
880
+ },
881
+ text: string,
882
+ source: string,
883
+ line: integer,
884
+ where: {
885
+ type: "string",
886
+ enum: ["canvas", "offscreen"]
887
+ },
888
+ count: integer,
889
+ first: { type: "number" },
890
+ last: { type: "number" }
891
+ })
892
+ },
893
+ ...truncation
894
+ });
895
+ const problemsSchema = object({
896
+ tab: ref,
897
+ design: ref,
898
+ problems: {
899
+ type: "array",
900
+ items: object({
901
+ ...frameSchema,
902
+ problem: string,
903
+ node: string
904
+ }, ["node"])
905
+ },
906
+ frames: integer,
907
+ warnings: strings
908
+ });
909
+ const accessibilitySchema = object({
910
+ tab: ref,
911
+ design: ref,
912
+ findings: {
913
+ type: "array",
914
+ items: object({
915
+ ...frameSchema,
916
+ rule: string,
917
+ impact: {
918
+ type: "string",
919
+ enum: [
920
+ "critical",
921
+ "serious",
922
+ "moderate",
923
+ "minor"
924
+ ]
925
+ },
926
+ help: string,
927
+ helpUrl: string,
928
+ node: string,
929
+ name: string,
930
+ detail: string
931
+ }, ["node", "name"])
932
+ },
933
+ frames: integer,
934
+ warnings: strings
935
+ });
936
+ const componentsSchema = object({
937
+ components: strings,
938
+ iconLibraries: strings,
939
+ leftOut: {
940
+ type: "array",
941
+ items: object({
942
+ name: string,
943
+ path: string,
944
+ reason: string,
945
+ undecided: boolean_
946
+ }, ["undecided"])
947
+ },
948
+ drawn: integer,
949
+ failing: {
950
+ type: "array",
951
+ items: object({
952
+ id: string,
953
+ problem: {
954
+ type: "string",
955
+ enum: [
956
+ "provider",
957
+ "server",
958
+ "async",
959
+ "error"
960
+ ]
961
+ },
962
+ reason: string
963
+ })
964
+ },
965
+ corrections: { type: "object" }
966
+ }, [
967
+ "iconLibraries",
968
+ "leftOut",
969
+ "drawn",
970
+ "failing",
971
+ "corrections"
972
+ ]);
973
+ const propsSchema = object({
974
+ component: string,
975
+ path: string,
976
+ import: string,
977
+ known: boolean_,
978
+ props: {
979
+ type: "array",
980
+ items: object({
981
+ name: string,
982
+ kind: string,
983
+ group: string,
984
+ options: {
985
+ type: "array",
986
+ items: { type: [
987
+ "string",
988
+ "number",
989
+ "boolean"
990
+ ] }
991
+ },
992
+ default: { type: [
993
+ "string",
994
+ "number",
995
+ "boolean",
996
+ "null"
997
+ ] },
998
+ required: boolean_
999
+ }, [
1000
+ "group",
1001
+ "options",
1002
+ "default",
1003
+ "required"
1004
+ ])
1005
+ }
1006
+ }, ["path", "import"]);
1007
+ const viewportSchema = object({
1008
+ id: string,
1009
+ name: string,
1010
+ width: { type: "number" },
1011
+ height: { type: "number" },
1012
+ device: string
1013
+ }, ["device"]);
1014
+ const frameReportSchema = object({
1015
+ mode: string,
1016
+ width: { type: "number" },
1017
+ height: {
1018
+ type: "number",
1019
+ description: "Fixed frame height, which clips; absent when it grows."
1020
+ },
1021
+ window: object({
1022
+ width: { type: "number" },
1023
+ height: { type: "number" }
1024
+ })
1025
+ }, ["height"]);
1026
+ const modesSchema = object({
1027
+ tab: ref,
1028
+ variables: {
1029
+ type: "array",
1030
+ items: object({
1031
+ id: string,
1032
+ name: string,
1033
+ kind: {
1034
+ type: "string",
1035
+ enum: [
1036
+ "viewport",
1037
+ "theme",
1038
+ "locale",
1039
+ "custom"
1040
+ ]
1041
+ },
1042
+ options: {
1043
+ type: "array",
1044
+ items: { type: "object" }
1045
+ }
1046
+ })
1047
+ },
1048
+ viewports: {
1049
+ type: "array",
1050
+ items: viewportSchema,
1051
+ description: "Viewport ids, which Responsive and a node's responsive patches take. Not mode ids."
1052
+ },
1053
+ modes: {
1054
+ type: "array",
1055
+ description: "Mode ids, which renders (screenshot, problems, accessibility) and a design's modes take.",
1056
+ items: object({
1057
+ id: string,
1058
+ name: string,
1059
+ primary: boolean_,
1060
+ viewport: string,
1061
+ width: { type: "number" },
1062
+ height: { type: "number" },
1063
+ values: {
1064
+ type: "object",
1065
+ additionalProperties: string
1066
+ },
1067
+ classNames: strings,
1068
+ attributes: {
1069
+ type: "object",
1070
+ additionalProperties: string
1071
+ }
1072
+ })
1073
+ },
1074
+ designs: {
1075
+ type: "array",
1076
+ items: object({
1077
+ id: string,
1078
+ name: string,
1079
+ state: string,
1080
+ states: strings,
1081
+ main: string,
1082
+ beside: strings,
1083
+ frames: {
1084
+ type: "array",
1085
+ items: frameReportSchema
1086
+ }
1087
+ }, ["frames"])
1088
+ }
1089
+ });
1090
+ const historySide = object({
1091
+ count: integer,
1092
+ labels: {
1093
+ ...strings,
1094
+ description: "With summary: newest first, up to limit."
1095
+ }
1096
+ }, ["labels"]);
1097
+ const statusSchema = object({
1098
+ tab: ref,
1099
+ revision: {
1100
+ type: "string",
1101
+ description: "Opaque; the same only while nothing in the tab has changed. Compare it with state's revision."
1102
+ },
1103
+ dirty: {
1104
+ ...boolean_,
1105
+ description: "Something applied (and undoable) isn't on disk yet."
1106
+ },
1107
+ autosave: {
1108
+ ...boolean_,
1109
+ description: "Changes save themselves; otherwise they wait for the person's Save."
1110
+ },
1111
+ unsaved: {
1112
+ type: "array",
1113
+ items: object({
1114
+ path: string,
1115
+ kind: {
1116
+ type: "string",
1117
+ enum: ["document", "stylesheet"]
1118
+ },
1119
+ designs: object({
1120
+ added: strings,
1121
+ changed: strings,
1122
+ removed: strings
1123
+ }),
1124
+ modes: boolean_,
1125
+ tokens: strings,
1126
+ more: integer
1127
+ }, [
1128
+ "designs",
1129
+ "modes",
1130
+ "tokens",
1131
+ "more"
1132
+ ])
1133
+ },
1134
+ tokenEdits: {
1135
+ ...integer,
1136
+ description: "Token values waiting for Save to write them to the stylesheet."
1137
+ },
1138
+ save: object({
1139
+ status: {
1140
+ type: "string",
1141
+ enum: [
1142
+ "saved",
1143
+ "unsaved",
1144
+ "saving",
1145
+ "failed"
1146
+ ]
1147
+ },
1148
+ error: string
1149
+ }, ["error"]),
1150
+ errors: {
1151
+ type: "array",
1152
+ items: object({
1153
+ path: string,
1154
+ error: string
1155
+ })
1156
+ },
1157
+ undo: historySide,
1158
+ redo: historySide
1159
+ }, []);
1160
+ const checks = {
1161
+ tab,
1162
+ design,
1163
+ state: selector("State id, list of ids, or \"all\".", "all"),
1164
+ mode: selector("Mode id, list of ids, \"shown\" or \"all\".", "shown")
1165
+ };
1166
+ const operations = {
1167
+ tabs: mapped("tabs", {}, object({ tabs: {
1168
+ type: "array",
1169
+ items: tabSchema
1170
+ } }), (tabs) => ({ tabs })),
1171
+ state: operation("state", { tab }, stateSchema),
1172
+ status: operation("status", {
1173
+ tab,
1174
+ summary: boolean("Add each unsaved file's designs and token values, and the undo and redo labels."),
1175
+ limit: limit("Most ids or labels in each list of the summary.", 10, 100)
1176
+ }, statusSchema),
1177
+ modes: operation("modes", {
1178
+ tab,
1179
+ design: text$1("Design id or name. With it, just that design, with each of its frames' sizes.")
1180
+ }, modesSchema),
1181
+ open: operation("open", { root: root$1 }, tabSchema),
1182
+ screenshot: mapped("screenshot", {
1183
+ tab,
1184
+ design,
1185
+ scale,
1186
+ state: selector("State id, list of ids, or \"all\". Defaults to the state the design shows."),
1187
+ mode: selector("Mode id, list of ids, \"shown\" or \"all\". Defaults to the primary mode.")
1188
+ }, execOutputSchema, (reply) => execResponse(reply, [
1189
+ "tab",
1190
+ "design",
1191
+ "scale",
1192
+ "state",
1193
+ "mode"
1194
+ ]), {
1195
+ failed: executionFailed,
1196
+ structured: structuredExec
1197
+ }),
1198
+ problems: operation("problems", checks, problemsSchema, { failed: (result) => result.problems.length > 0 }),
1199
+ accessibility: operation("accessibility", checks, accessibilitySchema, { failed: (result) => result.findings.length > 0 }),
1200
+ console: mapped("console", {
1201
+ tab,
1202
+ clear: boolean("Empty the console after reading, including entries beyond limit."),
1203
+ limit: limit("Most entries to return, newest kept.", 50, 1e3)
1204
+ }, consoleSchema, ({ tab: from, entries }, request) => {
1205
+ const { items, total, truncated } = bounded(entries, Number(request.limit), "last");
1206
+ return {
1207
+ tab: from,
1208
+ entries: items,
1209
+ total,
1210
+ truncated
1211
+ };
1212
+ }),
1213
+ tokens: mapped("tokens", {
1214
+ tab,
1215
+ category: text$1("Token category."),
1216
+ source: text$1("Token source, such as semantic or theme."),
1217
+ search: {
1218
+ schema: {
1219
+ type: "string",
1220
+ description: "Token name search."
1221
+ },
1222
+ read(value) {
1223
+ if (value !== void 0 && typeof value !== "string") throw invalidInput("search", "search must be a string");
1224
+ return value;
1225
+ }
1226
+ },
1227
+ limit: limit("Most tokens to return.", 100, 1e3)
1228
+ }, tokensSchema, (tokens, request) => {
1229
+ const { items, total, truncated } = bounded(tokens, Number(request.limit));
1230
+ return {
1231
+ tokens: items,
1232
+ total,
1233
+ truncated
1234
+ };
1235
+ }),
1236
+ components: operation("components", { tab }, componentsSchema),
1237
+ props: operation("props", {
1238
+ tab,
1239
+ component: text$1("Component id or name.", true),
1240
+ all: boolean("Include passed-through attributes.")
1241
+ }, propsSchema)
1242
+ };
1243
+ const skill = fieldsContract({ path: text$1("Skill file to read.", false, "SKILL.md") });
1244
+ const skillOperation = {
1245
+ inputSchema: skill.inputSchema,
1246
+ fields: ["path"],
1247
+ async run(input, read) {
1248
+ const { path } = skill.request(input);
1249
+ if (typeof path !== "string") throw invalidInput("path", "path must be a nonblank string");
1250
+ return read(path);
1251
+ }
1252
+ };
307
1253
  // src/skills.ts
308
1254
  const root = join(import.meta.dirname, "..", "skills");
309
1255
  /** Reads the Parity design skill: SKILL.md, or a file it references. */
310
1256
  async function readSkill(path = "SKILL.md") {
311
1257
  const target = normalize(join(root, path));
312
- if (relative(root, target).startsWith("..") || relative(root, target).includes(`..${sep}`)) throw new Error(`${path} isn't part of the skill`);
1258
+ if (relative(root, target).startsWith("..") || relative(root, target).includes(`..${sep}`)) throw invalidInput("path", `${path} isn't part of the skill`);
313
1259
  try {
314
1260
  return await readFile(target, "utf8");
315
1261
  } catch {
316
1262
  const files = await list();
317
- throw new Error(`No skill file ${path}. Files: ${files.join(", ")}`);
1263
+ throw new CodedError(`No skill file ${path}. Files: ${files.join(", ")}`, {
1264
+ code: "invalid_input",
1265
+ field: "path",
1266
+ alternatives: closest(path, files)
1267
+ });
318
1268
  }
319
1269
  }
320
1270
  async function list(directory = root) {
@@ -322,172 +1272,81 @@ async function list(directory = root) {
322
1272
  return (await Promise.all(entries.map(async (entry) => entry.isDirectory() ? list(join(directory, entry.name)) : [relative(root, join(directory, entry.name)).replaceAll("\\", "/")]))).flat();
323
1273
  }
324
1274
  // src/mcp.ts
325
- const tab = {
326
- type: "string",
327
- description: "Tab id, project root or name. Defaults to the active tab."
1275
+ /**
1276
+ * One adapter for RPC-backed tools: operation contracts own defaults, validation, failure semantics and the structured
1277
+ * result, which is what CLI --json prints. The text beside it is for people and clients that don't read structure.
1278
+ *
1279
+ * Tools publish no outputSchema (PAR-510). Claude Code never shows one to the model, Codex's code mode puts it in the
1280
+ * model's context, and with them tools/list was 33 KB rather than 11. structuredContent needs no schema; the contracts
1281
+ * keep theirs for CLI --json and the tests.
1282
+ */
1283
+ function operationTool(name, description, operation, format) {
1284
+ return {
1285
+ name,
1286
+ description,
1287
+ inputSchema: operation.inputSchema,
1288
+ fields: operation.fields,
1289
+ async run(input) {
1290
+ const result = await operation.run(input, call);
1291
+ return {
1292
+ content: format(result),
1293
+ structuredContent: operation.structured(result),
1294
+ isError: operation.failed(result)
1295
+ };
1296
+ }
1297
+ };
1298
+ }
1299
+ /** Results that are data to begin with, as compact JSON text for clients that don't read structuredContent. */
1300
+ const json = (value) => [text(JSON.stringify(value))];
1301
+ /** MCP's names for execute's label and code. */
1302
+ const executeNames = {
1303
+ label: "description",
1304
+ code: "input"
328
1305
  };
329
1306
  const tools = [
330
- {
331
- name: "get_tabs",
332
- description: "List the projects open in Parity, one per tab. The active tab is the one the person is looking at.",
333
- inputSchema: {
334
- type: "object",
335
- properties: {}
336
- },
337
- run: async () => [text(JSON.stringify(await call("tabs"), null, 2))]
338
- },
339
- {
340
- name: "get_app_state",
341
- description: "Get a tab's state: its designs (ids, states, modes shown, where their boards are and how big), boards that overlap or crowd each other's titles, the selection, project modes, components and tokens, and what Parity detected in the project (each tool's version, where it's installed and its config, the package manager, and a note naming versions Parity doesn't test). Call it first, and again after the person may have changed things. Keep boards apart: Overlaps() and FindEmptySpace() in execute help place them.",
342
- inputSchema: {
343
- type: "object",
344
- properties: { tab }
345
- },
346
- run: async (input) => [text(JSON.stringify(await call("state", { tab: input.tab }), null, 2))]
347
- },
1307
+ operationTool("get_tabs", "List the projects open in Parity, one per tab. The active tab is the one the person is looking at.", operations.tabs, json),
1308
+ operationTool("get_app_state", "Get a tab's state: its designs (ids, states, modes shown, where their boards are and how big), boards that overlap or crowd each other's titles, the selection, project modes, components and tokens, and what Parity detected in the project (each tool's version, where it's installed and its config, the package manager, and a note naming versions Parity doesn't test). Call it first, and again after the person may have changed things. Keep boards apart: Overlaps() and FindEmptySpace() in execute help place them.", operations.state, json),
1309
+ operationTool("get_status", "Read where a tab's edits stand, changing nothing: its revision (the same only while nothing has changed, so compare it before reading again), whether anything applied isn't saved (dirty), the files waiting for Save, token values waiting, how the last save went and writes that failed, and how many steps undo and redo hold. summary adds each unsaved file's designs added, changed and removed, the token values, and undo and redo labels, up to limit each. Changes are undoable once applied; only the person saves them, unless automatic saving is on.", operations.status, (result) => [text(describeStatus(result))]),
1310
+ operationTool("get_modes", "Read the project's modes and what each design shows. viewports are the ids Responsive takes; modes are the ids screenshot, find_problems and check_accessibility take, each one option per variable (viewport, theme, locale), with its width, window height and the classes and attributes it sets. designs gives each board's state, main mode and the modes beside it; name a design to get just it, with each frame's size and window. Reading changes nothing.", operations.modes, (result) => [text(describeModes(result))]),
348
1311
  {
349
1312
  name: "execute",
350
- description: "Run a JavaScript snippet against a tab's designs with the Parity API (read_skill \"execute.md\"). All changes land as one undoable step, or none if it fails. On failure, retry with editId and edits instead of resending.",
351
- inputSchema: {
352
- type: "object",
353
- properties: {
354
- description: {
355
- type: "string",
356
- description: "Shown to the person while it runs. 3 to 8 words, imperative, e.g. \"Add pricing tiers\"."
357
- },
358
- input: {
359
- type: "string",
360
- description: "The snippet. Omit when retrying with editId and edits."
361
- },
362
- editId: {
363
- type: "string",
364
- description: "The failed call's editId, to patch and rerun its snippet."
365
- },
366
- edits: {
367
- type: "array",
368
- description: "Exact find and replace edits to the failed snippet, applied in order.",
369
- items: {
370
- type: "object",
371
- properties: {
372
- find: { type: "string" },
373
- replace: { type: "string" },
374
- all: { type: "boolean" }
375
- },
376
- required: ["find", "replace"]
377
- }
378
- },
379
- tab
380
- },
381
- required: ["description"]
382
- },
383
- run: async (input) => {
384
- const result = await call("exec", {
1313
+ description: "Run a JavaScript snippet with the Parity API (read_skill \"execute.md\"). All changes land as one undoable step, or none on failure. Retry failures with editId and edits.",
1314
+ inputSchema: executeOperation.inputSchema(executeNames),
1315
+ fields: executeOperation.fields,
1316
+ names: executeNames,
1317
+ async run(input) {
1318
+ const result = await executeOperation.run({
385
1319
  tab: input.tab,
386
1320
  label: input.description,
387
1321
  code: input.input,
388
1322
  editId: input.editId,
389
1323
  edits: input.edits
390
- });
391
- return [text(describeExec(result)), ...result.screenshots.map((shot) => image(shot.png))];
392
- }
393
- },
394
- {
395
- name: "screenshot",
396
- description: "Capture a design in any of its states and modes, rendered offscreen at real widths. state and mode take an id, a list, or \"all\"; mode also takes \"shown\".",
397
- inputSchema: {
398
- type: "object",
399
- properties: {
400
- design: {
401
- type: "string",
402
- description: "Design id or name."
403
- },
404
- state: { description: "State id, list of ids, or \"all\". Defaults to the state the design shows." },
405
- mode: { description: "Mode id, list of ids, \"shown\" or \"all\". Defaults to the primary mode." },
406
- scale: {
407
- type: "number",
408
- description: "Pixel density, 0.25 to 3. Defaults to 1."
409
- },
410
- tab
411
- },
412
- required: ["design"]
413
- },
414
- run: async (input) => {
415
- const result = await call("screenshot", input);
416
- return [text(describeExec(result)), ...result.screenshots.map((shot) => image(shot.png))];
1324
+ }, call);
1325
+ const structured = executeOperation.structured(result);
1326
+ return {
1327
+ content: [text(describeExec(result)), ...result.screenshots.map((shot) => image(shot.png))],
1328
+ structuredContent: result.error ? {
1329
+ ...structured,
1330
+ error: renamePath(result.error, executeNames)
1331
+ } : structured,
1332
+ isError: executeOperation.failed(result)
1333
+ };
417
1334
  }
418
1335
  },
419
- {
420
- name: "find_problems",
421
- description: "Find layout problems in a design, rendered offscreen in every state and the modes it shows unless you narrow it: render errors, content wider than the frame, nodes cut off by a parent that clips, and components that aren't the project's. Run it after changes, with screenshot. state and mode take an id, a list, or \"all\"; mode also takes \"shown\".",
422
- inputSchema: {
423
- type: "object",
424
- properties: {
425
- design: {
426
- type: "string",
427
- description: "Design id or name."
428
- },
429
- state: { description: "State id, list of ids, or \"all\". Defaults to \"all\"." },
430
- mode: { description: "Mode id, list of ids, \"shown\" or \"all\". Defaults to \"shown\"." },
431
- tab
432
- },
433
- required: ["design"]
434
- },
435
- run: async (input) => [text(describeProblems(await call("problems", input)))]
436
- },
437
- {
438
- name: "check_accessibility",
439
- description: "Check a design's accessibility with axe-core against WCAG 2.2 AA, rendered offscreen in every state and the modes it shows unless you narrow it. Lists each rule broken, worst first, with the layers that break it (ids you can pass to execute), the frames, what's wrong and a link on how to fix it. state and mode take an id, a list, or \"all\"; mode also takes \"shown\". Automated checks find some issues, not all.",
440
- inputSchema: {
441
- type: "object",
442
- properties: {
443
- design: {
444
- type: "string",
445
- description: "Design id or name."
446
- },
447
- state: { description: "State id, list of ids, or \"all\". Defaults to \"all\"." },
448
- mode: { description: "Mode id, list of ids, \"shown\" or \"all\". Defaults to \"shown\"." },
449
- tab
450
- },
451
- required: ["design"]
452
- },
453
- run: async (input) => [text(describeAccessibility(await call("accessibility", input)))]
454
- },
455
- {
456
- name: "get_console",
457
- description: "Read what the project's own code wrote to the console: errors, warnings and logs from its components on the canvas, including while the person interacts with them, and from offscreen renders such as screenshots. Oldest first; the same message several times in a row is counted once. clear: true empties it after reading, so the next read shows only what's new.",
458
- inputSchema: {
459
- type: "object",
460
- properties: {
461
- clear: {
462
- type: "boolean",
463
- description: "Empty it after reading."
464
- },
465
- tab
466
- }
467
- },
468
- run: async (input) => [text(describeConsole(await call("console", input)))]
469
- },
470
- {
471
- name: "open_project",
472
- description: "Open a project folder in a new Parity tab, or switch to it if it's already open.",
473
- inputSchema: {
474
- type: "object",
475
- properties: { root: {
476
- type: "string",
477
- description: "Absolute path."
478
- } },
479
- required: ["root"]
480
- },
481
- run: async (input) => [text(JSON.stringify(await call("open", { root: input.root }), null, 2))]
482
- },
1336
+ operationTool("screenshot", "Capture a design in any of its states and modes, rendered offscreen at real widths. state and mode take an id, a list, or \"all\"; mode also takes \"shown\".", operations.screenshot, (result) => [text(describeExec(result)), ...result.screenshots.map((shot) => image(shot.png))]),
1337
+ operationTool("find_problems", "Find layout problems in a design, rendered offscreen in every state and the modes it shows unless you narrow it: render errors, content wider than the frame, nodes cut off by a parent that clips, and components that aren't the project's. Run it after changes, with screenshot. state and mode take an id, a list, or \"all\"; mode also takes \"shown\".", operations.problems, (result) => [text(describeProblems(result))]),
1338
+ operationTool("check_accessibility", "Check a design's accessibility with axe-core against WCAG 2.2 AA, rendered offscreen in every state and the modes it shows unless you narrow it. Lists each rule broken, worst first, with the layers that break it (ids you can pass to execute), the frames, what's wrong and a link on how to fix it. state and mode take an id, a list, or \"all\"; mode also takes \"shown\". Automated checks find some issues, not all.", operations.accessibility, (result) => [text(describeAccessibility(result))]),
1339
+ operationTool("get_console", "Read what the project's own code wrote to the console: errors, warnings and logs from its components on the canvas, including while the person interacts with them, and from offscreen renders such as screenshots. Oldest first; the same message several times in a row is counted once. clear: true empties it after reading, so the next read shows only what's new.", operations.console, (result) => [text(describeConsole(result))]),
1340
+ operationTool("open_project", "Open a project folder in a new Parity tab, or switch to it if it's already open.", operations.open, json),
1341
+ operationTool("get_tokens", "Read tokens filtered by category, source or name, with values in every theme.", operations.tokens, (result) => [text(describeTokens(result))]),
1342
+ operationTool("get_components", "List project components, libraries, icon sets and exports that were left out or failed to render.", operations.components, (result) => [text(describeComponents(result))]),
1343
+ operationTool("get_props", "Read a component's props from source. all includes attributes it passes through.", operations.props, (result) => [text(describeProps(result))]),
483
1344
  {
484
1345
  name: "read_skill",
485
- description: "Read Parity's design skill: SKILL.md, or a file it references such as execute.md or guide/web-app.md.",
486
- inputSchema: {
487
- type: "object",
488
- properties: { path: { type: "string" } }
489
- },
490
- run: async (input) => [text(await readSkill(input.path))]
1346
+ description: "Read the Parity design skill or a file it references.",
1347
+ inputSchema: skillOperation.inputSchema,
1348
+ fields: skillOperation.fields,
1349
+ run: async (input) => ({ content: [text(await skillOperation.run(input, readSkill))] })
491
1350
  }
492
1351
  ];
493
1352
  const instructions = "Parity renders a project's real components on a canvas. Designs are trees of the project's own components, layouts and text, styled only with its tokens, with states (like stories) and modes (viewport, theme, locale). Read the skill (read_skill) before designing, start with get_app_state, change designs with execute, check every state and mode with screenshot and find_problems, check accessibility with check_accessibility, and read what the components log with get_console.";
@@ -548,10 +1407,12 @@ async function respond(message) {
548
1407
  message: `Unknown tool ${String(message.params?.name)}`
549
1408
  } };
550
1409
  try {
551
- return { result: { content: await tool.run(message.params?.arguments ?? {}) } };
1410
+ return { result: await tool.run(message.params?.arguments ?? {}) };
552
1411
  } catch (error) {
1412
+ const failure = renamePath(operationError(error, tool.fields), tool.names ?? {});
553
1413
  return { result: {
554
- content: [text(error instanceof Error ? error.message : String(error))],
1414
+ content: [text(failure.message)],
1415
+ structuredContent: { error: failure },
555
1416
  isError: true
556
1417
  } };
557
1418
  }
@@ -580,6 +1441,12 @@ const usage = `parity: drive the Parity app from a terminal or an agent
580
1441
 
581
1442
  parity tabs list open project tabs
582
1443
  parity state [--tab t] designs, selection, modes, components and tokens of a tab
1444
+ parity status [--summary] [--limit 10] [--tab t]
1445
+ the tab's revision, whether anything applied isn't saved, the files and
1446
+ token values waiting for Save, the last save and failed writes, and undo
1447
+ and redo; --summary names the designs, tokens and steps
1448
+ parity modes [design] [--tab t] viewports (the ids Responsive takes), variables, modes (the ids renders
1449
+ take) and what each design shows; a design adds its frames' sizes
583
1450
  parity exec [file] --label "Do x" run a snippet (from a file, or stdin) as one undoable step
584
1451
  [--edit-id id --edits '[{"find":"a","replace":"b"}]'] retry a failed snippet with edits
585
1452
  parity screenshot <design> [--state s|all] [--mode m|shown|all] [--scale 2] [--tab t]
@@ -589,10 +1456,13 @@ const usage = `parity: drive the Parity app from a terminal or an agent
589
1456
  parity problems <design> [--state s|all] [--mode m|shown|all] [--tab t]
590
1457
  render errors, overflow, clipping and unknown components, in every state
591
1458
  and the modes shown unless narrowed; exits with 1 when it finds any
592
- parity console [--clear] [--tab t] what the project's code wrote to the console, on the canvas and in
593
- offscreen renders; --clear empties it after reading
594
- parity tokens [--category c] [--source semantic|theme] [--search s] [--tab t]
595
- the tokens styles can name, with their values per theme
1459
+ parity console [--clear] [--limit 50] [--tab t]
1460
+ what the project's code wrote to the console, on the canvas and in
1461
+ offscreen renders, the newest 50 unless --limit says; --clear empties it
1462
+ after reading
1463
+ parity tokens [--category c] [--source semantic|theme] [--search s] [--limit 100] [--tab t]
1464
+ the tokens styles can name, with their values per theme, the first 100
1465
+ unless --limit says
596
1466
  parity components [--tab t] the components designs can use, by library
597
1467
  parity props <Component> [--all] [--tab t]
598
1468
  a component's props from its source; --all adds the attributes it passes on
@@ -628,7 +1498,11 @@ const usage = `parity: drive the Parity app from a terminal or an agent
628
1498
  parity skill [file] print the design skill, or one of its files
629
1499
  parity mcp serve these as MCP tools on stdio
630
1500
 
631
- Options take a tab id, project root or name with --tab; the active tab is the default.`;
1501
+ Options take a tab id, project root or name with --tab, or "active" for what the person is looking at; without it, a
1502
+ command acts on the open project the current folder is in, else on "active". Each call goes to the running copy of
1503
+ Parity that has its project open. --json prints any command's
1504
+ result as the data MCP returns as structuredContent, and a failure as {"error": {code, message, retryable, path,
1505
+ alternatives, editId}}.`;
632
1506
  const { positionals, values } = parseArgs({
633
1507
  allowPositionals: true,
634
1508
  options: {
@@ -648,10 +1522,12 @@ const { positionals, values } = parseArgs({
648
1522
  json: { type: "boolean" },
649
1523
  record: { type: "boolean" },
650
1524
  clear: { type: "boolean" },
1525
+ summary: { type: "boolean" },
651
1526
  all: { type: "boolean" },
652
1527
  category: { type: "string" },
653
1528
  source: { type: "string" },
654
1529
  search: { type: "string" },
1530
+ limit: { type: "string" },
655
1531
  root: { type: "string" },
656
1532
  out: { type: "string" },
657
1533
  registry: { type: "boolean" },
@@ -669,10 +1545,14 @@ const { positionals, values } = parseArgs({
669
1545
  }
670
1546
  });
671
1547
  const [command, ...rest] = positionals;
1548
+ /** The running command's input fields, which a failure's path can point at. */
1549
+ let fields;
672
1550
  try {
673
1551
  await main();
674
1552
  } catch (error) {
675
- console.error(error instanceof Error ? error.message : String(error));
1553
+ const failure = operationError(error, fields);
1554
+ if (values.json) print({ error: failure });
1555
+ console.error(failure.message);
676
1556
  process.exitCode = 1;
677
1557
  }
678
1558
  async function main() {
@@ -680,14 +1560,23 @@ async function main() {
680
1560
  console.log(usage);
681
1561
  return;
682
1562
  }
1563
+ const limit = values.limit === void 0 ? void 0 : Number(values.limit);
683
1564
  switch (command) {
684
- case "tabs": return print(await call("tabs"));
685
- case "state": return print(await call("state", { tab: values.tab }));
686
- case "open":
687
- if (!rest[0]) throw new Error("parity open <folder>");
688
- return print(await call("open", { root: resolve(rest[0]) }));
1565
+ case "tabs": return show(operations.tabs, {});
1566
+ case "state": return show(operations.state, { tab: values.tab });
1567
+ case "status": return show(operations.status, {
1568
+ tab: values.tab,
1569
+ summary: values.summary,
1570
+ limit
1571
+ }, describeStatus);
1572
+ case "modes": return show(operations.modes, {
1573
+ tab: values.tab,
1574
+ design: rest[0]
1575
+ }, describeModes);
1576
+ case "open": return show(operations.open, { root: rest[0]?.trim() ? resolve(rest[0]) : rest[0] });
689
1577
  case "skill":
690
- console.log(await readSkill(rest[0]));
1578
+ fields = skillOperation.fields;
1579
+ console.log(await skillOperation.run({ path: rest[0] }, readSkill));
691
1580
  return;
692
1581
  case "mcp":
693
1582
  serveMcp();
@@ -714,111 +1603,82 @@ async function main() {
714
1603
  ...values.json ? { json: true } : {}
715
1604
  });
716
1605
  case "exec": {
1606
+ fields = executeOperation.fields;
717
1607
  const editId = values["edit-id"];
718
1608
  let code;
719
- if (!editId) code = rest[0] ? await readFile(rest[0], "utf8") : await stdin();
720
- const edits = values.edits ? JSON.parse(values.edits) : void 0;
721
- report(await call("exec", {
1609
+ if (rest[0]) code = await readFile(rest[0], "utf8");
1610
+ else if (!editId) code = await stdin() || void 0;
1611
+ let edits;
1612
+ try {
1613
+ edits = values.edits === void 0 ? void 0 : JSON.parse(values.edits);
1614
+ } catch {
1615
+ throw invalidInput("edits", "edits must be JSON: a list of {find, replace}");
1616
+ }
1617
+ const result = await executeOperation.run({
722
1618
  tab: values.tab,
723
- label: values.label ?? "Edit from the CLI",
1619
+ label: values.label,
724
1620
  code,
725
1621
  editId,
726
1622
  edits
727
- }));
1623
+ }, call);
1624
+ if (values.json) print(executeOperation.structured(result));
1625
+ else console.log(describeExec(result));
1626
+ if (executeOperation.failed(result)) process.exitCode = 1;
728
1627
  return;
729
1628
  }
730
- case "screenshot":
731
- if (!rest[0]) throw new Error("parity screenshot <design> [--state s] [--mode m]");
732
- report(await call("screenshot", {
733
- tab: values.tab,
734
- design: rest[0],
735
- state: one(values.state),
736
- mode: one(values.mode),
737
- scale: values.scale ? Number(values.scale) : void 0
738
- }));
739
- return;
1629
+ case "screenshot": return show(operations.screenshot, {
1630
+ tab: values.tab,
1631
+ design: rest[0],
1632
+ state: values.state,
1633
+ mode: values.mode,
1634
+ scale: values.scale === void 0 ? void 0 : Number(values.scale)
1635
+ }, describeExec);
740
1636
  case "a11y":
741
- case "accessibility": {
742
- if (!rest[0]) throw new Error("parity a11y <design> [--state s] [--mode m]");
743
- const result = await call("accessibility", {
744
- tab: values.tab,
745
- design: rest[0],
746
- state: one(values.state),
747
- mode: one(values.mode)
748
- });
749
- if (values.json) print(result);
750
- else console.log(describeAccessibility(result));
751
- if (result.findings.length > 0) process.exitCode = 1;
752
- return;
753
- }
754
- case "problems": {
755
- if (!rest[0]) throw new Error("parity problems <design> [--state s] [--mode m]");
756
- const result = await call("problems", {
757
- tab: values.tab,
758
- design: rest[0],
759
- state: one(values.state),
760
- mode: one(values.mode)
761
- });
762
- if (values.json) print(result);
763
- else console.log(describeProblems(result));
764
- if (result.problems.length > 0) process.exitCode = 1;
765
- return;
766
- }
767
- case "tokens": {
768
- const filter = {
769
- category: values.category,
770
- source: values.source,
771
- search: values.search
772
- };
773
- const tokens = await call("tokens", {
774
- tab: values.tab,
775
- ...filter
776
- });
777
- if (values.json) print(tokens);
778
- else console.log(describeTokens(tokens));
779
- return;
780
- }
781
- case "components": {
782
- const result = await call("components", { tab: values.tab });
783
- if (values.json) print(result);
784
- else console.log(describeComponents(result));
785
- return;
786
- }
787
- case "props": {
788
- if (!rest[0]) throw new Error("parity props <Component> [--all]");
789
- const result = await call("props", {
790
- tab: values.tab,
791
- component: rest[0],
792
- all: values.all
793
- });
794
- if (values.json) print(result);
795
- else console.log(describeProps(result));
796
- return;
797
- }
798
- case "console": {
799
- const result = await call("console", {
800
- tab: values.tab,
801
- clear: values.clear
802
- });
803
- if (values.json) print(result);
804
- else console.log(describeConsole(result));
805
- return;
806
- }
807
- default: throw new Error(`Unknown command ${command}\n\n${usage}`);
1637
+ case "accessibility": return show(operations.accessibility, {
1638
+ tab: values.tab,
1639
+ design: rest[0],
1640
+ state: values.state,
1641
+ mode: values.mode
1642
+ }, describeAccessibility);
1643
+ case "problems": return show(operations.problems, {
1644
+ tab: values.tab,
1645
+ design: rest[0],
1646
+ state: values.state,
1647
+ mode: values.mode
1648
+ }, describeProblems);
1649
+ case "tokens": return show(operations.tokens, {
1650
+ tab: values.tab,
1651
+ category: values.category,
1652
+ source: values.source,
1653
+ search: values.search,
1654
+ limit
1655
+ }, describeTokens);
1656
+ case "components": return show(operations.components, { tab: values.tab }, describeComponents);
1657
+ case "props": return show(operations.props, {
1658
+ tab: values.tab,
1659
+ component: rest[0],
1660
+ all: values.all
1661
+ }, describeProps);
1662
+ case "console": return show(operations.console, {
1663
+ tab: values.tab,
1664
+ clear: values.clear,
1665
+ limit
1666
+ }, describeConsole);
1667
+ default: throw invalidInput(void 0, `Unknown command ${command}
1668
+
1669
+ ${usage}`);
808
1670
  }
809
1671
  }
810
- function report(result) {
811
- if (values.json) print({
812
- ...result,
813
- screenshots: result.screenshots.map(({ png: _png, ...shot }) => shot)
814
- });
815
- else console.log(describeExec(result));
816
- if (!result.ok) process.exitCode = 1;
817
- }
818
- /** A repeated option becomes a list; one value stays a string, so "all" and "shown" work. */
819
- function one(list) {
820
- if (!list || list.length === 0) return void 0;
821
- return list.length === 1 ? list[0] : list;
1672
+ /**
1673
+ * Runs a shared operation and prints it: the structured result with --json, the same data MCP returns as
1674
+ * structuredContent, or the account for people. A failure exits with 1 either way.
1675
+ */
1676
+ async function show(operation, input, describe) {
1677
+ fields = operation.fields;
1678
+ const result = await operation.run(input, call);
1679
+ if (values.json || !describe) print(operation.structured(result));
1680
+ else console.log(describe(result));
1681
+ if (operation.failed(result)) process.exitCode = 1;
822
1682
  }
823
1683
  function print(value) {
824
1684
  console.log(JSON.stringify(value, null, 2));