formhaus 0.14.2 → 0.19.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/README.md +34 -4
- package/dist/cli.js +260 -96
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -6,7 +6,35 @@ Adds the [Formhaus](https://formhaus.dev) MCP server and agent instructions to t
|
|
|
6
6
|
npx formhaus
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Run it inside a project to also add the Cursor rule
|
|
9
|
+
Run it anywhere inside a project to also add the Cursor rule at the project root: the nearest folder above with `.git`, `package.json` or `.cursor`, below your home directory. Requires Node 22.12 or later.
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Formhaus is ready in Claude Code, Codex, Cursor and Claude Desktop.
|
|
13
|
+
|
|
14
|
+
✓ Claude Code added MCP server and form skill
|
|
15
|
+
✓ Codex added MCP server
|
|
16
|
+
✓ Cursor added MCP server · run in a project to add the Cursor rule
|
|
17
|
+
✓ Claude Desktop added MCP server
|
|
18
|
+
– OpenCode add it by hand: https://formhaus.dev/guide/mcp
|
|
19
|
+
|
|
20
|
+
Next
|
|
21
|
+
1. Restart your agents.
|
|
22
|
+
2. Ask one: "Build a contact form with Formhaus: name, email, message."
|
|
23
|
+
3. Show it on your site: npm i @formhaus/core @formhaus/react
|
|
24
|
+
For Vue use @formhaus/vue. Guide: https://formhaus.dev/guide/
|
|
25
|
+
No site? Publish it and collect responses at https://app.formhaus.dev
|
|
26
|
+
|
|
27
|
+
Config backups were saved as .bak. See every file: npx formhaus --verbose
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Running it again prints `already set up` for each agent. A step that fails is marked `✗` with the fix below it, and the exit code is 1. Symbols are colored only in a terminal, and `NO_COLOR` turns color off.
|
|
31
|
+
|
|
32
|
+
## What next
|
|
33
|
+
|
|
34
|
+
1. Restart your agents so they load the Formhaus MCP server.
|
|
35
|
+
2. Ask one to build a form: "Build a contact form with Formhaus: name, email, message."
|
|
36
|
+
3. Render the definition with `npm i @formhaus/core @formhaus/react` or `@formhaus/vue`. See [Getting started](https://formhaus.dev/guide/).
|
|
37
|
+
4. Or publish it without code and collect responses at [app.formhaus.dev](https://app.formhaus.dev).
|
|
10
38
|
|
|
11
39
|
## What it changes
|
|
12
40
|
|
|
@@ -14,12 +42,12 @@ Run it inside a project to also add the Cursor rule there. Requires Node 20 or l
|
|
|
14
42
|
|-------|-------------|--------|
|
|
15
43
|
| Claude Code | `claude` on `PATH` | `claude mcp add --scope user formhaus -- npx -y @formhaus/mcp` and the `formhaus-create-form` skill in `~/.claude/skills` |
|
|
16
44
|
| Codex | `~/.codex` or `codex` on `PATH` | `[mcp_servers.formhaus]` in `~/.codex/config.toml` |
|
|
17
|
-
| Cursor | `~/.cursor` or `cursor` on `PATH` | `formhaus` in `~/.cursor/mcp.json`, and `.cursor/rules/formhaus.mdc`
|
|
45
|
+
| Cursor | `~/.cursor` or `cursor` on `PATH` | `formhaus` in `~/.cursor/mcp.json`, and `.cursor/rules/formhaus.mdc` at the project root |
|
|
18
46
|
| Claude Desktop | its config directory on macOS and Windows | `formhaus` in `claude_desktop_config.json` |
|
|
19
47
|
|
|
20
|
-
Windsurf, Gemini CLI, OpenCode, VS Code and Claude Desktop on Linux are detected and listed with manual setup steps.
|
|
48
|
+
Windsurf, Gemini CLI, OpenCode, VS Code and Claude Desktop on Linux are detected and listed with a link to the manual setup steps.
|
|
21
49
|
|
|
22
|
-
Existing entries are kept. An existing `formhaus` server with another command or args is reported and left unchanged, and a project `.mcp.json` never counts as set up. Before the first change to a file, the original is saved as `<file>.bak`. Formhaus does not write to a file that is a symbolic link, or through a linked directory inside the project. Running it again changes nothing. If the Formhaus Claude Code plugin is installed, Claude Code is left as is.
|
|
50
|
+
Existing entries are kept. An existing `formhaus` server with another command or args is reported and left unchanged, and a project `.mcp.json` never counts as set up. Before the first change to a config file, the original is saved as `<file>.bak`. If you edited the Cursor rule or the skill, Formhaus keeps your version. `--force` replaces it and saves your version as `<file>.bak`. Formhaus does not write to a file that is a symbolic link, or through a linked directory inside the project. Running it again changes nothing. If the Formhaus Claude Code plugin is installed, Claude Code is left as is.
|
|
23
51
|
|
|
24
52
|
## Options
|
|
25
53
|
|
|
@@ -27,6 +55,8 @@ Existing entries are kept. An existing `formhaus` server with another command or
|
|
|
27
55
|
npx formhaus [setup] [options]
|
|
28
56
|
|
|
29
57
|
--dry-run Show the planned changes and write nothing
|
|
58
|
+
--force Replace your edited skill and rule, saving them as .bak
|
|
59
|
+
--verbose List every file it checks or changes, and the backups
|
|
30
60
|
-h, --help Show help
|
|
31
61
|
-v, --version Show the version
|
|
32
62
|
```
|
package/dist/cli.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { parseArgs, isDeepStrictEqual } from 'util';
|
|
4
|
-
import {
|
|
5
|
-
import { join, isAbsolute, sep, dirname, relative, resolve } from 'path';
|
|
4
|
+
import { join, isAbsolute, resolve, dirname, parse, sep, relative } from 'path';
|
|
6
5
|
import { spawnSync } from 'child_process';
|
|
6
|
+
import { randomBytes, createHash } from 'crypto';
|
|
7
|
+
import { existsSync, statSync, accessSync, constants, realpathSync, readFileSync, mkdirSync, copyFileSync, renameSync, rmSync, lstatSync, writeFileSync, chmodSync } from 'fs';
|
|
7
8
|
|
|
8
9
|
// package.json
|
|
9
|
-
var version = "0.
|
|
10
|
+
var version = "0.19.0";
|
|
10
11
|
|
|
11
12
|
// ../../.cursor/rules/formhaus.mdc
|
|
12
13
|
var formhaus_default = '---\ndescription: Formhaus form definitions. Use when creating, editing or reviewing a Formhaus JSON form definition, or code that passes one to FormEngine or FormRenderer.\nalwaysApply: false\n---\n\n# Formhaus form definitions\n\nA Formhaus form is a JSON definition rendered by `@formhaus/core` with `@formhaus/react` or `@formhaus/vue`. One definition drives the web form and the Figma plugin.\n\n- Start each definition file with `"$schema": "https://formhaus.dev/schema/form-definition.json"`.\n- Required keys: `id` (kebab-case), `title`, `submit: { "label": "..." }`.\n- Use `fields` for a single page or `steps` for a multi-step form, never both.\n- Field `key` values are camelCase and unique across all steps.\n- `show` and `showAny` conditions reference existing field keys and use `eq`, `neq`, `in`, `notIn` or `notEmpty`.\n- Step `routes` only point to later steps. `to: null` ends the form without auto-submission.\n\n## Checking definitions\n\nWhen the `formhaus` MCP server is available (`npx -y @formhaus/mcp`):\n\n- Call `capabilities` for the current field types, validation rules and step semantics.\n- Call `example_definitions` for working examples.\n- Call `validate_definition` after every change and fix all `errors` before finishing.\n- Call `simulate_path` with sample answers to check branching steps and conditions.\n\nReference: https://formhaus.dev/api/definition.html\n';
|
|
13
14
|
|
|
14
15
|
// ../../plugins/formhaus/skills/formhaus-create-form/SKILL.md
|
|
15
|
-
var SKILL_default = '---\nname: formhaus-create-form\ndescription: |\n Generate @formhaus/core form definitions from any input: text description,\n CSV/table, or screenshot of a form. Interactive step/condition builder.\n Output pastes directly into the Formhaus Figma plugin or FormRenderer.\n Use when: "generate form", "form definition", "form JSON", "create a form",\n "formhaus", or when user describes form fields in any format.\nallowed-tools:\n - Read\n - Write\n - Glob\n - Grep\n - AskUserQuestion\n - Agent\n - mcp__plugin_formhaus_formhaus__validate_definition\n - mcp__plugin_formhaus_formhaus__simulate_path\n - mcp__plugin_formhaus_formhaus__capabilities\n - mcp__plugin_formhaus_formhaus__example_definitions\n - mcp__plugin_formhaus_formhaus__publish_form\n - mcp__plugin_formhaus_formhaus__list_forms\n - mcp__formhaus__validate_definition\n - mcp__formhaus__simulate_path\n - mcp__formhaus__capabilities\n - mcp__formhaus__example_definitions\n - mcp__formhaus__publish_form\n - mcp__formhaus__list_forms\n - Edit\n - Bash\n---\n\n# Form Definition Generator\n\nYou generate valid `@formhaus/core` form definitions from any input.\nThe output is ready to paste into the Formhaus Figma plugin or use with FormRenderer.\nThe formhaus repo does not need to be cloned. The JSON Schema is published at\n`https://formhaus.dev/schema/form-definition.json`.\n\n## Formhaus MCP tools\n\nCheck whether the `formhaus` MCP server tools are available (`validate_definition`,\n`simulate_path`, `capabilities`, `example_definitions`, `publish_form`). The Formhaus Claude Code plugin\nstarts the server. Without the plugin, it is added with\n`claude mcp add formhaus -- npx -y @formhaus/mcp`.\n\n- `capabilities` returns the current field types, validation rules, condition operators\n and step semantics. Prefer it over the reference below when they differ.\n- `example_definitions` lists bundled example definitions and returns one by `id`.\n- `validate_definition` checks a definition against the JSON Schema and the engine.\n- `simulate_path` walks a multi-step definition with sample answers and reports the step path.\n- `publish_form` publishes a definition to Formhaus Cloud and returns a live endpoint, a hosted page URL, an embed snippet and a React snippet.\n\nIf the tools are not available, validate by hand against the types below and tell the\nuser that `@formhaus/mcp` can check the result.\n\n## Types Reference\n\nThese are the exact TypeScript types. Your output MUST conform to them.\n\n```typescript\ninterface FormDefinition {\n id: string; // kebab-case identifier\n title: string; // human-readable form title\n description?: string; // short intro under the title, up to 1000 characters\n submit: FormAction; // submit button config (required)\n cancel?: FormAction; // cancel button (optional)\n fields?: FormField[]; // single-step form\n steps?: FormStep[]; // multi-step form (use steps OR fields, not both)\n}\n\ninterface FormStep {\n id: string;\n title: string;\n description?: string;\n fields: FormField[];\n show?: ShowCondition[]; // step visible when ALL conditions true\n showAny?: ShowCondition[]; // step visible when ANY condition true\n routes?: StepRoute[]; // ordered forward destinations\n next?: FormAction | false; // custom "next" or false to hide it\n back?: FormAction | false; // custom "back" or false to hide it\n skip?: FormAction; // Skip button: resets the step, moves on without validation\n}\n\ninterface StepRoute {\n to: string | null; // later step id, or terminal without auto-submission\n show?: ShowCondition[];\n showAny?: ShowCondition[];\n}\n\ninterface FormField {\n key: string; // camelCase field identifier\n type: FieldType;\n label: string;\n placeholder?: string;\n helperText?: string;\n defaultValue?: unknown;\n autoAdvance?: boolean; // explicit activation advances; built-in radio supports it\n show?: ShowCondition[]; // field visible when ALL conditions true\n showAny?: ShowCondition[]; // field visible when ANY condition true\n validation?: FieldValidation;\n options?: FieldOption[]; // for select, multiselect, radio\n layout?: \'stack\' | \'inline\'; // radio/multiselect; inline only for a few short options\n optionsFrom?: string; // dynamic options loaded at runtime\n optionsDependsOn?: string[];\n accept?: string; // for file type: "image/*", ".pdf"\n rows?: number; // for textarea\n mask?: string; // input mask\n inputMode?: \'text\' | \'numeric\' | \'decimal\' | \'tel\' | \'email\' | \'url\' | \'search\';\n}\n\ntype FieldType =\n | \'text\' | \'email\' | \'phone\' | \'number\' | \'password\'\n | \'select\' | \'autocomplete\' | \'multiselect\'\n | \'checkbox\' | \'radio\' | \'switch\'\n | \'file\' | \'date\' | \'datetime\' | \'time\' | \'textarea\' | \'hidden\';\n\ninterface ShowCondition {\n field: string; // references another field\'s key\n eq?: string | number | boolean; // equals\n neq?: string | number | boolean; // not equals\n in?: (string | number)[]; // one of\n notIn?: (string | number)[]; // not one of\n notEmpty?: boolean; // has a value\n}\n\ninterface FieldValidation {\n required?: boolean | string; // true or custom message\n minLength?: number;\n minLengthMessage?: string;\n maxLength?: number;\n maxLengthMessage?: string;\n min?: number;\n minMessage?: string;\n max?: number;\n maxMessage?: string;\n pattern?: string; // regex\n patternMessage?: string;\n matchField?: string; // must match another field (e.g. confirmPassword)\n matchFieldMessage?: string;\n validator?: string; // custom validator name\n}\n\ninterface FieldOption { value: string; label: string; }\ninterface FormAction {\n label: string;\n variant?: \'primary\' | \'secondary\' | \'text\';\n action?: string;\n disabled?: ShowCondition[];\n}\n```\n\n## Workflow\n\n### Step 1: Detect input format\n\nLook at the user\'s input and classify:\n\n- **Text description**: "Registration form with name, email, password"\n- **CSV/table**: structured data with columns like key, type, label, required\n- **Screenshot**: user provides a path to an image file or says "look at this screenshot"\n- **Existing definition**: user references a bundled example (`example_definitions`)\n\n### Step 2: Extract fields\n\nFrom the input, extract a flat list of fields with: key, type, label, and any\nobvious validation (required, email format, etc).\n\n**Type inference rules:**\n- Email addresses \u2192 `email`\n- Phone numbers \u2192 `phone`\n- Passwords, PINs \u2192 `password`\n- "Agree to terms", "Subscribe" \u2192 `checkbox`\n- "Account type", "Gender" with 2-4 options \u2192 `radio`\n- Dropdowns, "Select..." with 5+ options \u2192 `select`\n- Searchable dropdowns, typeahead \u2192 `autocomplete`\n- Country, city, currency \u2192 `select`\n- Long text, "Description", "Notes", "Bio" \u2192 `textarea`\n- Dates, birthdays, "When" \u2192 `date`\n- Date and time together \u2192 `datetime`\n- Time of day only, "Start time", "Opening hours" \u2192 `time`\n- Tracking values the user never sees, "Source", "Campaign", "Referral code from the link" \u2192 `hidden`\n- File upload, "Attach", "Upload" \u2192 `file`\n- Toggle, "Enable/Disable", notifications \u2192 `switch`\n- Numbers, amounts, quantities \u2192 `number`\n- Everything else \u2192 `text`\n\n### Step 3: Interactive refinement\n\nUse AskUserQuestion for each decision point. ONE question at a time.\n\n**3a: Steps**\n\nIf the form has 5+ fields, ask:\n\n> "This form has N fields. Should it be a single page or split into steps?\n> If steps, describe how to group them (e.g. \'Personal info, Address, Payment\')."\n\nOptions:\n- A) Single step (all fields on one page)\n- B) Multi-step (I\'ll describe the groups)\n- C) Auto-group (you decide based on field types)\n\nIf C: group by semantic similarity (personal info, address, account, payment, etc.).\n\n**3b: Conditions**\n\nIf any field logically depends on another (e.g. "Bank name" only makes sense\nwhen payment method is "bank"), ask:\n\n> "I noticed [field X] probably only shows when [field Y] = [value].\n> Should I add show conditions?"\n\nOptions:\n- A) Yes, add the conditions you suggested\n- B) No conditions, show everything always\n- C) Let me specify which fields depend on what\n\n**3c: Validation**\n\nFor each field, infer validation:\n- Fields marked "required" or with * \u2192 `required: true`\n- Email fields \u2192 pattern for email format\n- Password fields \u2192 minLength: 8\n- Phone fields \u2192 inputMode: \'tel\'\n- Number fields \u2192 inputMode: \'numeric\'\n\nAsk only if there\'s ambiguity: "Should [field] be required?"\n\n### Step 4: Generate JSON\n\nOutput the complete form definition JSON. Follow these rules:\n\n1. `id` is kebab-case derived from the title\n2. `key` for each field is camelCase\n3. Every form MUST have `submit: { label: "..." }`\n4. Use `fields` for single-step, `steps` for multi-step\n5. Only include optional properties when they add value (no empty strings, no `required: false`)\n6. Options for select/radio must have both `value` and `label`\n\nPut `"$schema": "https://formhaus.dev/schema/form-definition.json"` as the first key\nso editors validate and autocomplete the file.\n\n### Step 5: Validate\n\nIf the MCP tools are available:\n\n1. Call `validate_definition` with the definition. Fix every entry in `errors` and\n re-run until `errors` is empty. Review `warnings` and fix the ones that are real mistakes.\n2. For multi-step definitions with conditions or routes, call `simulate_path` with answers\n that take each branch and check that the path matches the user\'s intent.\n\n### Step 6: Present and confirm\n\nShow the generated JSON in a code block. Ask:\n\n> "Here\'s your form definition. You can paste this directly into the\n> Formhaus Figma plugin or use with FormRenderer."\n\nOptions:\n- A) Looks good, copy and done\n- B) Make changes (specify what)\n- C) Publish it and get a live endpoint\n\nIf B: apply changes and re-present\n\n### Step 7: Publish and wire into the project\n\nOnly when the user chose C, or asked for a working form, a hosted form or an endpoint.\nRequires the `publish_form` tool and a definition that passed `validate_definition`.\n\n1. Call `publish_form` with the definition. Hosted forms do not support `password`, `file`,\n `autocomplete`, `optionsFrom` and custom field types. If the tool reports an error, fix every\n listed path and call it again.\n2. Tell the user the `hosted_url` and `endpoint`. Without an API key the form is unclaimed\n (7 days, 100 submissions): give them the `claim_url` from the result.\n3. Detect the project: React or Vue in `package.json`, or plain HTML. Install the matching\n package (`@formhaus/react` or `@formhaus/vue`, plus `@formhaus/core` for `style.css`) with\n the project\'s package manager.\n4. Add the `react_snippet` from the result where the user wants the form. It imports\n `FormhausForm` from `@formhaus/react/cloud`. For Vue use `FormhausForm` from\n `@formhaus/vue/cloud` with the same `id`. For plain HTML use `embed_snippet`.\n5. Publishing again with the same `form_id` ships a new version at the same endpoint, so\n later edits to the definition do not change the project code.\n\n## Example Definitions\n\nCall `example_definitions` to list the bundled examples and fetch one by `id` when you\nneed to verify a pattern or show the user an example. They cover common field types,\nvalidation, show conditions, linear steps, and branching steps with routes.\n\n## Rules\n\n- NEVER call `publish_form` unless the user asked for a hosted form or Formhaus Cloud. If the project has its own backend, render with `FormRenderer` and submit to that backend.\n- NEVER output invalid JSON. Validate before presenting, with `validate_definition` when available.\n- NEVER invent field types not in the FieldType union.\n- NEVER use `fields` AND `steps` in the same definition. Pick one.\n- Keys must be unique within a definition (across all steps).\n- ShowCondition.field must reference an existing field key.\n- For multi-step: each step needs `id`, `title`, and `fields`.\n- Submit label should match the form\'s purpose ("Submit", "Create transfer", "Register", etc.).\n';
|
|
16
|
+
var SKILL_default = '---\nname: formhaus-create-form\ndescription: |\n Generate @formhaus/core form definitions from any input: text description,\n CSV/table, or screenshot of a form. Interactive step/condition builder.\n Output pastes directly into the Formhaus Figma plugin or FormRenderer.\n Use when: "generate form", "form definition", "form JSON", "create a form",\n "formhaus", or when user describes form fields in any format.\nallowed-tools:\n - Read\n - Write\n - Glob\n - Grep\n - AskUserQuestion\n - Agent\n - mcp__plugin_formhaus_formhaus__validate_definition\n - mcp__plugin_formhaus_formhaus__simulate_path\n - mcp__plugin_formhaus_formhaus__capabilities\n - mcp__plugin_formhaus_formhaus__example_definitions\n - mcp__plugin_formhaus_formhaus__publish_form\n - mcp__plugin_formhaus_formhaus__list_forms\n - mcp__formhaus__validate_definition\n - mcp__formhaus__simulate_path\n - mcp__formhaus__capabilities\n - mcp__formhaus__example_definitions\n - mcp__formhaus__publish_form\n - mcp__formhaus__list_forms\n - Edit\n - Bash\n---\n\n# Form Definition Generator\n\nYou generate valid `@formhaus/core` form definitions from any input.\nThe output is ready to paste into the Formhaus Figma plugin or use with FormRenderer.\nThe formhaus repo does not need to be cloned. The JSON Schema is published at\n`https://formhaus.dev/schema/form-definition.json`.\n\n## Formhaus MCP tools\n\nCheck whether the `formhaus` MCP server tools are available (`validate_definition`,\n`simulate_path`, `capabilities`, `example_definitions`, `publish_form`). The Formhaus Claude Code plugin\nstarts the server. Without the plugin, it is added with\n`claude mcp add formhaus -- npx -y @formhaus/mcp`.\n\n- `capabilities` returns the current field types, validation rules, condition operators\n and step semantics. Prefer it over the reference below when they differ.\n- `example_definitions` lists bundled example definitions and returns one by `id`.\n- `validate_definition` checks a definition against the JSON Schema and the engine.\n- `simulate_path` walks a multi-step definition with sample answers and reports the step path.\n- `publish_form` publishes a definition to Formhaus Cloud and returns a live endpoint, a hosted page URL, an embed snippet and a React snippet.\n\nIf the tools are not available, validate by hand against the types below and tell the\nuser that `@formhaus/mcp` can check the result.\n\n## Types Reference\n\nThese are the exact TypeScript types. Your output MUST conform to them.\n\n```typescript\ninterface FormDefinition {\n id: string; // kebab-case identifier\n formatVersion?: string; // "1.3" when the form uses scale, rating, option images or a matrix\n title: string; // human-readable form title\n description?: string; // short intro under the title, up to 1000 characters\n submit: FormAction; // submit button config (required)\n cancel?: FormAction; // cancel button (optional)\n fields?: FormField[]; // single-step form\n steps?: FormStep[]; // multi-step form (use steps OR fields, not both)\n}\n\ninterface FormStep {\n id: string;\n title: string;\n description?: string;\n fields: (FormField | FormSection)[]; // may be empty for a welcome or statement screen\n show?: ShowCondition[]; // step visible when ALL conditions true\n showAny?: ShowCondition[]; // step visible when ANY condition true\n routes?: StepRoute[]; // ordered forward destinations\n next?: FormAction | false; // custom "next" or false to hide it\n back?: FormAction | false; // custom "back" or false to hide it\n skip?: FormAction; // Skip button: resets the step, moves on without validation\n}\n\ninterface StepRoute {\n to: string | null; // later step id, or terminal without auto-submission\n show?: ShowCondition[];\n showAny?: ShowCondition[];\n}\n\ninterface FormField {\n key: string; // camelCase field identifier\n type: FieldType;\n label: string;\n placeholder?: string;\n helperText?: string;\n defaultValue?: unknown;\n autoAdvance?: boolean; // explicit activation advances; built-in radio, scale and rating support it\n show?: ShowCondition[]; // field visible when ALL conditions true\n showAny?: ShowCondition[]; // field visible when ANY condition true\n validation?: FieldValidation;\n options?: FieldOption[]; // for select, multiselect, radio; anchor labels on the first, middle and last point of scale and rating\n layout?: \'stack\' | \'inline\'; // radio/multiselect; inline only for a few short options\n optionsFrom?: string; // dynamic options loaded at runtime\n optionsDependsOn?: string[];\n accept?: string; // for file type: "image/*", ".pdf"\n rows?: number; // for textarea\n mask?: string; // input mask\n inputMode?: \'text\' | \'numeric\' | \'decimal\' | \'tel\' | \'email\' | \'url\' | \'search\';\n}\n\ntype FieldType =\n | \'text\' | \'email\' | \'phone\' | \'number\' | \'password\'\n | \'select\' | \'autocomplete\' | \'multiselect\'\n | \'checkbox\' | \'radio\' | \'switch\'\n | \'file\' | \'date\' | \'datetime\' | \'time\' | \'textarea\' | \'hidden\'\n | \'scale\' // whole number from validation.min (0 or 1, default 1) to validation.max (default 5)\n | \'rating\'; // stars from 1 to validation.max (default 5, 3 to 10)\n\ninterface FormSection {\n key: string;\n type: \'section\';\n title: string;\n description?: string;\n layout?: \'matrix\'; // grid: each field is a radio row with the same options\n show?: ShowCondition[];\n fields: FormField[]; // may be empty for a statement between questions\n}\n\ninterface ShowCondition {\n field: string; // references another field\'s key\n eq?: string | number | boolean; // equals\n neq?: string | number | boolean; // not equals\n in?: (string | number)[]; // one of\n notIn?: (string | number)[]; // not one of\n notEmpty?: boolean; // has a value\n gt?: number; gte?: number; // numeric comparisons, such as an NPS score\n lt?: number; lte?: number;\n}\n\ninterface FieldValidation {\n required?: boolean | string; // true or custom message\n minLength?: number;\n minLengthMessage?: string;\n maxLength?: number;\n maxLengthMessage?: string;\n min?: number;\n minMessage?: string;\n max?: number;\n maxMessage?: string;\n pattern?: string; // regex\n patternMessage?: string;\n matchField?: string; // must match another field (e.g. confirmPassword)\n matchFieldMessage?: string;\n validator?: string; // custom validator name\n}\n\ninterface FieldOption {\n value: string; // on scale and rating anchors: the point number, such as "0" or "10"\n label: string;\n image?: string; // image URL for radio and multiselect (picture choice)\n}\ninterface FormAction {\n label: string;\n variant?: \'primary\' | \'secondary\' | \'text\';\n action?: string;\n disabled?: ShowCondition[];\n}\n```\n\n## Workflow\n\n### Step 1: Detect input format\n\nLook at the user\'s input and classify:\n\n- **Text description**: "Registration form with name, email, password"\n- **CSV/table**: structured data with columns like key, type, label, required\n- **Screenshot**: user provides a path to an image file or says "look at this screenshot"\n- **Existing definition**: user references a bundled example (`example_definitions`)\n\n### Step 2: Extract fields\n\nFrom the input, extract a flat list of fields with: key, type, label, and any\nobvious validation (required, email format, etc).\n\n**Type inference rules:**\n- Email addresses \u2192 `email`\n- Phone numbers \u2192 `phone`\n- Passwords, PINs \u2192 `password`\n- "Agree to terms", "Subscribe" \u2192 `checkbox`\n- "Account type", "Gender" with 2-4 options \u2192 `radio`\n- Dropdowns, "Select..." with 5+ options \u2192 `select`\n- Searchable dropdowns, typeahead \u2192 `autocomplete`\n- Country, city, currency \u2192 `select`\n- Long text, "Description", "Notes", "Bio" \u2192 `textarea`\n- Dates, birthdays, "When" \u2192 `date`\n- Date and time together \u2192 `datetime`\n- Time of day only, "Start time", "Opening hours" \u2192 `time`\n- Tracking values the user never sees, "Source", "Campaign", "Referral code from the link" \u2192 `hidden`\n- File upload, "Attach", "Upload" \u2192 `file`\n- Toggle, "Enable/Disable", notifications \u2192 `switch`\n- Numbers, amounts, quantities \u2192 `number`\n- "How likely are you to recommend" with 0 to 10 \u2192 `scale` with `validation: { min: 0, max: 10 }` and anchors `"0": "Not at all likely"`, `"10": "Extremely likely"` (NPS)\n- "Rate", "stars", satisfaction from 1 to 5 \u2192 `rating`\n- "Strongly disagree \u2026 Strongly agree" or "1 to 7" for one statement \u2192 `scale` with start and end anchors\n- Several statements with the same agree/disagree answers \u2192 a `section` with `layout: "matrix"` and one `radio` per statement, all with the same options\n- Options shown as pictures \u2192 `radio` or `multiselect` with `image` on each option\n- An intro, welcome or "thank you for joining" screen \u2192 a step with a title, a description, `next: { label: "Start" }` and `fields: []`\n- Everything else \u2192 `text`\n\n### Step 3: Interactive refinement\n\nUse AskUserQuestion for each decision point. ONE question at a time.\n\n**3a: Steps**\n\nIf the form has 5+ fields, ask:\n\n> "This form has N fields. Should it be a single page or split into steps?\n> If steps, describe how to group them (e.g. \'Personal info, Address, Payment\')."\n\nOptions:\n- A) Single step (all fields on one page)\n- B) Multi-step (I\'ll describe the groups)\n- C) Auto-group (you decide based on field types)\n\nIf C: group by semantic similarity (personal info, address, account, payment, etc.).\n\n**3b: Conditions**\n\nIf any field logically depends on another (e.g. "Bank name" only makes sense\nwhen payment method is "bank"), ask:\n\n> "I noticed [field X] probably only shows when [field Y] = [value].\n> Should I add show conditions?"\n\nOptions:\n- A) Yes, add the conditions you suggested\n- B) No conditions, show everything always\n- C) Let me specify which fields depend on what\n\n**3c: Validation**\n\nFor each field, infer validation:\n- Fields marked "required" or with * \u2192 `required: true`\n- Email fields \u2192 pattern for email format\n- Password fields \u2192 minLength: 8\n- Phone fields \u2192 inputMode: \'tel\'\n- Number fields \u2192 inputMode: \'numeric\'\n\nAsk only if there\'s ambiguity: "Should [field] be required?"\n\n### Step 4: Generate JSON\n\nOutput the complete form definition JSON. Follow these rules:\n\n1. `id` is kebab-case derived from the title\n2. `key` for each field is camelCase\n3. Every form MUST have `submit: { label: "..." }`\n4. Use `fields` for single-step, `steps` for multi-step\n5. Only include optional properties when they add value (no empty strings, no `required: false`)\n6. Options for select/radio must have both `value` and `label`\n7. Set `"formatVersion": "1.3"` when the form has a `scale`, a `rating`, an option `image` or a matrix section. Set at least `"1.1"` when it has sections or groups\n8. Conditions on `scale` and `rating` compare numbers: `{ "field": "nps", "lte": 6 }`. A range is two conditions: `gte` and `lte`\n\nPut `"$schema": "https://formhaus.dev/schema/form-definition.json"` as the first key\nso editors validate and autocomplete the file.\n\n### Step 5: Validate\n\nIf the MCP tools are available:\n\n1. Call `validate_definition` with the definition. Fix every entry in `errors` and\n re-run until `errors` is empty. Review `warnings` and fix the ones that are real mistakes.\n2. For multi-step definitions with conditions or routes, call `simulate_path` with answers\n that take each branch and check that the path matches the user\'s intent.\n\n### Step 6: Present and confirm\n\nShow the generated JSON in a code block. Ask:\n\n> "Here\'s your form definition. You can paste this directly into the\n> Formhaus Figma plugin or use with FormRenderer."\n\nOptions:\n- A) Looks good, copy and done\n- B) Make changes (specify what)\n- C) Publish it and get a live endpoint\n\nIf B: apply changes and re-present\n\n### Step 7: Publish and wire into the project\n\nOnly when the user chose C, or asked for a working form, a hosted form or an endpoint.\nRequires the `publish_form` tool and a definition that passed `validate_definition`.\n\n1. Call `publish_form` with the definition. Hosted forms do not support `password`, `file`,\n `autocomplete`, `optionsFrom` and custom field types. If the tool reports an error, fix every\n listed path and call it again.\n2. Tell the user the `hosted_url` and `endpoint`. Without an API key the form is unclaimed\n (7 days, 100 submissions): give them the `claim_url` from the result.\n3. Detect the project: React or Vue in `package.json`, or plain HTML. Install the matching\n package (`@formhaus/react` or `@formhaus/vue`, plus `@formhaus/core` for `style.css`) with\n the project\'s package manager.\n4. Add the `react_snippet` from the result where the user wants the form. It imports\n `FormhausForm` from `@formhaus/react/cloud`. For Vue use `FormhausForm` from\n `@formhaus/vue/cloud` with the same `id`. For plain HTML use `embed_snippet`.\n5. Publishing again with the same `form_id` ships a new version at the same endpoint, so\n later edits to the definition do not change the project code.\n\n## Example Definitions\n\nCall `example_definitions` to list the bundled examples and fetch one by `id` when you\nneed to verify a pattern or show the user an example. They cover common field types,\nvalidation, show conditions, linear steps, and branching steps with routes.\n\n## Rules\n\n- NEVER call `publish_form` unless the user asked for a hosted form or Formhaus Cloud. If the project has its own backend, render with `FormRenderer` and submit to that backend.\n- NEVER output invalid JSON. Validate before presenting, with `validate_definition` when available.\n- NEVER invent field types not in the FieldType union.\n- NEVER use `fields` AND `steps` in the same definition. Pick one.\n- Keys must be unique within a definition (across all steps).\n- ShowCondition.field must reference an existing field key.\n- For multi-step: each step needs `id`, `title`, and `fields`.\n- Submit label should match the form\'s purpose ("Submit", "Create transfer", "Register", etc.).\n';
|
|
16
17
|
|
|
17
18
|
// src/context.ts
|
|
18
19
|
var SetupError = class extends Error {
|
|
@@ -21,6 +22,8 @@ var SERVER_NAME = "formhaus";
|
|
|
21
22
|
var SERVER_PACKAGE = "@formhaus/mcp";
|
|
22
23
|
var SERVER = { command: "npx", args: ["-y", SERVER_PACKAGE] };
|
|
23
24
|
var GUIDE_URL = "https://formhaus.dev/guide/mcp";
|
|
25
|
+
var START_URL = "https://formhaus.dev/guide/";
|
|
26
|
+
var APP_URL = "https://app.formhaus.dev";
|
|
24
27
|
function isOurServer(command2, args) {
|
|
25
28
|
return command2 === SERVER.command && Array.isArray(args) && args.length === SERVER.args.length && args.every((arg, index) => arg === SERVER.args[index]);
|
|
26
29
|
}
|
|
@@ -44,38 +47,95 @@ function refuseLinkedDirectories(ctx, root, file) {
|
|
|
44
47
|
const parts = relative(root, dirname(file)).split(sep).filter(Boolean);
|
|
45
48
|
for (let index = 1; index <= parts.length; index++) refuseSymlink(ctx, resolve(root, ...parts.slice(0, index)));
|
|
46
49
|
}
|
|
47
|
-
function
|
|
48
|
-
const backup = `${file}.bak`;
|
|
49
|
-
const temporary = `${file}.tmp`;
|
|
50
|
+
function refuseLinks(ctx, file, project) {
|
|
50
51
|
if (project !== void 0) refuseLinkedDirectories(ctx, project, file);
|
|
51
|
-
for (const path of [file,
|
|
52
|
+
for (const path of [file, `${file}.bak`]) refuseSymlink(ctx, path);
|
|
53
|
+
}
|
|
54
|
+
function temporaryPath(file) {
|
|
55
|
+
return `${file}.${randomBytes(8).toString("hex")}.tmp`;
|
|
56
|
+
}
|
|
57
|
+
function replaceWith(temporary, file, create) {
|
|
58
|
+
try {
|
|
59
|
+
create();
|
|
60
|
+
renameSync(temporary, file);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error.code !== "EEXIST") rmSync(temporary, { force: true });
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function writeText(ctx, file, text, project, replaceBackup = false) {
|
|
67
|
+
const backup = `${file}.bak`;
|
|
68
|
+
refuseLinks(ctx, file, project);
|
|
52
69
|
if (ctx.dryRun) return;
|
|
53
70
|
mkdirSync(dirname(file), { recursive: true });
|
|
54
|
-
if (existsSync(file) &&
|
|
71
|
+
if (existsSync(file) && replaceBackup) {
|
|
72
|
+
const backupTemporary = temporaryPath(backup);
|
|
73
|
+
replaceWith(backupTemporary, backup, () => copyFileSync(file, backupTemporary, constants.COPYFILE_EXCL));
|
|
74
|
+
} else if (existsSync(file) && !existsSync(backup)) copyFileSync(file, backup, constants.COPYFILE_EXCL);
|
|
55
75
|
const mode = statSync(file, { throwIfNoEntry: false })?.mode;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
const temporary = temporaryPath(file);
|
|
77
|
+
replaceWith(temporary, file, () => {
|
|
78
|
+
writeFileSync(temporary, text, { flag: "wx", mode: mode === void 0 ? void 0 : mode & 384 });
|
|
79
|
+
if (mode !== void 0) chmodSync(temporary, mode & 4095);
|
|
80
|
+
});
|
|
60
81
|
}
|
|
61
|
-
function
|
|
82
|
+
function sha256(text) {
|
|
83
|
+
return createHash("sha256").update(text).digest("hex");
|
|
84
|
+
}
|
|
85
|
+
function writeAsset(ctx, file, text, shipped, project) {
|
|
62
86
|
const current = readText(file);
|
|
63
87
|
if (current === text) return "present";
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
refuseLinks(ctx, file, project);
|
|
89
|
+
if (current === void 0 || shipped.includes(sha256(current))) {
|
|
90
|
+
writeText(ctx, file, text, project);
|
|
91
|
+
return current === void 0 ? "added" : "updated";
|
|
92
|
+
}
|
|
93
|
+
if (!ctx.force) return "kept";
|
|
94
|
+
writeText(ctx, file, text, project, true);
|
|
95
|
+
return "replaced";
|
|
96
|
+
}
|
|
97
|
+
function sameDirectory(a, b) {
|
|
98
|
+
if (resolve(a) === resolve(b)) return true;
|
|
99
|
+
try {
|
|
100
|
+
return realpathSync(a) === realpathSync(b);
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function isProjectRoot(directory) {
|
|
106
|
+
return existsSync(join(directory, ".git")) || existsSync(join(directory, "package.json")) || isDirectory(join(directory, ".cursor"));
|
|
107
|
+
}
|
|
108
|
+
function isFilesystemRoot(directory) {
|
|
109
|
+
return parse(directory).root === directory;
|
|
110
|
+
}
|
|
111
|
+
function findProjectRoot(ctx, isRoot = isFilesystemRoot) {
|
|
112
|
+
let directory = resolve(ctx.cwd);
|
|
113
|
+
while (!sameDirectory(directory, ctx.home) && !isRoot(directory)) {
|
|
114
|
+
if (isProjectRoot(directory)) return directory;
|
|
115
|
+
const parent = dirname(directory);
|
|
116
|
+
if (parent === directory) return void 0;
|
|
117
|
+
directory = parent;
|
|
118
|
+
}
|
|
119
|
+
return void 0;
|
|
66
120
|
}
|
|
67
121
|
function display(ctx, path) {
|
|
68
|
-
return path.startsWith(ctx.home + sep) ? `~${path.slice(ctx.home.length)}` : path;
|
|
122
|
+
return path === ctx.home || path.startsWith(ctx.home + sep) ? `~${path.slice(ctx.home.length)}` : path;
|
|
69
123
|
}
|
|
70
124
|
function step(ctx, agent, item, where, apply) {
|
|
71
125
|
const target = display(ctx, where);
|
|
72
|
-
const
|
|
126
|
+
const backup = `${where}.bak`;
|
|
127
|
+
const hadBackup = existsSync(backup);
|
|
73
128
|
try {
|
|
74
129
|
const state = apply();
|
|
75
|
-
const
|
|
76
|
-
|
|
130
|
+
const change = { agent, item, where: target, state };
|
|
131
|
+
if (!ctx.dryRun && existsSync(backup)) {
|
|
132
|
+
change.backup = `${target}.bak`;
|
|
133
|
+
change.newBackup = !hadBackup || state === "replaced";
|
|
134
|
+
}
|
|
135
|
+
if (state === "kept") change.note = "Run npx formhaus --force to replace it and save your version as .bak.";
|
|
136
|
+
return change;
|
|
77
137
|
} catch (error) {
|
|
78
|
-
return { agent, item, where: target, state: "failed",
|
|
138
|
+
return { agent, item, where: target, state: "failed", note: error instanceof Error ? error.message : String(error) };
|
|
79
139
|
}
|
|
80
140
|
}
|
|
81
141
|
|
|
@@ -141,6 +201,10 @@ function command(ctx, executable, args) {
|
|
|
141
201
|
// src/claude-code.ts
|
|
142
202
|
var NAME = "Claude Code";
|
|
143
203
|
var PLUGIN_PREFIX = `${SERVER_NAME}@`;
|
|
204
|
+
var SHIPPED_SKILLS = [
|
|
205
|
+
"e929afc9f58a267647341d962283d84749a18028e5ec2c4bd770a214c26a7c25",
|
|
206
|
+
"7288b74973a7668592f0baef836793aaf8591f3468546c87ff0c2335c8380065"
|
|
207
|
+
];
|
|
144
208
|
var CLAUDE_ADD_ARGS = ["mcp", "add", "--scope", "user", SERVER_NAME, "--", SERVER.command, ...SERVER.args];
|
|
145
209
|
function configDir(ctx) {
|
|
146
210
|
return ctx.env.CLAUDE_CONFIG_DIR || join(ctx.home, ".claude");
|
|
@@ -164,9 +228,11 @@ function hasPlugin(ctx) {
|
|
|
164
228
|
([id, installs]) => id.startsWith(PLUGIN_PREFIX) && installedForUser(installs) && enabled[id] === true
|
|
165
229
|
);
|
|
166
230
|
}
|
|
231
|
+
function userConfig(ctx) {
|
|
232
|
+
return ctx.env.CLAUDE_CONFIG_DIR ? join(ctx.env.CLAUDE_CONFIG_DIR, ".claude.json") : join(ctx.home, ".claude.json");
|
|
233
|
+
}
|
|
167
234
|
function userServer(ctx) {
|
|
168
|
-
|
|
169
|
-
return jsonServerState(readJsonObject(ctx, file));
|
|
235
|
+
return jsonServerState(readJsonObject(ctx, userConfig(ctx)));
|
|
170
236
|
}
|
|
171
237
|
function projectWarning(ctx) {
|
|
172
238
|
const file = join(ctx.cwd, ".mcp.json");
|
|
@@ -197,20 +263,14 @@ var claudeCode = {
|
|
|
197
263
|
const claude = findOnPath(ctx, "claude");
|
|
198
264
|
const skill = join(configDir(ctx), "skills", "formhaus-create-form", "SKILL.md");
|
|
199
265
|
return [
|
|
200
|
-
step(ctx, NAME, "MCP server",
|
|
201
|
-
step(
|
|
202
|
-
ctx,
|
|
203
|
-
NAME,
|
|
204
|
-
"formhaus-create-form skill",
|
|
205
|
-
skill,
|
|
206
|
-
() => hasPlugin(ctx) ? "present" : writeIfChanged(ctx, skill, SKILL_default)
|
|
207
|
-
),
|
|
266
|
+
step(ctx, NAME, "MCP server", userConfig(ctx), () => addServer(ctx, claude)),
|
|
267
|
+
step(ctx, NAME, "form skill", skill, () => hasPlugin(ctx) ? "present" : writeAsset(ctx, skill, SKILL_default, SHIPPED_SKILLS)),
|
|
208
268
|
...projectWarning(ctx)
|
|
209
269
|
];
|
|
210
270
|
}
|
|
211
271
|
};
|
|
212
272
|
|
|
213
|
-
// ../../node_modules/.pnpm/smol-toml@1.9.
|
|
273
|
+
// ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/error.js
|
|
214
274
|
function getLineColFromPtr(string, ptr) {
|
|
215
275
|
let lines = string.slice(0, ptr).split(/\r?\n/);
|
|
216
276
|
return [lines.length, lines.pop().length + 1];
|
|
@@ -254,7 +314,7 @@ ${codeblock}`, options);
|
|
|
254
314
|
}
|
|
255
315
|
};
|
|
256
316
|
|
|
257
|
-
// ../../node_modules/.pnpm/smol-toml@1.9.
|
|
317
|
+
// ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/primitive.js
|
|
258
318
|
function parseString(ctx) {
|
|
259
319
|
let startPtr = ctx.p;
|
|
260
320
|
let c = ctx.s.charCodeAt(ctx.p++);
|
|
@@ -355,7 +415,7 @@ function parseString(ctx) {
|
|
|
355
415
|
TomlError.x("unfinished string", ctx, startPtr);
|
|
356
416
|
}
|
|
357
417
|
|
|
358
|
-
// ../../node_modules/.pnpm/smol-toml@1.9.
|
|
418
|
+
// ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/date.js
|
|
359
419
|
var DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[Tt ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|z|[-+]\d{2}:\d{2})?$/i;
|
|
360
420
|
var TomlDate = class _TomlDate extends Date {
|
|
361
421
|
#hasDate = false;
|
|
@@ -470,7 +530,7 @@ var TomlDate = class _TomlDate extends Date {
|
|
|
470
530
|
}
|
|
471
531
|
};
|
|
472
532
|
|
|
473
|
-
// ../../node_modules/.pnpm/smol-toml@1.9.
|
|
533
|
+
// ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/extract.js
|
|
474
534
|
function isDigit(char, base = 10) {
|
|
475
535
|
return base === 16 ? char > 47 && char < 58 || char > 64 && char < 71 || char > 96 && char < 103 : char > 47 && char < 48 + base;
|
|
476
536
|
}
|
|
@@ -557,7 +617,9 @@ function parseNumber(ctx, startPtr, startChr, sign, endChr) {
|
|
|
557
617
|
state = 2;
|
|
558
618
|
else if ((state === 1 || state === 3) && (c === 101 || c === 69))
|
|
559
619
|
state = 4;
|
|
560
|
-
else if (state === 4 && (c === 43 || c === 45))
|
|
620
|
+
else if (state === 4 && (c === 43 || c === 45)) {
|
|
621
|
+
state = 6;
|
|
622
|
+
} else if (!isDigit(c))
|
|
561
623
|
TomlError.x(`illegal character in numeric literal`, ctx);
|
|
562
624
|
else if (state > 9)
|
|
563
625
|
state -= 11;
|
|
@@ -677,7 +739,7 @@ function readDate(ctx, str, type, unsafeDelim, errPtr, temporalSuffix) {
|
|
|
677
739
|
}
|
|
678
740
|
}
|
|
679
741
|
|
|
680
|
-
// ../../node_modules/.pnpm/smol-toml@1.9.
|
|
742
|
+
// ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/util.js
|
|
681
743
|
function skipComment(ctx) {
|
|
682
744
|
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
683
745
|
let c = ctx.s.charCodeAt(ctx.p);
|
|
@@ -703,7 +765,7 @@ function skipVoid(ctx, banNewLines, banComments) {
|
|
|
703
765
|
}
|
|
704
766
|
}
|
|
705
767
|
|
|
706
|
-
// ../../node_modules/.pnpm/smol-toml@1.9.
|
|
768
|
+
// ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/struct.js
|
|
707
769
|
function parseKey(ctx, end = 61) {
|
|
708
770
|
let startPtr;
|
|
709
771
|
let state = 0;
|
|
@@ -820,7 +882,7 @@ function parseArray(ctx) {
|
|
|
820
882
|
TomlError.x("unfinished array", ctx, startPtr);
|
|
821
883
|
}
|
|
822
884
|
|
|
823
|
-
// ../../node_modules/.pnpm/smol-toml@1.9.
|
|
885
|
+
// ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/parse.js
|
|
824
886
|
function peekTable(ctx, key, table, meta, type) {
|
|
825
887
|
let t = table;
|
|
826
888
|
let m = meta;
|
|
@@ -887,7 +949,7 @@ function validateTablePeek(ctx, peek, ptr) {
|
|
|
887
949
|
if (peek === null || ctx.uk === 2)
|
|
888
950
|
TomlError.x(peek === null ? "trying to redefine an already defined table or value" : "document contains an unsafe property", ctx, ptr);
|
|
889
951
|
}
|
|
890
|
-
function
|
|
952
|
+
function parse2(toml, options = {}) {
|
|
891
953
|
let ctx = {
|
|
892
954
|
s: toml,
|
|
893
955
|
p: 0,
|
|
@@ -975,7 +1037,7 @@ function isTable(value) {
|
|
|
975
1037
|
}
|
|
976
1038
|
function parseToml(text) {
|
|
977
1039
|
try {
|
|
978
|
-
return
|
|
1040
|
+
return parse2(text, { integersAsBigInt: true });
|
|
979
1041
|
} catch {
|
|
980
1042
|
return void 0;
|
|
981
1043
|
}
|
|
@@ -1026,9 +1088,7 @@ function claudeDesktopDir(ctx) {
|
|
|
1026
1088
|
function found(ctx, directory, bin) {
|
|
1027
1089
|
return isDirectory(directory) || findOnPath(ctx, bin) !== void 0;
|
|
1028
1090
|
}
|
|
1029
|
-
|
|
1030
|
-
return ctx.cwd !== ctx.home && existsSync(join(ctx.cwd, "package.json"));
|
|
1031
|
-
}
|
|
1091
|
+
var SHIPPED_RULES = ["1bb79502ba88f1d38bef0bb3f34ede713f94487643496eae94ca6be7d2c8a32c"];
|
|
1032
1092
|
var codex = {
|
|
1033
1093
|
name: "Codex",
|
|
1034
1094
|
detect: (ctx) => found(ctx, codexHome(ctx), "codex"),
|
|
@@ -1043,9 +1103,12 @@ var cursor = {
|
|
|
1043
1103
|
apply(ctx) {
|
|
1044
1104
|
const file = join(ctx.home, ".cursor", "mcp.json");
|
|
1045
1105
|
const changes = [step(ctx, "Cursor", "MCP server", file, () => addJsonServer(ctx, file))];
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1106
|
+
const root = findProjectRoot(ctx);
|
|
1107
|
+
if (root === void 0) {
|
|
1108
|
+
return [...changes, { agent: "Cursor", item: "Cursor rule", where: display(ctx, ctx.cwd), state: "skipped" }];
|
|
1109
|
+
}
|
|
1110
|
+
const rule = join(root, ".cursor", "rules", "formhaus.mdc");
|
|
1111
|
+
changes.push(step(ctx, "Cursor", "Cursor rule", rule, () => writeAsset(ctx, rule, formhaus_default, SHIPPED_RULES, root)));
|
|
1049
1112
|
return changes;
|
|
1050
1113
|
}
|
|
1051
1114
|
};
|
|
@@ -1071,62 +1134,156 @@ var MANUAL_AGENTS = [
|
|
|
1071
1134
|
];
|
|
1072
1135
|
|
|
1073
1136
|
// src/run.ts
|
|
1074
|
-
var
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1137
|
+
var WIDTH = 79;
|
|
1138
|
+
var INDENT = " ";
|
|
1139
|
+
var DETAIL = " ";
|
|
1140
|
+
var SYMBOLS = {
|
|
1141
|
+
ok: ["\u2713", "32"],
|
|
1142
|
+
manual: ["\u2013", "2"],
|
|
1143
|
+
failed: ["\u2717", "31"],
|
|
1144
|
+
warning: ["!", "33"]
|
|
1080
1145
|
};
|
|
1081
|
-
function
|
|
1082
|
-
return
|
|
1146
|
+
function useColor(ctx) {
|
|
1147
|
+
return ctx.tty === true && !ctx.env.NO_COLOR && ctx.env.TERM !== "dumb";
|
|
1148
|
+
}
|
|
1149
|
+
function paint(ctx, code, text) {
|
|
1150
|
+
return useColor(ctx) ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
1083
1151
|
}
|
|
1084
|
-
function
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1152
|
+
function wrap(text, first, rest = first) {
|
|
1153
|
+
const lines = [];
|
|
1154
|
+
let line = first;
|
|
1155
|
+
let empty = true;
|
|
1156
|
+
for (const word of text.split(" ")) {
|
|
1157
|
+
if (!empty && line.length + 1 + word.length > WIDTH) {
|
|
1158
|
+
lines.push(line);
|
|
1159
|
+
line = rest + word;
|
|
1160
|
+
} else line += (empty ? "" : " ") + word;
|
|
1161
|
+
empty = false;
|
|
1162
|
+
}
|
|
1163
|
+
lines.push(line);
|
|
1164
|
+
return lines;
|
|
1165
|
+
}
|
|
1166
|
+
function list(names) {
|
|
1167
|
+
return names.length < 2 ? names.join("") : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
1168
|
+
}
|
|
1169
|
+
function items(changes, state) {
|
|
1170
|
+
return changes.filter((change) => change.state === state).map((change) => change.item);
|
|
1171
|
+
}
|
|
1172
|
+
function summary(ctx, changes) {
|
|
1173
|
+
const will = (done, planned) => ctx.dryRun ? planned : done;
|
|
1174
|
+
const parts = [];
|
|
1175
|
+
const add = (state, text) => {
|
|
1176
|
+
const names = items(changes, state);
|
|
1177
|
+
if (names.length > 0) parts.push(text(list(names)));
|
|
1178
|
+
};
|
|
1179
|
+
add("failed", (names) => `${names} failed`);
|
|
1180
|
+
for (const change of changes.filter((change2) => change2.state === "different")) {
|
|
1181
|
+
const place = change.item === "MCP server" ? "is configured" : `in ${change.where}`;
|
|
1182
|
+
parts.push(`another ${SERVER_NAME} server ${place}, check it`);
|
|
1183
|
+
}
|
|
1184
|
+
add("added", (names) => `${will("added", "will add")} ${names}`);
|
|
1185
|
+
add("updated", (names) => `${will("updated", "will update")} ${names}`);
|
|
1186
|
+
add("replaced", (names) => `${will("replaced", "will replace")} ${names}`);
|
|
1187
|
+
add("kept", (names) => `${names} ${names.includes(" and ") ? "have" : "has"} your edits, ${will("kept", "will keep")}`);
|
|
1188
|
+
const active = changes.filter((change) => change.state !== "skipped");
|
|
1189
|
+
if (active.length > 0 && active.every((change) => change.state === "present")) parts.push("already set up");
|
|
1190
|
+
else add("present", (names) => `${names} already set up`);
|
|
1191
|
+
if (items(changes, "skipped").length > 0) parts.push("run in a project to add the Cursor rule");
|
|
1192
|
+
return parts.join(" \xB7 ");
|
|
1193
|
+
}
|
|
1194
|
+
function details(change) {
|
|
1195
|
+
if (change.state === "skipped") return [`no project found in ${change.where}`];
|
|
1196
|
+
return change.backup ? [change.where, `backup: ${change.backup}`] : [change.where];
|
|
1197
|
+
}
|
|
1198
|
+
function agentRow(ctx, name, changes) {
|
|
1199
|
+
const has = (state) => changes.some((change) => change.state === state);
|
|
1200
|
+
return {
|
|
1201
|
+
mark: has("failed") ? "failed" : has("different") ? "warning" : "ok",
|
|
1202
|
+
name,
|
|
1203
|
+
summary: summary(ctx, changes),
|
|
1204
|
+
notes: changes.flatMap((change) => change.note ? [change.note] : []),
|
|
1205
|
+
details: changes.flatMap(details)
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
var MANUAL_STEP = `Add a stdio MCP server named "${SERVER_NAME}" with command "${SERVER.command}" and args ${JSON.stringify(SERVER.args)}.`;
|
|
1209
|
+
function manualRow(name) {
|
|
1210
|
+
return { mark: "manual", name, summary: `add it by hand: ${GUIDE_URL}`, notes: [], details: [MANUAL_STEP] };
|
|
1211
|
+
}
|
|
1212
|
+
function printRows(ctx, rows, print) {
|
|
1213
|
+
const width = Math.max(...rows.map((row) => row.name.length)) + 2;
|
|
1214
|
+
for (const row of rows) {
|
|
1215
|
+
const [symbol, code] = SYMBOLS[row.mark];
|
|
1216
|
+
const lead = `${INDENT}${symbol} ${row.name.padEnd(width)}`;
|
|
1217
|
+
const [first, ...rest] = wrap(row.summary, lead, " ".repeat(lead.length));
|
|
1218
|
+
print(first.replace(symbol, paint(ctx, code, symbol)));
|
|
1219
|
+
for (const line of rest) print(line);
|
|
1220
|
+
for (const note of row.notes) for (const line of wrap(note, DETAIL)) print(line);
|
|
1221
|
+
if (ctx.verbose) for (const detail of row.details) for (const line of wrap(detail, DETAIL)) print(line);
|
|
1093
1222
|
}
|
|
1094
1223
|
}
|
|
1095
|
-
function
|
|
1096
|
-
print(
|
|
1097
|
-
print(
|
|
1098
|
-
print(
|
|
1224
|
+
function printNext(ctx, print) {
|
|
1225
|
+
print("");
|
|
1226
|
+
print(paint(ctx, "1", "Next"));
|
|
1227
|
+
print(`${INDENT}1. Restart your agents.`);
|
|
1228
|
+
print(`${INDENT}2. Ask one: "Build a contact form with Formhaus: name, email, message."`);
|
|
1229
|
+
print(`${INDENT}3. Show it on your site: npm i @formhaus/core @formhaus/react`);
|
|
1230
|
+
print(`${INDENT} For Vue use @formhaus/vue. Guide: ${START_URL}`);
|
|
1231
|
+
print(`${INDENT} No site? Publish it and collect responses at ${APP_URL}`);
|
|
1232
|
+
}
|
|
1233
|
+
function printNoAgents(ctx, manual, print) {
|
|
1234
|
+
print("No supported coding agent found.");
|
|
1235
|
+
print("Formhaus sets up Claude Code, Codex, Cursor and Claude Desktop.");
|
|
1236
|
+
if (manual.length > 0) {
|
|
1237
|
+
print("");
|
|
1238
|
+
printRows(ctx, manual.map(manualRow), print);
|
|
1239
|
+
}
|
|
1240
|
+
print("");
|
|
1241
|
+
print("Set up any MCP client by hand:");
|
|
1242
|
+
for (const line of wrap(MANUAL_STEP, INDENT)) print(line);
|
|
1243
|
+
print(`${INDENT}Guide: ${GUIDE_URL}`);
|
|
1244
|
+
return 0;
|
|
1099
1245
|
}
|
|
1100
1246
|
function setup(ctx, print) {
|
|
1101
1247
|
const agents = AGENTS.filter((agent) => agent.detect(ctx));
|
|
1102
1248
|
const manual = MANUAL_AGENTS.filter((agent) => agent.detect(ctx)).map((agent) => agent.name);
|
|
1103
1249
|
if (ctx.dryRun) print("Dry run: nothing is written.");
|
|
1104
|
-
if (agents.length === 0)
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
const
|
|
1250
|
+
if (agents.length === 0) return printNoAgents(ctx, manual, print);
|
|
1251
|
+
const applied = agents.map((agent) => ({ name: agent.name, changes: agent.apply(ctx) }));
|
|
1252
|
+
const changes = applied.flatMap((agent) => agent.changes);
|
|
1253
|
+
const rows = applied.map((agent) => agentRow(ctx, agent.name, agent.changes));
|
|
1254
|
+
const ready = rows.filter((row) => row.mark === "ok").map((row) => row.name);
|
|
1255
|
+
const failed = rows.some((row) => row.mark === "failed");
|
|
1256
|
+
const different = rows.some((row) => row.mark === "warning");
|
|
1257
|
+
const changed = changes.some((change) => change.state === "added" || change.state === "updated" || change.state === "replaced");
|
|
1258
|
+
const names = list(applied.map((agent) => agent.name));
|
|
1259
|
+
if (ctx.dryRun && changed) print(`Formhaus will set up ${names}.`);
|
|
1260
|
+
else if (ready.length === 0) print(`Formhaus could not finish setup in ${names}.`);
|
|
1261
|
+
else print(`Formhaus is ${changed ? "ready" : "already set up"} in ${list(ready)}.`);
|
|
1111
1262
|
print("");
|
|
1112
|
-
|
|
1113
|
-
if (
|
|
1263
|
+
printRows(ctx, [...rows, ...manual.map(manualRow)], print);
|
|
1264
|
+
if (different) {
|
|
1114
1265
|
print("");
|
|
1115
|
-
|
|
1266
|
+
const warning = `A "${SERVER_NAME}" server with another command is configured where marked. Formhaus left it unchanged. Check that you trust it.`;
|
|
1267
|
+
for (const line of wrap(warning, "")) print(line);
|
|
1116
1268
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
if (
|
|
1122
|
-
print(
|
|
1123
|
-
|
|
1124
|
-
|
|
1269
|
+
if (failed) {
|
|
1270
|
+
print("");
|
|
1271
|
+
print("Fix the errors above and run npx formhaus again.");
|
|
1272
|
+
}
|
|
1273
|
+
if (ctx.dryRun) {
|
|
1274
|
+
print("");
|
|
1275
|
+
print("Run without --dry-run to apply.");
|
|
1276
|
+
return failed ? 1 : 0;
|
|
1277
|
+
}
|
|
1278
|
+
if (ready.length > 0) printNext(ctx, print);
|
|
1279
|
+
const footer = [
|
|
1280
|
+
changes.some((change) => change.newBackup) ? "Config backups were saved as .bak." : "",
|
|
1281
|
+
ctx.verbose ? "" : "See every file: npx formhaus --verbose"
|
|
1282
|
+
].filter(Boolean);
|
|
1283
|
+
if (footer.length > 0) {
|
|
1284
|
+
print("");
|
|
1285
|
+
print(footer.join(" "));
|
|
1125
1286
|
}
|
|
1126
|
-
if (failed) print("Some steps failed. Fix the errors above and run formhaus again.");
|
|
1127
|
-
else if (ctx.dryRun) print("Run without --dry-run to apply.");
|
|
1128
|
-
else if (changed) print("Done. Restart your agents to load the Formhaus MCP server.");
|
|
1129
|
-
else if (!different) print("Everything is already set up.");
|
|
1130
1287
|
return failed ? 1 : 0;
|
|
1131
1288
|
}
|
|
1132
1289
|
|
|
@@ -1136,16 +1293,21 @@ var HELP = `Usage: npx formhaus [setup] [options]
|
|
|
1136
1293
|
Adds the Formhaus MCP server (npx -y @formhaus/mcp) to the coding agents
|
|
1137
1294
|
installed on this machine: Claude Code, Codex, Cursor and Claude Desktop.
|
|
1138
1295
|
Installs the formhaus-create-form skill in Claude Code and, inside a project,
|
|
1139
|
-
the Formhaus rule in .cursor/rules. Running it again
|
|
1296
|
+
the Formhaus rule in .cursor/rules at the project root. Running it again
|
|
1297
|
+
changes nothing. Your edits to the skill and the rule are kept.
|
|
1140
1298
|
|
|
1141
1299
|
Options:
|
|
1142
1300
|
--dry-run Show the planned changes and write nothing
|
|
1301
|
+
--force Replace your edited skill and rule, saving them as .bak
|
|
1302
|
+
--verbose List every file it checks or changes, and the backups
|
|
1143
1303
|
-h, --help Show this help
|
|
1144
|
-
-v, --version Show the version
|
|
1304
|
+
-v, --version Show the version
|
|
1305
|
+
|
|
1306
|
+
Guide: https://formhaus.dev/guide/mcp`;
|
|
1145
1307
|
function main(argv, ctx, print, printError) {
|
|
1146
1308
|
let parsed;
|
|
1147
1309
|
try {
|
|
1148
|
-
parsed =
|
|
1310
|
+
parsed = parse3(argv);
|
|
1149
1311
|
} catch (error) {
|
|
1150
1312
|
printError(`${error instanceof Error ? error.message : String(error)}
|
|
1151
1313
|
Run npx formhaus --help for usage.`);
|
|
@@ -1161,14 +1323,16 @@ Run npx formhaus --help for usage.`);
|
|
|
1161
1323
|
Run npx formhaus --help for usage.`);
|
|
1162
1324
|
return 1;
|
|
1163
1325
|
}
|
|
1164
|
-
return setup({ ...ctx, dryRun: values["dry-run"] === true }, print);
|
|
1326
|
+
return setup({ ...ctx, dryRun: values["dry-run"] === true, force: values.force === true, verbose: values.verbose === true }, print);
|
|
1165
1327
|
}
|
|
1166
|
-
function
|
|
1328
|
+
function parse3(argv) {
|
|
1167
1329
|
return parseArgs({
|
|
1168
1330
|
args: argv,
|
|
1169
1331
|
allowPositionals: true,
|
|
1170
1332
|
options: {
|
|
1171
1333
|
"dry-run": { type: "boolean" },
|
|
1334
|
+
force: { type: "boolean" },
|
|
1335
|
+
verbose: { type: "boolean" },
|
|
1172
1336
|
help: { type: "boolean", short: "h" },
|
|
1173
1337
|
version: { type: "boolean", short: "v" }
|
|
1174
1338
|
}
|
|
@@ -1178,7 +1342,7 @@ function parse2(argv) {
|
|
|
1178
1342
|
// src/cli.ts
|
|
1179
1343
|
process.exitCode = main(
|
|
1180
1344
|
process.argv.slice(2),
|
|
1181
|
-
{ home: homedir(), cwd: process.cwd(), env: process.env, platform: process.platform },
|
|
1345
|
+
{ home: homedir(), cwd: process.cwd(), env: process.env, platform: process.platform, tty: process.stdout.isTTY === true },
|
|
1182
1346
|
(line) => console.log(line),
|
|
1183
1347
|
(line) => console.error(line)
|
|
1184
1348
|
);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "formhaus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Set up the Formhaus MCP server and agent instructions in Claude Code, Codex, Cursor and Claude Desktop with one command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
7
|
+
"node": ">=22.12"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
10
|
"formhaus": "./dist/cli.js"
|
|
@@ -14,19 +14,19 @@
|
|
|
14
14
|
],
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/node": "^20",
|
|
17
|
-
"smol-toml": "1.9.
|
|
17
|
+
"smol-toml": "1.9.1",
|
|
18
18
|
"tsup": "^8.5.1",
|
|
19
19
|
"typescript": "^5.7.3",
|
|
20
|
-
"vitest": "^
|
|
20
|
+
"vitest": "^5.0.3"
|
|
21
21
|
},
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"homepage": "https://formhaus.dev/guide/mcp.html",
|
|
24
24
|
"bugs": {
|
|
25
|
-
"url": "https://github.com/
|
|
25
|
+
"url": "https://github.com/formhaus-dev/formhaus/issues"
|
|
26
26
|
},
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
29
|
-
"url": "git+https://github.com/
|
|
29
|
+
"url": "git+https://github.com/formhaus-dev/formhaus.git",
|
|
30
30
|
"directory": "packages/formhaus"
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|