tsquare 0.6.1 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### MCP tools
6
+
7
+ - **Hosted MCP server at `https://tsquare.dev/mcp`** (in the site repo), with three tools: `wireframe_guide` (the language plus a write → render → fix → share workflow), `render_wireframe` (a PNG for the model to look at, at most 1600px on its longest side, or line-numbered problems) and `share_wireframe` (SVG, PNG, playground and Markdown links). Setup for Claude, Claude Code, Cursor and VS Code is in [Using it with AI](docs/ai.md#connect-over-mcp).
8
+ - **The tools live in the library** as plain functions with no MCP SDK, so a local server can reuse them: `mcpTools` (names, descriptions, Zod input schemas, annotations), `MCP_INSTRUCTIONS`, `wireframeGuide()`, `renderWireframeTool()`, `shareWireframeTool()` and `callMcpTool(name, args)`. Results use the MCP tool-result shape; problems come back as `isError` results, not exceptions, so the model can read and fix them. Share links drop a surrounding code fence.
9
+ - `wireframePrompt()` is unchanged; its language part is now shared with the guide.
10
+
11
+ ### Errors
12
+
13
+ - **A value under the wrong prop points at the right one:** `image type=map` now says "for an Image, write kind=map (or just map)"; likewise `input kind=date` → `type=date` and `chart type=pie` → `kind=pie`.
14
+ - Grammar: "a Input" → "an Input" in error messages.
15
+
3
16
  ## 0.6.1
4
17
 
5
18
  - **Fix: the playground opens 0.6.0 share links.** 0.6.0 made share links with the new `y` prefix, but its playground only opened links starting with `z`. Opening a new share link left the editor on whatever it showed before. The prefixes now live in one place that both the library and the playground use, and a test guards it. Links themselves didn't change: every `y` link made by 0.6.0 opens correctly now.
@@ -1892,7 +1892,7 @@ for (const [name, def] of Object.entries(componentDefinitions)) {
1892
1892
  for (const v of ambiguous) enumValues.delete(v);
1893
1893
  COMPONENTS.set(name.toLowerCase(), { name, props: shape, enumValues, ambiguous, booleans: booleans2 });
1894
1894
  }
1895
- function belongsElsewhere(component, word, asProp) {
1895
+ function belongsElsewhere(component, word, asProp, value) {
1896
1896
  const info = COMPONENTS.get(component.toLowerCase());
1897
1897
  if (!info) return "";
1898
1898
  const props = [], options = [];
@@ -1903,7 +1903,9 @@ function belongsElsewhere(component, word, asProp) {
1903
1903
  }
1904
1904
  const names = (xs) => xs.length > 3 ? `${xs.slice(0, 3).join(", ")}\u2026` : xs.join(", ").replace(/, ([^,]*)$/, " and $1");
1905
1905
  const a = (name) => /^[AEIOU]/.test(name) ? `an ${name}` : `a ${name}`;
1906
- let msg = props.length ? `${word} is a ${names(props)} prop, not ${a(info.name)} one` : options.length ? `${word} is a ${names(options)} option, not ${a(info.name)} one` : "";
1906
+ let msg = props.length ? `${word} is ${a(names(props))} prop, not ${a(info.name)} one` : options.length ? `${word} is ${a(names(options))} option, not ${a(info.name)} one` : "";
1907
+ const right = typeof value === "string" ? info.enumValues.get(value) : void 0;
1908
+ if (asProp && right && right !== word) msg += `${msg ? "; " : ""}for ${a(info.name)}, write ${right}=${value} (or just ${value})`;
1907
1909
  const width = info.props.width;
1908
1910
  if (msg && word === "fullWidth" && /fills the space/i.test(width?.description ?? unwrap(width)?.description ?? "")) {
1909
1911
  msg += `; ${a(info.name)} already fills its width (width=\u2026 sets a fixed one), so remove it`;
@@ -2134,11 +2136,12 @@ function parseWireframeText(source) {
2134
2136
  }
2135
2137
  continue;
2136
2138
  }
2139
+ const value = parseValue(c);
2137
2140
  if (!(key in info.props)) {
2138
- const elsewhere = belongsElsewhere(info.name, key, true);
2141
+ const elsewhere = belongsElsewhere(info.name, key, true, value);
2139
2142
  issues.push({ line: lineNo, message: `${info.name} has no prop "${key}"` + (elsewhere ? `: ${elsewhere}` : "") + ` (its props: ${Object.keys(info.props).join(", ")})` });
2140
2143
  }
2141
- props[key] = key in info.props ? textWhereExpected(info.props[key], parseValue(c)) : parseValue(c);
2144
+ props[key] = key in info.props ? textWhereExpected(info.props[key], value) : value;
2142
2145
  continue;
2143
2146
  }
2144
2147
  c.next();
@@ -2726,7 +2729,7 @@ function checkSpec(spec) {
2726
2729
  continue;
2727
2730
  }
2728
2731
  if (!(key in def.props.shape)) {
2729
- const elsewhere = belongsElsewhere(el.type, key, true);
2732
+ const elsewhere = belongsElsewhere(el.type, key, true, el.props[key]);
2730
2733
  issues.push(`${id}.props: ${el.type} has no prop "${key}"${elsewhere ? `: ${elsewhere}` : ""}`);
2731
2734
  }
2732
2735
  }
@@ -3212,6 +3215,11 @@ function wireframePrompt() {
3212
3215
  "You write low-fidelity UI wireframes as specs that a renderer turns into images.",
3213
3216
  "Reply with only the spec in a single code block, with no explanation.",
3214
3217
  "",
3218
+ languageReference()
3219
+ ].join("\n");
3220
+ }
3221
+ function languageReference() {
3222
+ return [
3215
3223
  TEXT_FORMAT,
3216
3224
  "",
3217
3225
  RULES,
@@ -3263,5 +3271,6 @@ export {
3263
3271
  printWireframeText,
3264
3272
  componentDocs,
3265
3273
  wireframePrompt,
3274
+ languageReference,
3266
3275
  repairPrompt
3267
3276
  };
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  printWireframeText,
7
7
  renderWireframe,
8
8
  wireframePrompt
9
- } from "./chunks/chunk-HDSKRKDL.js";
9
+ } from "./chunks/chunk-Z4RBFAOM.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import { readFile, writeFile } from "node:fs/promises";
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { renderWireframe, compileWireframe, formatIssues, WireframeError, type C
3
3
  export { wireframePrompt, repairPrompt } from "./prompt.js";
4
4
  export { printWireframeText as formatWireframe } from "./print.js";
5
5
  export { encodeWireframe, decodeWireframe, MAX_SHARED_TEXT } from "./share.js";
6
+ export { mcpTools, MCP_INSTRUCTIONS, wireframeGuide, renderWireframeTool, shareWireframeTool, callMcpTool, type ToolResult, type ToolContent, type ToolOptions, type McpToolName } from "./mcp.js";
6
7
  export { upgradeWireframe, upgradeSpec, RENAMED_PROPS, type UpgradeOptions } from "./upgrade.js";
7
8
  export { renderWireframeSvg, renderWireframePng, checkSpec, boardSize, boardLayout, type BoardItem, type RenderWireframeOptions } from "./render.js";
8
9
  export { parseWireframeText, type TextIssue, type ParseResult } from "./text.js";
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  compileWireframe,
10
10
  componentDefinitions,
11
11
  formatIssues,
12
+ languageReference,
12
13
  parseWireframeText,
13
14
  printWireframeText,
14
15
  registry,
@@ -20,7 +21,7 @@ import {
20
21
  upgradeSpec,
21
22
  upgradeWireframe,
22
23
  wireframePrompt
23
- } from "./chunks/chunk-HDSKRKDL.js";
24
+ } from "./chunks/chunk-Z4RBFAOM.js";
24
25
 
25
26
  // src/share.ts
26
27
  import { deflateRawSync, inflateRawSync } from "node:zlib";
@@ -32,22 +33,126 @@ var isLinkData = (data) => data.startsWith(LINK_PREFIX) || data.startsWith(LEGAC
32
33
 
33
34
  // src/share.ts
34
35
  var MAX_SHARED_TEXT = 64e3;
35
- function encodeWireframe(text) {
36
- return LINK_PREFIX + deflateRawSync(Buffer.from(text, "utf8"), { level: 9 }).toString("base64url");
36
+ function encodeWireframe(text2) {
37
+ return LINK_PREFIX + deflateRawSync(Buffer.from(text2, "utf8"), { level: 9 }).toString("base64url");
37
38
  }
38
39
  function decodeWireframe(data) {
39
40
  const prefix = data.slice(0, 1);
40
41
  if (!isLinkData(data)) throw new Error(`unknown encoding "${prefix}" (expected a "${LINK_PREFIX}" or "${LEGACY_LINK_PREFIX}" prefix)`);
41
- const text = inflateRawSync(Buffer.from(data.slice(1), "base64url"), { maxOutputLength: MAX_SHARED_TEXT }).toString("utf8");
42
- return upgradeWireframe(text, { fromLink: prefix === LEGACY_LINK_PREFIX });
42
+ const text2 = inflateRawSync(Buffer.from(data.slice(1), "base64url"), { maxOutputLength: MAX_SHARED_TEXT }).toString("utf8");
43
+ return upgradeWireframe(text2, { fromLink: prefix === LEGACY_LINK_PREFIX });
44
+ }
45
+
46
+ // src/mcp.ts
47
+ import { z } from "zod";
48
+ var MODEL_IMAGE_MAX = 1600;
49
+ var MAX_IMAGE_LINK_DATA = 16e3;
50
+ var DEFAULT_BASE = "https://tsquare.dev";
51
+ var MCP_INSTRUCTIONS = "tsquare draws low-fidelity UI wireframes from a small text language: multi-screen boards with notes and flow arrows, grayscale by default. Call wireframe_guide once before writing your first wireframe, then render_wireframe to check and see it, and share_wireframe to give the user links.";
52
+ var textInput = z.string().max(MAX_SHARED_TEXT).describe("The wireframe in tsquare text (a ```tsquare code fence around it is fine).");
53
+ var flowsInput = z.boolean().optional().describe("Draw flow arrows (default true). false leaves them out, as if there were no flow lines.");
54
+ var GUIDE_HEADER = `# Writing tsquare wireframes
55
+
56
+ Workflow:
57
+ 1. Write the wireframe text for the user's request, following the language below.
58
+ 2. Call render_wireframe with it. If it reports problems, fix every line it names and render again.
59
+ 3. Look at the image for what the checker can't see: content cut off at the bottom of a screen (make the screen taller with height=\u2026 or remove content), cramped rows, text that wraps badly. Fix and render again.
60
+ 4. Call share_wireframe and give the user the links: the image link to look at or embed, and the playground link to edit. Show the wireframe text too, in a \`\`\`tsquare code block, so they can keep it.
61
+ `;
62
+ var mcpTools = {
63
+ wireframe_guide: {
64
+ title: "tsquare language guide",
65
+ description: "How to write a tsquare wireframe: the text format, the rules, a worked example and every component with its props. Call it once before writing your first wireframe.",
66
+ inputSchema: z.object({}),
67
+ annotations: { readOnlyHint: true, openWorldHint: false }
68
+ },
69
+ render_wireframe: {
70
+ title: "Render a wireframe",
71
+ description: "Check tsquare wireframe text and render it. Returns a PNG of the board so you can see it, or the problems by line number (with the valid options) if the text isn't valid. Fix every problem and call it again.",
72
+ inputSchema: z.object({ text: textInput, flows: flowsInput }),
73
+ annotations: { readOnlyHint: true, openWorldHint: false }
74
+ },
75
+ share_wireframe: {
76
+ title: "Share a wireframe",
77
+ description: "Links for valid tsquare wireframe text: an image link (SVG or PNG) to show or embed in Markdown, Notion or docs, and a playground link to edit it. The links contain the wireframe text itself (compressed), so anyone with a link can read it.",
78
+ inputSchema: z.object({ text: textInput, flows: flowsInput }),
79
+ annotations: { readOnlyHint: true, openWorldHint: false }
80
+ }
81
+ };
82
+ var text = (t) => ({ type: "text", text: t });
83
+ var problems = (issues) => ({
84
+ content: [text(`The wireframe has ${issues.length} problem${issues.length === 1 ? "" : "s"}. Fix each line and try again.
85
+ ${formatIssues(issues)}`)],
86
+ isError: true
87
+ });
88
+ function wireframeGuide() {
89
+ return { content: [text(`${GUIDE_HEADER}
90
+ ${languageReference()}`)] };
91
+ }
92
+ async function renderWireframeTool(args) {
93
+ const { spec, issues } = compileWireframe(args.text);
94
+ if (!spec || issues.length) return problems(issues);
95
+ const flows = args.flows !== false;
96
+ const { width, height } = boardSize(spec, { flows });
97
+ const scale = Math.min(1, MODEL_IMAGE_MAX / Math.max(width, height));
98
+ const png = await renderWireframePng(spec, { skipValidation: true, flows, scale });
99
+ const shown = scale < 1 ? `, shown at ${Math.round(width * scale)}\xD7${Math.round(height * scale)}` : "";
100
+ return {
101
+ content: [
102
+ text(`Valid. The board is ${width}\xD7${height}${shown}. Check the image for clipped or cramped content, then call share_wireframe for links.`),
103
+ { type: "image", data: Buffer.from(png).toString("base64"), mimeType: "image/png" }
104
+ ]
105
+ };
106
+ }
107
+ function shareWireframeTool(args, opts = {}) {
108
+ const { spec, issues } = compileWireframe(args.text);
109
+ if (!spec || issues.length) return problems(issues);
110
+ const base = (opts.base ?? DEFAULT_BASE).replace(/\/+$/, "");
111
+ const bare = args.text.replace(/^[ \t]*```.*(\r?\n|$)/gm, "").trim() + "\n";
112
+ if (Buffer.byteLength(bare, "utf8") > MAX_SHARED_TEXT)
113
+ return { content: [text(`This wireframe is too long to share as a link (over ${MAX_SHARED_TEXT / 1e3} KB of text). Split it into smaller boards.`)], isError: true };
114
+ const data = encodeWireframe(bare);
115
+ const query = args.flows === false ? "?flows=0" : "";
116
+ const title = String(spec.elements[spec.root]?.props?.title ?? "Wireframe").replace(/[[\]\n]/g, " ");
117
+ const playground = `${base}/playground#${data}`;
118
+ if (data.length > MAX_IMAGE_LINK_DATA)
119
+ return { content: [text(`Playground (to view and edit): ${playground}
120
+
121
+ This wireframe is too long for an image link; the playground link still works. To get image links, split it into smaller boards.`)] };
122
+ const svg = `${base}/svg/${data}${query}`;
123
+ const png = `${base}/png/${data}${query}`;
124
+ return {
125
+ content: [
126
+ text(
127
+ [
128
+ `Image (SVG): ${svg}`,
129
+ `Image (PNG): ${png}`,
130
+ `Playground (to view and edit): ${playground}`,
131
+ `Markdown: ![${title}](${svg})`,
132
+ "",
133
+ "The links contain the wireframe text, so anyone with a link can read it."
134
+ ].join("\n")
135
+ )
136
+ ]
137
+ };
138
+ }
139
+ async function callMcpTool(name, args = {}, opts = {}) {
140
+ if (name === "wireframe_guide") return wireframeGuide();
141
+ if (name !== "render_wireframe" && name !== "share_wireframe")
142
+ return { content: [text(`Unknown tool "${name}". Tools: ${Object.keys(mcpTools).join(", ")}.`)], isError: true };
143
+ const parsed = mcpTools[name].inputSchema.safeParse(args);
144
+ if (!parsed.success) return { content: [text(`Invalid arguments: ${z.prettifyError(parsed.error)}`)], isError: true };
145
+ return name === "render_wireframe" ? renderWireframeTool(parsed.data) : shareWireframeTool(parsed.data, opts);
43
146
  }
44
147
  export {
45
148
  DEVICES,
46
149
  MAX_SHARED_TEXT,
150
+ MCP_INSTRUCTIONS,
47
151
  RENAMED_PROPS,
48
152
  WireframeError,
49
153
  boardLayout,
50
154
  boardSize,
155
+ callMcpTool,
51
156
  catalog,
52
157
  checkSpec,
53
158
  compileWireframe,
@@ -56,14 +161,18 @@ export {
56
161
  encodeWireframe,
57
162
  formatIssues,
58
163
  printWireframeText as formatWireframe,
164
+ mcpTools,
59
165
  parseWireframeText,
60
166
  registry,
61
167
  renderWireframe,
62
168
  renderWireframePng,
63
169
  renderWireframeSvg,
170
+ renderWireframeTool,
64
171
  repairPrompt,
172
+ shareWireframeTool,
65
173
  theme,
66
174
  upgradeSpec,
67
175
  upgradeWireframe,
176
+ wireframeGuide,
68
177
  wireframePrompt
69
178
  };
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The MCP tools (guide, render, share) as plain functions, with no MCP SDK: tsquare.dev wraps them
3
+ * in its hosted endpoint (tsquare.dev/mcp), and a local stdio server can reuse them unchanged.
4
+ *
5
+ * Results use the MCP tool-result shape ({ content, isError }), so a server returns them as they are.
6
+ * Problems come back as a result with isError, not a thrown error, so the model reads them and fixes
7
+ * its text: the messages are the same line-numbered ones `tsquare check` prints.
8
+ */
9
+ import { z } from "zod";
10
+ export type ToolContent = {
11
+ type: "text";
12
+ text: string;
13
+ } | {
14
+ type: "image";
15
+ data: string;
16
+ mimeType: string;
17
+ };
18
+ export interface ToolResult {
19
+ content: ToolContent[];
20
+ isError?: boolean;
21
+ [key: string]: unknown;
22
+ }
23
+ export interface ToolOptions {
24
+ /** Where share and image links point (default https://tsquare.dev). */
25
+ base?: string;
26
+ }
27
+ /** Longest side of the PNG a model gets back. Models downscale anything bigger, and it keeps the result small. */
28
+ export declare const MODEL_IMAGE_MAX = 1600;
29
+ /** Longest image link the site serves (its render URLs cap the data at this many characters). */
30
+ export declare const MAX_IMAGE_LINK_DATA = 16000;
31
+ /** Short server instructions: what the tools are for and the order to use them in. */
32
+ export declare const MCP_INSTRUCTIONS: string;
33
+ export declare const mcpTools: {
34
+ readonly wireframe_guide: {
35
+ readonly title: "tsquare language guide";
36
+ readonly description: "How to write a tsquare wireframe: the text format, the rules, a worked example and every component with its props. Call it once before writing your first wireframe.";
37
+ readonly inputSchema: z.ZodObject<{}, z.core.$strip>;
38
+ readonly annotations: {
39
+ readonly readOnlyHint: true;
40
+ readonly openWorldHint: false;
41
+ };
42
+ };
43
+ readonly render_wireframe: {
44
+ readonly title: "Render a wireframe";
45
+ readonly description: "Check tsquare wireframe text and render it. Returns a PNG of the board so you can see it, or the problems by line number (with the valid options) if the text isn't valid. Fix every problem and call it again.";
46
+ readonly inputSchema: z.ZodObject<{
47
+ text: z.ZodString;
48
+ flows: z.ZodOptional<z.ZodBoolean>;
49
+ }, z.core.$strip>;
50
+ readonly annotations: {
51
+ readonly readOnlyHint: true;
52
+ readonly openWorldHint: false;
53
+ };
54
+ };
55
+ readonly share_wireframe: {
56
+ readonly title: "Share a wireframe";
57
+ readonly description: "Links for valid tsquare wireframe text: an image link (SVG or PNG) to show or embed in Markdown, Notion or docs, and a playground link to edit it. The links contain the wireframe text itself (compressed), so anyone with a link can read it.";
58
+ readonly inputSchema: z.ZodObject<{
59
+ text: z.ZodString;
60
+ flows: z.ZodOptional<z.ZodBoolean>;
61
+ }, z.core.$strip>;
62
+ readonly annotations: {
63
+ readonly readOnlyHint: true;
64
+ readonly openWorldHint: false;
65
+ };
66
+ };
67
+ };
68
+ export type McpToolName = keyof typeof mcpTools;
69
+ /** The wireframe_guide tool: workflow plus the language reference. */
70
+ export declare function wireframeGuide(): ToolResult;
71
+ /** The render_wireframe tool: a PNG sized for a model to look at, or line-numbered problems. */
72
+ export declare function renderWireframeTool(args: {
73
+ text: string;
74
+ flows?: boolean;
75
+ }): Promise<ToolResult>;
76
+ /** The share_wireframe tool: image and playground links, computed locally (nothing is sent anywhere). */
77
+ export declare function shareWireframeTool(args: {
78
+ text: string;
79
+ flows?: boolean;
80
+ }, opts?: ToolOptions): ToolResult;
81
+ /** Run a tool by name, for servers that dispatch by name. Unknown names come back as an error result. */
82
+ export declare function callMcpTool(name: string, args?: Record<string, unknown>, opts?: ToolOptions): Promise<ToolResult>;
@@ -13,7 +13,7 @@ import {
13
13
  renderWireframeSvg,
14
14
  takesUniversalProps,
15
15
  wireframePrompt
16
- } from "../chunks/chunk-HDSKRKDL.js";
16
+ } from "../chunks/chunk-Z4RBFAOM.js";
17
17
 
18
18
  // src/playground/index.ts
19
19
  import { createServer } from "node:http";
package/dist/prompt.d.ts CHANGED
@@ -18,5 +18,7 @@ export declare const TEXT_FORMAT = "## Output format: wireframe text\nOne elemen
18
18
  export declare const EXAMPLE_REQUEST = "Two phone screens for a recipe app: a browse screen with search, category tabs, a grid of recipe cards and a tab bar; and a recipe detail screen with its options sheet open.";
19
19
  /** The system prompt for writing wireframe text. */
20
20
  export declare function wireframePrompt(): string;
21
+ /** The language itself: format, rules, a worked example and every component. Shared by the prompt and the MCP guide. */
22
+ export declare function languageReference(): string;
21
23
  /** Follow-up message asking a model to fix the problems in its last reply. */
22
24
  export declare function repairPrompt(issues: string): string;
package/dist/text.d.ts CHANGED
@@ -26,7 +26,7 @@ export declare const PRIMARY_PROP: Record<string, string>;
26
26
  * For an error message: where a prop name or bare word that this component doesn't take
27
27
  * belongs instead, e.g. fullWidth on an input → "fullWidth is a Button prop". Empty if nowhere.
28
28
  */
29
- export declare function belongsElsewhere(component: string, word: string, asProp: boolean): string;
29
+ export declare function belongsElsewhere(component: string, word: string, asProp: boolean, value?: unknown): string;
30
30
  /** What a component accepts as bare words, for docs: option values (with their prop), ambiguous values, and boolean props. */
31
31
  export declare function bareWords(component: string): {
32
32
  options: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsquare",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Wireframes in plain text: easy for LLMs to write, fast to render as SVG or PNG.",
5
5
  "keywords": [
6
6
  "wireframe",