diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 984dbfef3e..f52ffc4303 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "plugins": [ { "name": "pascal-agent-skills", - "source": "./", + "source": "./skills", "description": "Create, inspect, validate, and assess furniture layouts with bounded next actions in Pascal through MCP.", "author": { "name": "Pascal", @@ -18,7 +18,7 @@ }, "version": "0.1.8", "category": "productivity", - "skills": ["./skills/pascal-3d", "./skills/furniture-fit"] + "skills": ["./pascal-3d", "./furniture-fit"] } ] } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index a778917c90..777a39a15e 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -13,7 +13,7 @@ "keywords": ["pascal", "3d", "architecture", "mcp", "furniture", "spatial"], "skills": "./skills/", "interface": { - "displayName": "Pascal agent skills", + "displayName": "Pascal", "shortDescription": "Build 3D scenes and check fit", "longDescription": "Use Pascal's MCP tools to work with editable building scenes, validate and save results, and produce bounded furniture footprint reports with explicit evidence, limitations, and blocker-aware next actions.", "developerName": "Pascal", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 59bb45e662..72a47d3e30 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pascal-agent-skills", - "displayName": "Pascal agent skills", + "displayName": "Pascal", "version": "0.1.8", "description": "Create, inspect, validate, and assess furniture layouts with bounded next actions in Pascal through MCP.", "author": { diff --git a/CHANGELOG.md b/CHANGELOG.md index cec036d527..d76ede4b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixes +- The Claude Code plugin root is now `skills/` instead of the repository root, so installing `pascal-agent-skills@pascal` copies the two skill bundles and their MCP configuration instead of caching the whole monorepo and running `bun install` against the root lockfile ([#832](https://github.com/pascalorg/editor/pull/832)) - Preserve custom scene materials across save, load, clone, fork, and live sync. Materials were dropped at every persistence boundary, so a scene reopened with default surfaces. Collections were dropped on MCP import for the same reason ([#597](https://github.com/pascalorg/editor/pull/597)) by [@ShiroKSH](https://github.com/ShiroKSH) - Wall junction mitering is now deterministic for exactly-collinear walls, so identical scenes produce identical geometry regardless of node iteration order ([#596](https://github.com/pascalorg/editor/pull/596)) by [@tomatotomata](https://github.com/tomatotomata) diff --git a/README.md b/README.md index 283a0d6995..f875e63250 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Claude Code users can install the same canonical skill source as a plugin: /plugin install pascal-agent-skills@pascal ``` -The Claude plugin also supplies the local `pascal mcp connect` server. Install and start the Pascal CLI first, and keep `pascal` on the `PATH` used to launch Claude Code. This local connector needs no Pascal account or API key and does not upload projects automatically. +The Claude plugin also supplies the local `pascal mcp connect` server. Install and start the Pascal CLI first, and keep `pascal` on the `PATH` used to launch Claude Code. This local connector needs no Pascal account or API key and does not upload projects automatically. Its plugin root is this repository's `skills/` directory, so an install copies only the skill bundles and their plugin metadata rather than the repository. Claude Code 2.1.258 loads both the user-scoped `pascal` server created by `pascal mcp setup claude` and the plugin-provided server. Remove the manual entry before reloading or restarting Claude Code so only the plugin owns the connection lifecycle: diff --git a/plugin.json b/plugin.json index 730df47a9b..26656f63fb 100644 --- a/plugin.json +++ b/plugin.json @@ -15,7 +15,7 @@ "extensions": { "com.openai": { "interface": { - "displayName": "Pascal agent skills", + "displayName": "Pascal", "shortDescription": "Build 3D scenes and check fit", "longDescription": "Use Pascal's MCP tools to work with editable building scenes, validate and save results, and produce bounded furniture footprint reports with explicit evidence, limitations, and blocker-aware next actions.", "developerName": "Pascal", diff --git a/scripts/claude-mcp-config-policy.test.ts b/scripts/claude-mcp-config-policy.test.ts index b6fe2e244b..b5d1468da0 100644 --- a/scripts/claude-mcp-config-policy.test.ts +++ b/scripts/claude-mcp-config-policy.test.ts @@ -5,10 +5,10 @@ import { validateClaudeMcpPolicy } from './claude-mcp-config-policy' const repositoryRoot = resolve(import.meta.dir, '..') const canonicalConfig = JSON.parse( - readFileSync(join(repositoryRoot, '.mcp.json'), 'utf8'), + readFileSync(join(repositoryRoot, 'skills', '.mcp.json'), 'utf8'), ) as unknown const canonicalPlugin = JSON.parse( - readFileSync(join(repositoryRoot, '.claude-plugin', 'plugin.json'), 'utf8'), + readFileSync(join(repositoryRoot, 'skills', '.claude-plugin', 'plugin.json'), 'utf8'), ) as Record const marketplace = JSON.parse( readFileSync(join(repositoryRoot, '.claude-plugin', 'marketplace.json'), 'utf8'), @@ -79,8 +79,8 @@ describe('Claude plugin MCP configuration', () => { canonicalMarketplaceEntry, ), ).toEqual([ - '.mcp.json pascal server command must be pascal', - '.mcp.json pascal server args must be exactly ["mcp", "connect"]', + 'skills/.mcp.json pascal server command must be pascal', + 'skills/.mcp.json pascal server args must be exactly ["mcp", "connect"]', ]) }) diff --git a/scripts/claude-mcp-config-policy.ts b/scripts/claude-mcp-config-policy.ts index c97c40f51b..57609a1453 100644 --- a/scripts/claude-mcp-config-policy.ts +++ b/scripts/claude-mcp-config-policy.ts @@ -18,31 +18,32 @@ export function validateClaudeMcpPolicy( ): string[] { const failures: string[] = [] if (!isRecord(config) || !hasExactKeys(config, ['mcpServers'])) { - return ['.mcp.json must contain only the mcpServers object'] + return ['skills/.mcp.json must contain only the mcpServers object'] } const servers = config.mcpServers if (!isRecord(servers) || !hasExactKeys(servers, ['pascal'])) { - return ['.mcp.json must contain exactly one server named pascal'] + return ['skills/.mcp.json must contain exactly one server named pascal'] } const pascal = servers.pascal if (!isRecord(pascal) || !hasExactKeys(pascal, ['type', 'command', 'args'])) { failures.push( - '.mcp.json pascal server must contain only type, command, and args; remote or credential fields are not allowed', + 'skills/.mcp.json pascal server must contain only type, command, and args; remote or credential fields are not allowed', ) return failures } - if (pascal.type !== 'stdio') failures.push('.mcp.json pascal server type must be stdio') - if (pascal.command !== 'pascal') failures.push('.mcp.json pascal server command must be pascal') + if (pascal.type !== 'stdio') failures.push('skills/.mcp.json pascal server type must be stdio') + if (pascal.command !== 'pascal') + failures.push('skills/.mcp.json pascal server command must be pascal') if ( !Array.isArray(pascal.args) || pascal.args.length !== 2 || pascal.args[0] !== 'mcp' || pascal.args[1] !== 'connect' ) { - failures.push('.mcp.json pascal server args must be exactly ["mcp", "connect"]') + failures.push('skills/.mcp.json pascal server args must be exactly ["mcp", "connect"]') } if (!isRecord(pluginManifest)) { diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts index 8911e747c6..4ebcd5028c 100644 --- a/scripts/validate-skills.ts +++ b/scripts/validate-skills.ts @@ -285,6 +285,8 @@ for (const discoveryFailure of validatePublicSkillDiscoverySurface( } for (const entry of readdirSync(join(root, 'skills'), { withFileTypes: true })) { + // skills/ is also the Claude plugin root, so its dot-entries carry plugin metadata, not bundles. + if (entry.name.startsWith('.')) continue if (entry.isDirectory() && !skillNames.includes(entry.name as (typeof skillNames)[number])) { fail(`OpenAI skills directory contains an unexpected non-skill directory: ${entry.name}`) } @@ -820,9 +822,10 @@ for (const item of publishingCases) { if (positivePublishingCases < 5) fail('Publishing suite needs at least 5 positive cases') if (negativePublishingCases < 3) fail('Publishing suite needs at least 3 negative cases') -const claudePlugin = parseJson(join(root, '.claude-plugin', 'plugin.json')) +const claudePluginRoot = join(root, 'skills') +const claudePlugin = parseJson(join(claudePluginRoot, '.claude-plugin', 'plugin.json')) const claudeMarketplace = parseJson(join(root, '.claude-plugin', 'marketplace.json')) -const claudeMcpConfig = parseJson(join(root, '.mcp.json')) +const claudeMcpConfig = parseJson(join(claudePluginRoot, '.mcp.json')) const portableMcpConfig = parseJson(join(root, 'mcp.json')) const portablePlugin = parseJson(join(root, 'plugin.json')) const codexPlugin = parseJson(join(root, '.codex-plugin', 'plugin.json')) @@ -907,7 +910,7 @@ if (Object.keys(portableMcpConfig).sort().join(',') !== '$schema,mcpServers') { fail('Portable mcp.json must contain only $schema and mcpServers') } if (canonicalJson(portableMcpConfig.mcpServers) !== canonicalJson(claudeMcpConfig.mcpServers)) { - fail('Portable mcp.json and .mcp.json must declare the same mcpServers block') + fail('Portable mcp.json and skills/.mcp.json must declare the same mcpServers block') } const portablePascalServer = (portableMcpConfig.mcpServers as Record | undefined) @@ -1097,23 +1100,30 @@ if (!Array.isArray(marketplacePlugins) || marketplacePlugins.length !== 1) { for (const configFailure of validateClaudeMcpPolicy(claudeMcpConfig, claudePlugin, plugin)) { fail(configFailure) } - if (plugin.source !== './') fail('Claude marketplace plugin must use the repository root') + // The plugin root must stay skills/: a repository-root source makes Claude Code cache the whole + // monorepo and run bun install against the root lockfile on every install. + if (plugin.source !== './skills') { + fail('Claude marketplace plugin must use the skills directory as its plugin root') + } // A listed skills array is the complete set Claude Code loads for the entry, so it must equal // every packaged bundle; a new skills//SKILL.md is otherwise installed but never loaded. - const bundledSkillPaths = readdirSync(join(root, 'skills'), { withFileTypes: true }) + const bundledSkillPaths = readdirSync(claudePluginRoot, { withFileTypes: true }) .filter( - (entry) => entry.isDirectory() && existsSync(join(root, 'skills', entry.name, 'SKILL.md')), + (entry) => entry.isDirectory() && existsSync(join(claudePluginRoot, entry.name, 'SKILL.md')), ) - .map((entry) => `./skills/${entry.name}`) + .map((entry) => `./${entry.name}`) .sort() - const packagedSkills = plugin.skills - const listedSkillPaths = Array.isArray(packagedSkills) - ? [...packagedSkills].map(String).sort() - : [] - if (listedSkillPaths.join(',') !== bundledSkillPaths.join(',')) { - fail( - `Claude marketplace skills must list exactly the packaged bundles ${bundledSkillPaths.join(', ')}; found ${listedSkillPaths.join(', ') || 'none'}`, - ) + for (const [label, descriptor] of [ + ['Claude marketplace', plugin], + ['Claude plugin manifest', claudePlugin], + ] as const) { + const declared = descriptor.skills + const declaredPaths = Array.isArray(declared) ? [...declared].map(String).sort() : [] + if (declaredPaths.join(',') !== bundledSkillPaths.join(',')) { + fail( + `${label} skills must list exactly the packaged bundles ${bundledSkillPaths.join(', ')}; found ${declaredPaths.join(', ') || 'none'}`, + ) + } } } diff --git a/.claude-plugin/plugin.json b/skills/.claude-plugin/plugin.json similarity index 86% rename from .claude-plugin/plugin.json rename to skills/.claude-plugin/plugin.json index 8d513f0ec0..73926b3e61 100644 --- a/.claude-plugin/plugin.json +++ b/skills/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin.json", "name": "pascal-agent-skills", - "displayName": "Pascal agent skills", + "displayName": "Pascal", "version": "0.1.8", "description": "Create, inspect, validate, and assess furniture layouts with bounded next actions in Pascal through MCP.", "author": { @@ -12,5 +12,6 @@ "homepage": "https://editor.pascal.app/docs/developers/mcp", "repository": "https://github.com/pascalorg/editor", "license": "MIT", - "keywords": ["pascal", "3d", "architecture", "mcp", "furniture", "spatial"] + "keywords": ["pascal", "3d", "architecture", "mcp", "furniture", "spatial"], + "skills": ["./pascal-3d", "./furniture-fit"] } diff --git a/.mcp.json b/skills/.mcp.json similarity index 100% rename from .mcp.json rename to skills/.mcp.json diff --git a/skills/README.md b/skills/README.md index 31fd209ecf..0548e8a221 100644 --- a/skills/README.md +++ b/skills/README.md @@ -70,13 +70,13 @@ codex plugin marketplace add pascalorg/editor codex plugin add pascal-agent-skills@pascal ``` -The Claude plugin installs the instructions from the canonical `skills/` directory and supplies one local stdio server that runs `pascal mcp connect`. Install and start the Pascal CLI first, and keep `pascal` on Claude Code's `PATH`. The bundled local connector needs no Pascal account or API key and does not upload projects automatically. Codex and individually installed skills still use the setup reference included in either skill. +The Claude plugin installs the instructions from the canonical `skills/` directory and supplies one local stdio server that runs `pascal mcp connect`. This `skills/` directory is itself the Claude plugin root, so an install copies only the two skill bundles and their plugin metadata rather than the repository. Install and start the Pascal CLI first, and keep `pascal` on Claude Code's `PATH`. The bundled local connector needs no Pascal account or API key and does not upload projects automatically. Codex and individually installed skills still use the setup reference included in either skill. Claude Code 2.1.258 loads both the user-scoped `pascal` server created by `pascal mcp setup claude` and the plugin-provided server. Run `claude mcp remove --scope user pascal` before reloading or restarting Claude Code so only the plugin owns the connection lifecycle. Use `/mcp` to remove or disable any project- or local-scoped Pascal connection too. Leaving both connections active violates the one-active-agent-client-per-local-service requirement. For a hosted Pascal project, disable the plugin-provided local server in `/mcp`, then configure the hosted endpoint from the setup reference. Plugin installation alone never creates an account, uploads a project, or authorizes paid work. -The root [`plugin.json`](../plugin.json) is the portable Agent Plugins manifest used for OpenAI submission, and the root [`mcp.json`](../mcp.json) is the only MCP configuration path Codex and Cursor read; Claude Code reads the same server from `.mcp.json`. The repository keeps `.codex-plugin/plugin.json` as a compatibility fallback and validates that both expose the same OpenAI listing metadata. Public-directory submission, review, and publication are separate external steps; a Git marketplace install does not make the plugin publicly listed in ChatGPT or Codex. +The root [`plugin.json`](../plugin.json) is the portable Agent Plugins manifest used for OpenAI submission, and the root [`mcp.json`](../mcp.json) is the only MCP configuration path Codex and Cursor read; Claude Code reads the same server from [`.mcp.json`](.mcp.json) in its `skills/` plugin root, next to [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json). The repository keeps `.codex-plugin/plugin.json` as a compatibility fallback and validates that both expose the same OpenAI listing metadata. Public-directory submission, review, and publication are separate external steps; a Git marketplace install does not make the plugin publicly listed in ChatGPT or Codex. The npm `beta` CLI remains on the older runtime contract. For the read-only `check_collisions.candidate` capability used by the current furniture workflow, follow the checksum-verified [GitHub preview instructions](pascal-3d/references/setup.md#verified-github-preview). The preview archive is published on GitHub, not npm. @@ -111,4 +111,4 @@ bun run skills:validate Run `claude plugin validate . --strict` manually as well. It stays out of the script and out of CI because it needs the Claude Code CLI, which is not installed on every runner. -The repository validator checks the exact two-skill public discovery surface, keeps contributor-only workflows internal, and checks frontmatter, semantic skill versions, bundled links and their heading anchors, task and trigger fixtures, semantic furniture next-action decision cases, scoped ClawHub ignore policies without re-inclusion overrides, the exact credential-free Claude local MCP configuration, an identical `mcp.json` server block for Codex and Cursor, a Gemini CLI manifest that runs the same command at the same version, a Claude marketplace skills list that equals the packaged bundles exactly, the publishing suite, the exact 46-tool OpenAI annotation and justification packet, portable and compatibility manifest consistency with one plugin version, description, and author across every plugin descriptor, OpenAI public-directory metadata limits including its documented interface fields, bundled branding assets, and accidental private-path or credential leakage. +The repository validator checks the exact two-skill public discovery surface, keeps contributor-only workflows internal, and checks frontmatter, semantic skill versions, bundled links and their heading anchors, task and trigger fixtures, semantic furniture next-action decision cases, scoped ClawHub ignore policies without re-inclusion overrides, the exact credential-free Claude local MCP configuration, an identical `mcp.json` server block for Codex and Cursor, a Gemini CLI manifest that runs the same command at the same version, Claude marketplace and plugin skills lists that equal the packaged bundles exactly, the publishing suite, the exact 46-tool OpenAI annotation and justification packet, portable and compatibility manifest consistency with one plugin version, description, and author across every plugin descriptor, OpenAI public-directory metadata limits including its documented interface fields, bundled branding assets, and accidental private-path or credential leakage.