From 0e4407b4b2c83c794753b7f5ff924a2d9be165bd Mon Sep 17 00:00:00 2001 From: Joel Horwitz Date: Thu, 24 Sep 2026 12:22:43 -0700 Subject: [PATCH] chore(cursor): prep Cursor Marketplace submission (relative logo, no 'free' copy, real root plugin.json) --- .cursor-plugin/marketplace.json | 2 +- .cursor-plugin/plugin.json | 4 +-- CHANGELOG.md | 8 +++++ README.md | 5 ++-- plugin.json | 29 +++++++++++++++++- scripts/validate-manifests.js | 52 +++++++++++++++++++++++++++++++++ 6 files changed, 94 insertions(+), 6 deletions(-) mode change 120000 => 100644 plugin.json diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 1f2df9c..b41fc54 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -12,7 +12,7 @@ { "name": "synter", "source": ".", - "logo": "https://raw.githubusercontent.com/Synter-Media-AI/plugin/main/assets/logo.png", + "logo": "assets/logo.png", "description": "The AI Agent Operator for Ads. Connect 21 ad platforms, build audiences, generate on-brand creative, launch cross-platform campaigns, and reallocate spend by ROAS — one conversation, every platform. You direct, the agents execute. Nothing that spends money ships without your approval." } ] diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index b1e2018..5a951e9 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -11,7 +11,7 @@ "homepage": "https://syntermedia.ai", "repository": "https://github.com/Synter-Media-AI/plugin", "license": "MIT", - "logo": "https://raw.githubusercontent.com/Synter-Media-AI/plugin/main/assets/logo.png", + "logo": "assets/logo.png", "keywords": [ "ads", "advertising", @@ -36,7 +36,7 @@ "SYNTER_API_KEY": { "type": "string", "title": "Synter API key", - "description": "Optional for onboarding. Paste your syn_... key from syntermedia.ai/developer to use connected ad accounts. Leave blank to run /synter:quickstart and the free GA4 tools." + "description": "Optional for onboarding. Paste your syn_... key from syntermedia.ai/developer to use connected ad accounts. Leave blank to run /synter:quickstart and the GA4 and onboarding tools first; paid actions need Solo ($20/mo) or credits." } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 631fd87..0659917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the Synter plugin are documented here. This project follows [semantic versioning](https://semver.org). +## [Unreleased] + +### Changed +- Cursor Marketplace submission prep: `logo` in `.cursor-plugin/plugin.json` and `.cursor-plugin/marketplace.json` is now the repo-relative `assets/logo.png` (Cursor resolves it to a commit-pinned raw.githubusercontent URL), per the submission checklist. +- Removed "free" wording from the Cursor `SYNTER_API_KEY` description and README; paid actions are framed as Solo ($20/mo) or credits. +- Root `plugin.json` is now a real Agent Plugins 1.0.0 manifest instead of a symlink to the Claude manifest (the symlink failed the Agent Plugins schema and breaks on symlink-less checkouts). +- `scripts/validate-manifests.js` guards the root manifest, relative Cursor logo paths, and "free" wording in Cursor listing copy. + ## [1.1.2] — 2026-09-06 ### Fixed diff --git a/README.md b/README.md index 2d6f775..dc9f017 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Official install documentation: [Use plugins in Claude](https://support.claude.c After install, set `SYNTER_API_KEY` in **Plugins → Configure**. The hosted MCP is `https://mcp.syntermedia.ai`. -Free GA4 and onboarding tools work with no key and no credits. +GA4 and onboarding tools work before you add a key; paid actions need Solo ($20/mo) or credits. > Campaign write actions spend real money — every spend asks for your approval first. @@ -134,6 +134,7 @@ plugin/ ├── .cursor-plugin/ │ ├── plugin.json # Cursor Plugin manifest + SYNTER_API_KEY variable │ └── marketplace.json # Cursor Team Marketplace index +├── plugin.json # Agent Plugins 1.0.0 manifest (open standard; real file, not a symlink) ├── .mcp.json # Synter remote MCP for Claude (browser OAuth) ├── mcp.json # Synter MCP for Cursor (header: ${SYNTER_API_KEY}) ├── skills/ # /synter:* slash commands (SKILL.md each) @@ -210,7 +211,7 @@ and store it in the client's secret configuration, never in chat. ## Safety -Your agent can create campaigns, change budgets, and pause spend. The plugin defaults to **recommend-then-execute** and asks for explicit approval before anything spends money. It confirms the org/account before any write, uses only the real IDs the platform returns, and guards against fat-finger budgets. Reads are always free to run. +Your agent can create campaigns, change budgets, and pause spend. The plugin defaults to **recommend-then-execute** and asks for explicit approval before anything spends money. It confirms the org/account before any write, uses only the real IDs the platform returns, and guards against fat-finger budgets. Reads (lists, reports, pulls) never need spend approval. --- diff --git a/plugin.json b/plugin.json deleted file mode 120000 index e22f3da..0000000 --- a/plugin.json +++ /dev/null @@ -1 +0,0 @@ -.claude-plugin/plugin.json \ No newline at end of file diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..287b6ff --- /dev/null +++ b/plugin.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "synter", + "version": "1.1.2", + "description": "The AI Agent Operator for Ads. Connect every ad platform, build audiences from signals and first-party data, generate on-brand creative, launch cross-platform campaigns, and reallocate spend by ROAS — all in one conversation. You direct, the agents execute. Nothing ships without your approval.", + "author": { + "name": "Synter", + "email": "joel@synterai.com", + "url": "https://syntermedia.ai" + }, + "homepage": "https://syntermedia.ai", + "repository": "https://github.com/Synter-Media-AI/plugin", + "license": "MIT", + "keywords": [ + "ads", + "advertising", + "campaigns", + "audiences", + "media-buying", + "creative", + "roas", + "google-ads", + "meta-ads", + "linkedin-ads", + "mcp", + "synter" + ] +} diff --git a/scripts/validate-manifests.js b/scripts/validate-manifests.js index ec87308..2c88dc3 100644 --- a/scripts/validate-manifests.js +++ b/scripts/validate-manifests.js @@ -32,6 +32,7 @@ const MANIFEST_PATHS = [ '.cursor-plugin/marketplace.json', '.mcp.json', 'mcp.json', + 'plugin.json', ]; const manifests = {}; @@ -125,6 +126,57 @@ if (pluginJson && pluginJson.userConfig && pluginJson.userConfig.synter_api_key) errors.push('.claude-plugin/plugin.json: Claude plugin must not collect a static Synter API key'); } +// (e) Root plugin.json is an Agent Plugins 1.0.0 manifest (Cursor detects it +// as a second plugin format). Keep it a real file — symlinks break on Windows +// checkouts and some marketplace crawlers — and keep it schema-clean. +const AGENT_PLUGIN_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json'; +const AGENT_PLUGIN_KEYS = new Set([ + '$schema', 'name', 'version', 'description', 'author', 'homepage', + 'repository', 'license', 'keywords', 'extensions', +]); +if (fs.lstatSync(path.join(REPO_ROOT, 'plugin.json')).isSymbolicLink()) { + errors.push('plugin.json: must be a regular file, not a symlink'); +} +const rootPluginJson = manifests['plugin.json']; +if (rootPluginJson) { + if (rootPluginJson.$schema !== AGENT_PLUGIN_SCHEMA) { + errors.push(`plugin.json: $schema must be ${AGENT_PLUGIN_SCHEMA}`); + } + for (const key of Object.keys(rootPluginJson)) { + if (!AGENT_PLUGIN_KEYS.has(key)) { + errors.push(`plugin.json: "${key}" is not an Agent Plugins 1.0.0 manifest field`); + } + } + if (pluginJson && rootPluginJson.name !== pluginJson.name) { + errors.push('plugin.json: name does not match .claude-plugin/plugin.json'); + } + if (pluginJson && rootPluginJson.version !== pluginJson.version) { + errors.push('plugin.json: version does not match .claude-plugin/plugin.json'); + } +} + +// (f) Cursor Marketplace checklist: logo committed and referenced by a +// relative path that exists in the repo. +for (const [relPath, logo] of [ + ['.cursor-plugin/plugin.json', manifests['.cursor-plugin/plugin.json'] && manifests['.cursor-plugin/plugin.json'].logo], + ['.cursor-plugin/marketplace.json', (manifests['.cursor-plugin/marketplace.json'] && manifests['.cursor-plugin/marketplace.json'].plugins || [])[0] && manifests['.cursor-plugin/marketplace.json'].plugins[0].logo], +]) { + if (!logo) continue; + if (/^[a-z]+:\/\//i.test(logo) || logo.startsWith('/') || logo.includes('..')) { + errors.push(`${relPath}: logo must be a relative repo path (got "${logo}")`); + } else if (!fs.existsSync(path.join(REPO_ROOT, logo))) { + errors.push(`${relPath}: logo "${logo}" does not exist`); + } +} + +// (g) Listing copy: no "free" framing in Cursor-facing manifest text (pricing +// is Solo $20/mo or credits). +for (const relPath of ['.cursor-plugin/plugin.json', '.cursor-plugin/marketplace.json']) { + if (/\bfree\b/i.test(readFile(relPath))) { + errors.push(`${relPath}: remove "free" wording from listing copy`); + } +} + const mcpJson = readFile('mcp.json'); if (!mcpJson.includes('SYNTER_API_KEY')) { errors.push('mcp.json: missing required literal "SYNTER_API_KEY"');