From 7ee8d87752fb23248fb63a7aaac002707e9d6d94 Mon Sep 17 00:00:00 2001 From: Yonathan Morales Date: Tue, 29 Sep 2026 06:32:10 +0000 Subject: [PATCH 1/2] feat: split coordinator into core, plugin, and MCP packages Core owns the Cloud client and CLI. The OpenClaw plugin and the MCP stdio server depend on it, so the same tools can ship without sharing a host runtime. Co-authored-by: AIPAL GROK (4.6) Co-authored-by: Cursor --- .gitignore | 1 + CHANGELOG.md | 5 + CONTRIBUTING.md | 30 ++--- README.md | 117 ++++++++---------- SECURITY.md | 2 +- package-lock.json | 83 +++++++++++-- package.json | 69 ++--------- packages/core/README.md | 5 + .../core/examples}/cli-config.json | 0 packages/core/package.json | 33 +++++ {src => packages/core/src}/actions.test.ts | 0 {src => packages/core/src}/actions.ts | 0 {src => packages/core/src}/artifacts.ts | 0 {src => packages/core/src}/brief.test.ts | 0 {src => packages/core/src}/brief.ts | 0 {src => packages/core/src}/cli.ts | 25 +--- {src => packages/core/src}/client.test.ts | 0 {src => packages/core/src}/client.ts | 0 {src => packages/core/src}/config.ts | 0 .../core/src}/env-catalog.test.ts | 0 {src => packages/core/src}/env-catalog.ts | 0 {src => packages/core/src}/errors.ts | 0 {src => packages/core/src}/harvest.ts | 22 ---- {src => packages/core/src}/host.test.ts | 0 {src => packages/core/src}/host.ts | 0 {src => packages/core/src}/ids.ts | 0 packages/core/src/index.ts | 42 +++++++ {src => packages/core/src}/ledger.test.ts | 0 {src => packages/core/src}/ledger.ts | 0 {src => packages/core/src}/load-config.ts | 0 {src => packages/core/src}/lock.ts | 0 {src => packages/core/src}/model.test.ts | 0 {src => packages/core/src}/model.ts | 0 {src => packages/core/src}/notify.test.ts | 0 {src => packages/core/src}/notify.ts | 0 {src => packages/core/src}/paths.ts | 0 {src => packages/core/src}/placement.test.ts | 0 {src => packages/core/src}/placement.ts | 0 {src => packages/core/src}/registry.test.ts | 0 {src => packages/core/src}/registry.ts | 0 {src => packages/core/src}/runtime.ts | 2 +- {src => packages/core/src}/sse.test.ts | 0 {src => packages/core/src}/sse.ts | 0 {src => packages/core/src}/tool-schemas.ts | 0 {src => packages/core/src}/transcript.ts | 0 {src => packages/core/src}/types.ts | 0 {src => packages/core/src}/version.ts | 2 +- {src => packages/core/src}/waiter.test.ts | 0 {src => packages/core/src}/waiter.ts | 0 .../core/src}/watch-process.test.ts | 0 {src => packages/core/src}/watch-process.ts | 0 packages/core/tsconfig.json | 15 +++ packages/mcp/README.md | 18 +++ {examples => packages/mcp/examples}/mcp.json | 3 +- packages/mcp/package.json | 36 ++++++ packages/mcp/src/cli.ts | 7 ++ packages/mcp/src/index.ts | 1 + {src => packages/mcp/src}/mcp.test.ts | 5 +- {src => packages/mcp/src}/mcp.ts | 29 +++-- packages/mcp/src/version.ts | 6 + packages/mcp/tsconfig.json | 15 +++ packages/plugin/README.md | 11 ++ .../plugin/examples}/gateway-plugin.json | 0 .../examples}/instance-enable.batch.json | 0 .../plugin/openclaw.plugin.json | 2 +- packages/plugin/package.json | 66 ++++++++++ packages/plugin/scripts/install-gateway.sh | 11 ++ .../scripts}/preserve-manifest-skills.mjs | 0 .../plugin/skills}/cursor-cloud/SKILL.md | 0 .../skills}/cursor-cloud/references/smoke.md | 0 packages/plugin/src/cli.ts | 61 +++++++++ packages/plugin/src/harvest.ts | 28 +++++ {src => packages/plugin/src}/index.test.ts | 0 packages/plugin/src/index.ts | 22 ++++ {src => packages/plugin/src}/plugin.ts | 30 +++-- {src => packages/plugin/src}/setup.test.ts | 0 {src => packages/plugin/src}/setup.ts | 0 packages/plugin/src/version.ts | 6 + packages/plugin/tsconfig.json | 15 +++ scripts/install-gateway.sh | 11 +- scripts/release.sh | 48 +++++-- src/index.ts | 25 ---- tsconfig.json | 19 +-- vitest.config.ts | 8 +- 84 files changed, 638 insertions(+), 298 deletions(-) create mode 100644 packages/core/README.md rename {examples => packages/core/examples}/cli-config.json (100%) create mode 100644 packages/core/package.json rename {src => packages/core/src}/actions.test.ts (100%) rename {src => packages/core/src}/actions.ts (100%) rename {src => packages/core/src}/artifacts.ts (100%) rename {src => packages/core/src}/brief.test.ts (100%) rename {src => packages/core/src}/brief.ts (100%) rename {src => packages/core/src}/cli.ts (91%) rename {src => packages/core/src}/client.test.ts (100%) rename {src => packages/core/src}/client.ts (100%) rename {src => packages/core/src}/config.ts (100%) rename {src => packages/core/src}/env-catalog.test.ts (100%) rename {src => packages/core/src}/env-catalog.ts (100%) rename {src => packages/core/src}/errors.ts (100%) rename {src => packages/core/src}/harvest.ts (69%) rename {src => packages/core/src}/host.test.ts (100%) rename {src => packages/core/src}/host.ts (100%) rename {src => packages/core/src}/ids.ts (100%) create mode 100644 packages/core/src/index.ts rename {src => packages/core/src}/ledger.test.ts (100%) rename {src => packages/core/src}/ledger.ts (100%) rename {src => packages/core/src}/load-config.ts (100%) rename {src => packages/core/src}/lock.ts (100%) rename {src => packages/core/src}/model.test.ts (100%) rename {src => packages/core/src}/model.ts (100%) rename {src => packages/core/src}/notify.test.ts (100%) rename {src => packages/core/src}/notify.ts (100%) rename {src => packages/core/src}/paths.ts (100%) rename {src => packages/core/src}/placement.test.ts (100%) rename {src => packages/core/src}/placement.ts (100%) rename {src => packages/core/src}/registry.test.ts (100%) rename {src => packages/core/src}/registry.ts (100%) rename {src => packages/core/src}/runtime.ts (92%) rename {src => packages/core/src}/sse.test.ts (100%) rename {src => packages/core/src}/sse.ts (100%) rename {src => packages/core/src}/tool-schemas.ts (100%) rename {src => packages/core/src}/transcript.ts (100%) rename {src => packages/core/src}/types.ts (100%) rename {src => packages/core/src}/version.ts (85%) rename {src => packages/core/src}/waiter.test.ts (100%) rename {src => packages/core/src}/waiter.ts (100%) rename {src => packages/core/src}/watch-process.test.ts (100%) rename {src => packages/core/src}/watch-process.ts (100%) create mode 100644 packages/core/tsconfig.json create mode 100644 packages/mcp/README.md rename {examples => packages/mcp/examples}/mcp.json (65%) create mode 100644 packages/mcp/package.json create mode 100644 packages/mcp/src/cli.ts create mode 100644 packages/mcp/src/index.ts rename {src => packages/mcp/src}/mcp.test.ts (89%) rename {src => packages/mcp/src}/mcp.ts (96%) create mode 100644 packages/mcp/src/version.ts create mode 100644 packages/mcp/tsconfig.json create mode 100644 packages/plugin/README.md rename {examples => packages/plugin/examples}/gateway-plugin.json (100%) rename {examples => packages/plugin/examples}/instance-enable.batch.json (100%) rename openclaw.plugin.json => packages/plugin/openclaw.plugin.json (99%) create mode 100644 packages/plugin/package.json create mode 100755 packages/plugin/scripts/install-gateway.sh rename {scripts => packages/plugin/scripts}/preserve-manifest-skills.mjs (100%) rename {skills => packages/plugin/skills}/cursor-cloud/SKILL.md (100%) rename {skills => packages/plugin/skills}/cursor-cloud/references/smoke.md (100%) create mode 100644 packages/plugin/src/cli.ts create mode 100644 packages/plugin/src/harvest.ts rename {src => packages/plugin/src}/index.test.ts (100%) create mode 100644 packages/plugin/src/index.ts rename {src => packages/plugin/src}/plugin.ts (96%) rename {src => packages/plugin/src}/setup.test.ts (100%) rename {src => packages/plugin/src}/setup.ts (100%) create mode 100644 packages/plugin/src/version.ts create mode 100644 packages/plugin/tsconfig.json delete mode 100644 src/index.ts diff --git a/.gitignore b/.gitignore index 63be609..4a08b30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ dist/ +packages/*/dist coverage/ *.tgz .env diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc791c..0840bfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.3.0 + +- Split into three packages: `cursor-cloud-core`, `openclaw-plugin-cursor-cloud`, and `cursor-cloud-mcp`. +- MCP is a standalone stdio binary (`cursor-cloud-mcp`). The OpenClaw plugin remains the gateway door. + ## 0.2.0 - `cursor_cloud_launch` accepts `effort` (`low|med|high|xhigh`) and `fast` and sends them as `model.params`. `med` maps to Cursor `medium`; Grok 4.7 uses `reasoning_effort`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a79312e..ce1439f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,19 +4,13 @@ | Path | Role | | --- | --- | -| `src/client.ts` | Cursor Cloud Agents API v1 | -| `src/registry.ts` | Env ids → `env` payload; base vs project allocation | -| `src/ledger.ts` | Local `agents.json` of launched `bc-…` | -| `src/env-catalog.ts` | Harvest named envs from `GET /v1/agents` | -| `src/harvest.ts` | Gateway init harvest service | -| `src/placement.ts` | Fresh vs reuse; session-bound agents | -| `src/actions.ts` | Shared launch/reply/status/cancel/watch | -| `src/plugin.ts` | OpenClaw tools | -| `src/cli.ts` / `src/mcp.ts` | Other surfaces | -| `src/setup.ts` | One-command gateway install | -| `skills/cursor-cloud/` | Agent door and first-proof | -| `examples/gateway-plugin.json` | Gateway plugin config | -| `examples/cli-config.json` | CLI config | +| `packages/core` | `cursor-cloud-core` — API client, harvest, actions, CLI | +| `packages/plugin` | `openclaw-plugin-cursor-cloud` — OpenClaw tools, skill, setup | +| `packages/mcp` | `cursor-cloud-mcp` — stdio MCP server | +| `packages/core/src/client.ts` | Cursor Cloud Agents API v1 | +| `packages/core/src/actions.ts` | launch/reply/status/cancel/watch | +| `packages/plugin/src/plugin.ts` | `defineToolPlugin` | +| `packages/mcp/src/mcp.ts` | JSON-RPC tools/list + tools/call | Re-read [Cursor Cloud Agents API](https://cursor.com/docs/cloud-agent/api/endpoints) when changing the client. Do not copy community MCP source into this tree. @@ -25,19 +19,20 @@ Re-read [Cursor Cloud Agents API](https://cursor.com/docs/cloud-agent/api/endpoi ```bash npm install npm test +npm run build npm run plugin:build npm run plugin:validate ``` -Commit the generated `openclaw.plugin.json` when tool names or config schema change. +Commit `packages/plugin/openclaw.plugin.json` when tool names or config schema change. ## Release 1. `./scripts/release.sh ` 2. Commit the version bump (`chore: release `). -3. Tag `v` and push the tag (publishes npm when `NPM_TOKEN` is set). -4. `clawhub package publish .` when the ClawHub owner is ready. -5. After ClawHub exists, set `package.json#openclaw.install.clawhubSpec`. +3. Tag `v` and push the tag. +4. Publish **core**, then **mcp**, then **plugin** (`npm publish --access public` in each package). +5. `clawhub package publish packages/plugin` when the ClawHub owner is ready. ## Rules that should not regress @@ -48,3 +43,4 @@ Commit the generated `openclaw.plugin.json` when tool names or config schema cha 5. Do not add archive/delete/artifact tools to the default set. 6. Operator and skill first proof stay `cursor_cloud_me` then a `bc-…`. 7. Launched-agent JSON stays off git (`agents.json` / `state/`). +8. Core has no OpenClaw or MCP dependency. Plugin and MCP both depend on core only. diff --git a/README.md b/README.md index eaf9211..1398073 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,89 @@ -# openclaw-plugin-cursor-cloud +# cursor-cloud-agent -OpenClaw plugin that delegates coding work to **Cursor Cloud Agents on named saved environments**. Also a CLI and stdio MCP server. +Three packages, one coordinator. Coding work runs on **Cursor Cloud Agents** (`bc-…`), not on the host. -**Door:** `cursor_cloud_*` tools and the `cursor-cloud` skill. -**Implementer:** a Cloud agent (`bc-…`). -**Not the door:** `agent -p` (runs here), or `repos` together with a named cloud env. +| Package | npm | Role | +| --- | --- | --- | +| `packages/core` | `cursor-cloud-core` | API client, harvest, launch/reply/status/watch, `cursor-cloud` CLI | +| `packages/plugin` | `openclaw-plugin-cursor-cloud` | OpenClaw `cursor_cloud_*` tools + skill | +| `packages/mcp` | `cursor-cloud-mcp` | Stdio MCP server for the same tools | -Official API: [Cloud Agents API v1](https://cursor.com/docs/cloud-agent/api/endpoints). Host pin: `https://api.cursor.com`. Key: `CURSOR_API_KEY` on the gateway environment, never in git. +**Not the door:** `agent -p` on the coordinator, or `repos` together with a named cloud env. + +Official API: [Cloud Agents API v1](https://cursor.com/docs/cloud-agent/api/endpoints). Host pin: `https://api.cursor.com`. Key: `CURSOR_API_KEY`, never in git. ## You are done when -1. `cursor_cloud_me` (or `openclaw-cursor-cloud me`) returns `ok: true` and a key name. -2. A **new** chat can see `cursor_cloud_launch`. -3. `cursor_cloud_envs` lists a **base** env (clone the target) and any **project** envs (repos already loaded). -4. `cursor_cloud_launch` with a listed id returns `agent.id` (`bc-…`) and `agent.url`. A later chat reads `cursor_cloud_agents` and replies on that id. +1. `cursor_cloud_me` (or `cursor-cloud me`) returns `ok: true` and a key name. +2. A **new** OpenClaw chat can see `cursor_cloud_launch`, **or** an MCP client lists the same tools. +3. `cursor_cloud_envs` lists a **base** env and any **project** envs. +4. Launch returns `agent.id` (`bc-…`) and `agent.url`. Follow-ups use `cursor_cloud_reply`. 5. A finished run is judged by `proof.prUrls` (or an exact blocker). `IDLE` only means follow-ups are accepted. -## Install - -One command after npm publish: - -```bash -npx -y openclaw-plugin-cursor-cloud@0.2.0 setup -``` - -From this checkout: +## OpenClaw plugin ```bash +npx -y openclaw-plugin-cursor-cloud@0.3.0 setup +# from this checkout: ./scripts/install-gateway.sh ``` -`setup` installs the plugin, enables it, enables the `cursor-cloud` skill, and adds `cursor-cloud` to `tools.alsoAllow`. It does not write keys. - -Then: +`setup` links **this repo’s `packages/plugin`**, enables the plugin and `cursor-cloud` skill, and adds `cursor-cloud` to `tools.alsoAllow`. It does not write keys. ```bash -# gateway EnvironmentFile — not openclaw.json -CURSOR_API_KEY=... +CURSOR_API_KEY=... # gateway EnvironmentFile — not openclaw.json ``` -Restart the gateway. Open a new chat. Run the first proof above. - -Pinned install without `npx` (still needs the key, env registry, restart, new chat): +Restart the gateway. Open a **new** chat. ```bash -openclaw plugins install npm:openclaw-plugin-cursor-cloud@0.2.0 --force --accept-capabilities +openclaw plugins install npm:openclaw-plugin-cursor-cloud@0.3.0 --force --accept-capabilities ``` -After a ClawHub publish, use `clawhub:/openclaw-plugin-cursor-cloud` the same way. - -## Register an environment - -The model passes a **registry id** (`env: "payments"`), never an invented `{ type, name }`. - -- **base** — bootstrap env. Clone the target when it is not the env primary. -- **project** — repos already loaded and prepared. Set `allowRepos` to those URLs and a `project` key. +Gateway config: merge `packages/plugin/examples/gateway-plugin.json` into `plugins.entries.cursor-cloud`. -Plugin init (gateway startup service) harvests named environments from documented `GET /v1/agents` + `GET /v1/agents/{id}` into `~/.local/state/openclaw-cursor-cloud/envs.json`. Official v1 has no environment-list route. Unnamed dashboard fallbacks cannot be launched by `env.name` and are skipped. `cursor_cloud_envs` reads that catalog; `refresh: true` only forces a new harvest. +## MCP server -`cursor_cloud_launch` is the placement operation. OpenClaw gives the plugin `sessionId` (new on `/new` and `/reset`, kept across compact). If this chat is already bound to a `bc-…`, launch reuses it. If the user was not explicit, launch returns `phase: choose` — ask that tree, then recall with the option's `recall` fields. The CLI skips the ask and uses `defaultEnv`. - -Gateway: merge `examples/gateway-plugin.json` into `plugins.entries.cursor-cloud`. -CLI: copy `examples/cli-config.json` to `~/.config/openclaw-cursor-cloud/config.json`. -Allowlist only: `examples/instance-enable.batch.json`. - -Named `cloud` envs omit `repos` on the wire even when `allowRepos` lists a URL. - -Launched `bc-…` ids are written to `~/.local/state/openclaw-cursor-cloud/agents.json` (or `$XDG_STATE_HOME/...`). That file is not git. Override with `ledgerPath` (for example `state/agents.json` in a checkout — `state/` is gitignored). - -## Tools - -Always on: `cursor_cloud_launch`, `cursor_cloud_reply`, `cursor_cloud_status`, `cursor_cloud_cancel`, `cursor_cloud_watch`, `cursor_cloud_me`, `cursor_cloud_envs`, `cursor_cloud_agents`, `cursor_cloud_models`. - -Optional until `setup` / `tools.alsoAllow`: `cursor_cloud_list`. - -`cursor_cloud_status` returns final/partial result, `messages[]`, and short-lived artifact download refs. It does not download artifact bytes onto the coordinator. +```json +{ + "mcpServers": { + "cursor-cloud": { + "command": "cursor-cloud-mcp", + "env": { "CURSOR_API_KEY": "${CURSOR_API_KEY}" } + } + } +} +``` -Not shipped: archive, delete, GitHub repository listing. +See `packages/mcp/examples/mcp.json`. Same config file as the CLI: `CURSOR_CLOUD_CONFIG` or `~/.config/openclaw-cursor-cloud/config.json` (`packages/core/examples/cli-config.json`). -When to call which: `skills/cursor-cloud/SKILL.md`. +`openclaw-cursor-cloud mcp` still delegates to `cursor-cloud-mcp`. -## CLI and MCP +## CLI ```bash export CURSOR_API_KEY=... -openclaw-cursor-cloud me -openclaw-cursor-cloud envs --refresh -openclaw-cursor-cloud agents -openclaw-cursor-cloud launch --env base --prompt "Smoke the env, then stop." -openclaw-cursor-cloud reply --agent-id bc-… --prompt "Continue on the same branch." -openclaw-cursor-cloud mcp +cursor-cloud me +cursor-cloud envs --refresh +cursor-cloud launch --env base --prompt "Smoke the env, then stop." +cursor-cloud reply --agent-id bc-… --prompt "Continue on the same branch." ``` -MCP (`examples/mcp.json`) uses the same CLI config as above unless `CURSOR_CLOUD_CONFIG` is set. +Launch: `model` + optional `effort` (`low|med|high|xhigh`) + optional `fast`. `med` is sent as Cursor `medium`. Reply cannot change those (`model_locked`). + +## Environments and tools + +The model passes a **registry id** (`env: "payments"`), never an invented `{ type, name }`. Harvest (plugin init or `envs --refresh`) fills named envs from `GET /v1/agents`. Unnamed dashboard envs are skipped. + +Always-on tools: launch, reply, status, cancel, watch, me, envs, agents, models. Optional: list. -Watch notify: the waiter injects `openclaw agent --session-key` / `--session-id` for the originating chat. Override with `watch.notifyCommand`. Safe placeholders: `{agentId} {runId} {runStatus} {url} {prUrl}`. Result text stays in `CURSOR_CLOUD_RESULT`, not the command line. Watch is not the transcript — poll `cursor_cloud_status`. +`cursor_cloud_status` returns final/partial result, `messages[]`, and short-lived artifact download refs. Watch is not the transcript. -Launch: `model` + optional `effort` (`low|med|high|xhigh`) + optional `fast`. `med` is sent as Cursor `medium`. Reply cannot change those (`model_locked`). Default when `effort`/`fast` are omitted: Cursor's own variant (often fast). +Skill: `packages/plugin/skills/cursor-cloud/SKILL.md`. ## Versioning -`package.json` and `openclaw.plugin.json` share the semver. Authors: `CONTRIBUTING.md`. +Workspace packages share the semver. `./scripts/release.sh ` bumps all three. Authors: `CONTRIBUTING.md`. ## License diff --git a/SECURITY.md b/SECURITY.md index 8a890e6..04edd73 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -29,7 +29,7 @@ auth bypass, or host-pinning failure. ## Default blast radius -Default tools: launch, reply, status, cancel, watch, me, envs, agents, models. +Packages: `cursor-cloud-core`, `openclaw-plugin-cursor-cloud`, `cursor-cloud-mcp`. Same default tools on plugin and MCP. Optional tools (must be allowlisted): list. diff --git a/package-lock.json b/package-lock.json index d998e36..319dddb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,18 @@ { - "name": "openclaw-plugin-cursor-cloud", - "version": "0.2.0", + "name": "cursor-cloud-agent", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "openclaw-plugin-cursor-cloud", - "version": "0.2.0", + "name": "cursor-cloud-agent", + "version": "0.3.0", "license": "MIT", - "dependencies": { - "typebox": "^1.1.38" - }, - "bin": { - "openclaw-cursor-cloud": "dist/cli.js" - }, + "workspaces": [ + "packages/core", + "packages/plugin", + "packages/mcp" + ], "devDependencies": { "@types/node": "^24.5.2", "openclaw": "2026.9.2", @@ -22,9 +21,6 @@ }, "engines": { "node": ">=22.22.3" - }, - "peerDependencies": { - "openclaw": ">=2026.5.17" } }, "node_modules/@agentclientprotocol/sdk": { @@ -2865,6 +2861,14 @@ "dev": true, "license": "MIT" }, + "node_modules/cursor-cloud-core": { + "resolved": "packages/core", + "link": true + }, + "node_modules/cursor-cloud-mcp": { + "resolved": "packages/mcp", + "link": true + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -4852,6 +4856,10 @@ "sqlite-vec": "0.1.9" } }, + "node_modules/openclaw-plugin-cursor-cloud": { + "resolved": "packages/plugin", + "link": true + }, "node_modules/openclaw/node_modules/typebox": { "version": "1.3.18", "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.18.tgz", @@ -6684,6 +6692,55 @@ "peerDependencies": { "zod": "^3.25.28 || ^4" } + }, + "packages/core": { + "name": "cursor-cloud-core", + "version": "0.3.0", + "license": "MIT", + "dependencies": { + "typebox": "^1.1.38" + }, + "bin": { + "cursor-cloud": "dist/cli.js" + }, + "engines": { + "node": ">=22.22.3" + } + }, + "packages/mcp": { + "name": "cursor-cloud-mcp", + "version": "0.3.0", + "license": "MIT", + "dependencies": { + "cursor-cloud-core": "0.3.0" + }, + "bin": { + "cursor-cloud-mcp": "dist/cli.js" + }, + "engines": { + "node": ">=22.22.3" + } + }, + "packages/plugin": { + "name": "openclaw-plugin-cursor-cloud", + "version": "0.3.0", + "license": "MIT", + "dependencies": { + "cursor-cloud-core": "0.3.0", + "cursor-cloud-mcp": "0.3.0" + }, + "bin": { + "openclaw-cursor-cloud": "dist/cli.js" + }, + "devDependencies": { + "openclaw": "2026.9.2" + }, + "engines": { + "node": ">=22.22.3" + }, + "peerDependencies": { + "openclaw": ">=2026.5.17" + } } } } diff --git a/package.json b/package.json index 079c2ff..1e83b6c 100644 --- a/package.json +++ b/package.json @@ -1,76 +1,29 @@ { - "name": "openclaw-plugin-cursor-cloud", - "version": "0.2.0", - "description": "OpenClaw plugin and CLI that launch Cursor Cloud Agents on named saved environments — without running the coding work on the coordinator host.", + "name": "cursor-cloud-agent", + "version": "0.3.0", + "private": true, + "description": "Monorepo for Cursor Cloud coordinator: core lib, OpenClaw plugin, MCP server.", "type": "module", "license": "MIT", - "keywords": [ - "openclaw", - "plugin", - "cursor", - "cloud-agents", - "clawhub", - "mcp" + "workspaces": [ + "packages/core", + "packages/plugin", + "packages/mcp" ], "engines": { "node": ">=22.22.3" }, - "bin": { - "openclaw-cursor-cloud": "./dist/cli.js" - }, - "exports": { - ".": "./dist/index.js", - "./client": "./dist/client.js", - "./mcp": "./dist/mcp.js" - }, "scripts": { - "build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"", - "plugin:build": "npm run build && openclaw plugins build --entry ./dist/index.js && node scripts/preserve-manifest-skills.mjs", - "plugin:validate": "npm run build && openclaw plugins validate --entry ./dist/index.js", + "build": "npm run build -w cursor-cloud-core && npm run build -w cursor-cloud-mcp && npm run build -w openclaw-plugin-cursor-cloud", + "plugin:build": "npm run plugin:build -w openclaw-plugin-cursor-cloud", + "plugin:validate": "npm run plugin:validate -w openclaw-plugin-cursor-cloud", "test": "vitest run --config ./vitest.config.ts", - "prepack": "npm run plugin:build", "release": "./scripts/release.sh" }, - "files": [ - "dist", - "openclaw.plugin.json", - "README.md", - "LICENSE", - "SECURITY.md", - "skills", - "examples", - "scripts/install-gateway.sh" - ], - "publishConfig": { - "access": "public" - }, - "peerDependencies": { - "openclaw": ">=2026.5.17" - }, - "dependencies": { - "typebox": "^1.1.38" - }, "devDependencies": { "@types/node": "^24.5.2", "openclaw": "2026.9.2", "typescript": "^5.9.0", "vitest": "^3.2.0" - }, - "openclaw": { - "extensions": [ - "./dist/index.js" - ], - "compat": { - "pluginApi": ">=2026.5.17", - "minGatewayVersion": "2026.5.17" - }, - "install": { - "npmSpec": "openclaw-plugin-cursor-cloud@0.2.0", - "defaultChoice": "npm", - "minHostVersion": ">=2026.5.17" - }, - "build": { - "openclawVersion": "2026.9.2" - } } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..e8c3c83 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,5 @@ +# cursor-cloud-core + +Shared library for coordinating Cursor Cloud Agents: pinned `api.cursor.com` client, env harvest, model params, launch/reply/status/watch. + +Used by `openclaw-plugin-cursor-cloud` and `cursor-cloud-mcp`. CLI: `cursor-cloud`. diff --git a/examples/cli-config.json b/packages/core/examples/cli-config.json similarity index 100% rename from examples/cli-config.json rename to packages/core/examples/cli-config.json diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..7fced11 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,33 @@ +{ + "name": "cursor-cloud-core", + "version": "0.3.0", + "description": "Shared Cursor Cloud Agents coordinator: API client, env harvest, launch/reply/status/watch.", + "type": "module", + "license": "MIT", + "engines": { + "node": ">=22.22.3" + }, + "bin": { + "cursor-cloud": "./dist/cli.js" + }, + "exports": { + ".": "./dist/index.js", + "./client": "./dist/client.js", + "./cli": "./dist/cli.js", + "./ledger": "./dist/ledger.js" + }, + "files": [ + "dist", + "examples", + "README.md" + ], + "scripts": { + "build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "typebox": "^1.1.38" + } +} diff --git a/src/actions.test.ts b/packages/core/src/actions.test.ts similarity index 100% rename from src/actions.test.ts rename to packages/core/src/actions.test.ts diff --git a/src/actions.ts b/packages/core/src/actions.ts similarity index 100% rename from src/actions.ts rename to packages/core/src/actions.ts diff --git a/src/artifacts.ts b/packages/core/src/artifacts.ts similarity index 100% rename from src/artifacts.ts rename to packages/core/src/artifacts.ts diff --git a/src/brief.test.ts b/packages/core/src/brief.test.ts similarity index 100% rename from src/brief.test.ts rename to packages/core/src/brief.test.ts diff --git a/src/brief.ts b/packages/core/src/brief.ts similarity index 100% rename from src/brief.ts rename to packages/core/src/brief.ts diff --git a/src/cli.ts b/packages/core/src/cli.ts similarity index 91% rename from src/cli.ts rename to packages/core/src/cli.ts index 39322e5..a1f3a05 100644 --- a/src/cli.ts +++ b/packages/core/src/cli.ts @@ -14,9 +14,7 @@ import { } from "./actions.js"; import { formatError } from "./errors.js"; import { loadCliConfig } from "./load-config.js"; -import { startMcpServer } from "./mcp.js"; import { createRuntime } from "./runtime.js"; -import { setupGateway } from "./setup.js"; import { VERSION } from "./version.js"; type Flags = Record; @@ -31,20 +29,6 @@ async function main(argv = process.argv.slice(2)): Promise { console.log(VERSION); return; } - if (command === "mcp") { - await startMcpServer(); - return; - } - if (command === "setup") { - const source = str(flags.source); - printJson( - await setupGateway({ - source: source === "link" || source === "npm" || source === "skip" ? source : undefined, - }), - ); - return; - } - const runtime = createRuntime(loadCliConfig()); const prompt = readPrompt(flags, rest); @@ -137,7 +121,7 @@ async function main(argv = process.argv.slice(2)): Promise { ); return; default: - throw new Error(`Unknown command '${command}'. Try openclaw-cursor-cloud help.`); + throw new Error(`Unknown command '${command}'. Try cursor-cloud help.`); } } @@ -227,7 +211,7 @@ function printJson(value: unknown): void { function printHelp(): void { writeFileSync( 1, - `openclaw-cursor-cloud ${VERSION} + `cursor-cloud ${VERSION} Commands: launch --env --prompt [--name] [--model] [--effort low|med|high|xhigh] [--fast] [--repo] [--watch] @@ -240,11 +224,12 @@ Commands: me envs [--refresh] [--role base|project] [--project ] [--repo ] agents [--env ] [--project ] [--repo ] - mcp - setup [--source link|npm|skip] Auth: CURSOR_API_KEY (or apiKeyEnv). Config: CURSOR_CLOUD_CONFIG or ~/.config/openclaw-cursor-cloud/config.json + +OpenClaw setup: openclaw-cursor-cloud setup +MCP stdio: cursor-cloud-mcp `, ); } diff --git a/src/client.test.ts b/packages/core/src/client.test.ts similarity index 100% rename from src/client.test.ts rename to packages/core/src/client.test.ts diff --git a/src/client.ts b/packages/core/src/client.ts similarity index 100% rename from src/client.ts rename to packages/core/src/client.ts diff --git a/src/config.ts b/packages/core/src/config.ts similarity index 100% rename from src/config.ts rename to packages/core/src/config.ts diff --git a/src/env-catalog.test.ts b/packages/core/src/env-catalog.test.ts similarity index 100% rename from src/env-catalog.test.ts rename to packages/core/src/env-catalog.test.ts diff --git a/src/env-catalog.ts b/packages/core/src/env-catalog.ts similarity index 100% rename from src/env-catalog.ts rename to packages/core/src/env-catalog.ts diff --git a/src/errors.ts b/packages/core/src/errors.ts similarity index 100% rename from src/errors.ts rename to packages/core/src/errors.ts diff --git a/src/harvest.ts b/packages/core/src/harvest.ts similarity index 69% rename from src/harvest.ts rename to packages/core/src/harvest.ts index 2c78b67..5b9a207 100644 --- a/src/harvest.ts +++ b/packages/core/src/harvest.ts @@ -3,28 +3,6 @@ import { createRuntime, type Runtime } from "./runtime.js"; const CATALOG_MAX_AGE_MS = 6 * 60 * 60 * 1000; -export type HarvestApi = { - pluginConfig?: unknown; - registerService: (service: { - id: string; - start: (ctx: { serviceHealth?: { clearFailure?: () => void; reportFailure?: (error: unknown) => void } }) => void; - stop: () => void; - }) => void; -}; - -export function registerHarvestService(api: HarvestApi): void { - api.registerService({ - id: "cursor-cloud-env-harvest", - start(ctx) { - void runInitHarvest(api.pluginConfig).then( - () => ctx.serviceHealth?.clearFailure?.(), - (error) => ctx.serviceHealth?.reportFailure?.(error), - ); - }, - stop() {}, - }); -} - export async function runInitHarvest(pluginConfig: unknown, env: NodeJS.ProcessEnv = process.env): Promise { const runtime = createRuntime(pluginConfig as never, env); await ensureHarvested(runtime); diff --git a/src/host.test.ts b/packages/core/src/host.test.ts similarity index 100% rename from src/host.test.ts rename to packages/core/src/host.test.ts diff --git a/src/host.ts b/packages/core/src/host.ts similarity index 100% rename from src/host.ts rename to packages/core/src/host.ts diff --git a/src/ids.ts b/packages/core/src/ids.ts similarity index 100% rename from src/ids.ts rename to packages/core/src/ids.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..6d1f14d --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,42 @@ +export { CursorCloudClient } from "./client.js"; +export { pluginConfigSchema, resolveConfig, publicConfig } from "./config.js"; +export { composePrompt, proofFromRun } from "./brief.js"; +export { presentModel, resolveModelSelection } from "./model.js"; +export { FollowUpError, ModelLockedError, formatError } from "./errors.js"; +export { ensureHarvested, runInitHarvest } from "./harvest.js"; +export { resolvePlacement } from "./placement.js"; +export type { SessionRef } from "./placement.js"; +export { catalogFromAgents, fetchEnvCatalog, mergeCatalogIntoConfig } from "./env-catalog.js"; +export { listRegisteredEnvs, resolveLaunchTarget } from "./registry.js"; +export { watchRun } from "./waiter.js"; +export { createRuntime } from "./runtime.js"; +export type { Runtime } from "./runtime.js"; +export { loadCliConfig } from "./load-config.js"; +export { upsertLedger } from "./ledger.js"; +export { + agentsAction, + cancelAction, + envsAction, + launchAction, + ledgerAction, + listAction, + meAction, + modelsAction, + replyAction, + statusAction, + watchAction, + watchForeground, +} from "./actions.js"; +export { + agentsParamsSchema, + cancelParamsSchema, + emptyParamsSchema, + envsParamsSchema, + jsonSchema, + launchParamsSchema, + listParamsSchema, + replyParamsSchema, + statusParamsSchema, + watchParamsSchema, +} from "./tool-schemas.js"; +export { PACKAGE_NAME, VERSION, DEFAULT_API_BASE_URL } from "./version.js"; diff --git a/src/ledger.test.ts b/packages/core/src/ledger.test.ts similarity index 100% rename from src/ledger.test.ts rename to packages/core/src/ledger.test.ts diff --git a/src/ledger.ts b/packages/core/src/ledger.ts similarity index 100% rename from src/ledger.ts rename to packages/core/src/ledger.ts diff --git a/src/load-config.ts b/packages/core/src/load-config.ts similarity index 100% rename from src/load-config.ts rename to packages/core/src/load-config.ts diff --git a/src/lock.ts b/packages/core/src/lock.ts similarity index 100% rename from src/lock.ts rename to packages/core/src/lock.ts diff --git a/src/model.test.ts b/packages/core/src/model.test.ts similarity index 100% rename from src/model.test.ts rename to packages/core/src/model.test.ts diff --git a/src/model.ts b/packages/core/src/model.ts similarity index 100% rename from src/model.ts rename to packages/core/src/model.ts diff --git a/src/notify.test.ts b/packages/core/src/notify.test.ts similarity index 100% rename from src/notify.test.ts rename to packages/core/src/notify.test.ts diff --git a/src/notify.ts b/packages/core/src/notify.ts similarity index 100% rename from src/notify.ts rename to packages/core/src/notify.ts diff --git a/src/paths.ts b/packages/core/src/paths.ts similarity index 100% rename from src/paths.ts rename to packages/core/src/paths.ts diff --git a/src/placement.test.ts b/packages/core/src/placement.test.ts similarity index 100% rename from src/placement.test.ts rename to packages/core/src/placement.test.ts diff --git a/src/placement.ts b/packages/core/src/placement.ts similarity index 100% rename from src/placement.ts rename to packages/core/src/placement.ts diff --git a/src/registry.test.ts b/packages/core/src/registry.test.ts similarity index 100% rename from src/registry.test.ts rename to packages/core/src/registry.test.ts diff --git a/src/registry.ts b/packages/core/src/registry.ts similarity index 100% rename from src/registry.ts rename to packages/core/src/registry.ts diff --git a/src/runtime.ts b/packages/core/src/runtime.ts similarity index 92% rename from src/runtime.ts rename to packages/core/src/runtime.ts index 4d278a5..53a3e33 100644 --- a/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -25,7 +25,7 @@ export function createRuntime( const apiKey = env[config.apiKeyEnv]?.trim(); if (!apiKey) { throw new ConfigError( - `Missing ${config.apiKeyEnv}. Set it on the OpenClaw gateway environment; do not put the key in plugin config.`, + `Missing ${config.apiKeyEnv}. Set it on the host environment; do not put the key in config.`, ); } return { diff --git a/src/sse.test.ts b/packages/core/src/sse.test.ts similarity index 100% rename from src/sse.test.ts rename to packages/core/src/sse.test.ts diff --git a/src/sse.ts b/packages/core/src/sse.ts similarity index 100% rename from src/sse.ts rename to packages/core/src/sse.ts diff --git a/src/tool-schemas.ts b/packages/core/src/tool-schemas.ts similarity index 100% rename from src/tool-schemas.ts rename to packages/core/src/tool-schemas.ts diff --git a/src/transcript.ts b/packages/core/src/transcript.ts similarity index 100% rename from src/transcript.ts rename to packages/core/src/transcript.ts diff --git a/src/types.ts b/packages/core/src/types.ts similarity index 100% rename from src/types.ts rename to packages/core/src/types.ts diff --git a/src/version.ts b/packages/core/src/version.ts similarity index 85% rename from src/version.ts rename to packages/core/src/version.ts index 2a31642..245615f 100644 --- a/src/version.ts +++ b/packages/core/src/version.ts @@ -2,7 +2,7 @@ import { createRequire } from "node:module"; const require = createRequire(import.meta.url); -export const PACKAGE_NAME = "openclaw-plugin-cursor-cloud"; +export const PACKAGE_NAME = "cursor-cloud-core"; export const VERSION: string = require("../package.json").version; export const USER_AGENT = `${PACKAGE_NAME}/${VERSION}`; export const DEFAULT_API_BASE_URL = "https://api.cursor.com"; diff --git a/src/waiter.test.ts b/packages/core/src/waiter.test.ts similarity index 100% rename from src/waiter.test.ts rename to packages/core/src/waiter.test.ts diff --git a/src/waiter.ts b/packages/core/src/waiter.ts similarity index 100% rename from src/waiter.ts rename to packages/core/src/waiter.ts diff --git a/src/watch-process.test.ts b/packages/core/src/watch-process.test.ts similarity index 100% rename from src/watch-process.test.ts rename to packages/core/src/watch-process.test.ts diff --git a/src/watch-process.ts b/packages/core/src/watch-process.ts similarity index 100% rename from src/watch-process.ts rename to packages/core/src/watch-process.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..4a26fb1 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 0000000..5ff178c --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,18 @@ +# cursor-cloud-mcp + +Stdio MCP server for the same `cursor_cloud_*` tools as the OpenClaw plugin. + +```json +{ + "mcpServers": { + "cursor-cloud": { + "command": "cursor-cloud-mcp", + "env": { + "CURSOR_API_KEY": "${CURSOR_API_KEY}" + } + } + } +} +``` + +Config: `CURSOR_CLOUD_CONFIG` or `~/.config/openclaw-cursor-cloud/config.json` (see `cursor-cloud-core` examples). diff --git a/examples/mcp.json b/packages/mcp/examples/mcp.json similarity index 65% rename from examples/mcp.json rename to packages/mcp/examples/mcp.json index 598e4b0..9334fd5 100644 --- a/examples/mcp.json +++ b/packages/mcp/examples/mcp.json @@ -1,8 +1,7 @@ { "mcpServers": { "cursor-cloud": { - "command": "openclaw-cursor-cloud", - "args": ["mcp"], + "command": "cursor-cloud-mcp", "env": { "CURSOR_API_KEY": "${CURSOR_API_KEY}" } diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 0000000..07c648d --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,36 @@ +{ + "name": "cursor-cloud-mcp", + "version": "0.3.0", + "description": "MCP stdio server for Cursor Cloud Agents, built on cursor-cloud-core.", + "type": "module", + "license": "MIT", + "keywords": [ + "mcp", + "cursor", + "cloud-agents" + ], + "engines": { + "node": ">=22.22.3" + }, + "bin": { + "cursor-cloud-mcp": "./dist/cli.js" + }, + "exports": { + ".": "./dist/index.js", + "./cli": "./dist/cli.js" + }, + "files": [ + "dist", + "examples", + "README.md" + ], + "scripts": { + "build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "cursor-cloud-core": "0.3.0" + } +} diff --git a/packages/mcp/src/cli.ts b/packages/mcp/src/cli.ts new file mode 100644 index 0000000..310e3b5 --- /dev/null +++ b/packages/mcp/src/cli.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import { startMcpServer } from "./mcp.js"; + +startMcpServer().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; +}); diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts new file mode 100644 index 0000000..cb27aa9 --- /dev/null +++ b/packages/mcp/src/index.ts @@ -0,0 +1 @@ +export { handle, startMcpServer } from "./mcp.js"; diff --git a/src/mcp.test.ts b/packages/mcp/src/mcp.test.ts similarity index 89% rename from src/mcp.test.ts rename to packages/mcp/src/mcp.test.ts index de47a86..7474c1a 100644 --- a/src/mcp.test.ts +++ b/packages/mcp/src/mcp.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; +import type { Runtime } from "cursor-cloud-core"; import { handle } from "./mcp.js"; -import type { Runtime } from "./runtime.js"; describe("mcp", () => { it("lists tools and calls launch through the runtime", async () => { @@ -27,8 +27,9 @@ describe("mcp", () => { }, env: { CURSOR_API_KEY: "k" }, ledgerPath: "/tmp/ledger-unused", + catalogPath: "/tmp/catalog-unused", } as unknown as Runtime; - vi.spyOn(await import("./ledger.js"), "upsertLedger").mockReturnValue([]); + vi.spyOn(await import("../../core/src/ledger.js"), "upsertLedger").mockReturnValue([]); const called = await handle( { jsonrpc: "2.0", diff --git a/src/mcp.ts b/packages/mcp/src/mcp.ts similarity index 96% rename from src/mcp.ts rename to packages/mcp/src/mcp.ts index c49c59e..180d459 100644 --- a/src/mcp.ts +++ b/packages/mcp/src/mcp.ts @@ -1,31 +1,30 @@ import { createInterface } from "node:readline"; import { agentsAction, - cancelAction, - envsAction, - launchAction, - listAction, - meAction, - modelsAction, - replyAction, - statusAction, - watchAction, -} from "./actions.js"; -import { formatError } from "./errors.js"; -import { loadCliConfig } from "./load-config.js"; -import { createRuntime, type Runtime } from "./runtime.js"; -import { agentsParamsSchema, + cancelAction, cancelParamsSchema, + createRuntime, emptyParamsSchema, + envsAction, envsParamsSchema, + formatError, jsonSchema, + launchAction, launchParamsSchema, + listAction, listParamsSchema, + loadCliConfig, + meAction, + modelsAction, + replyAction, replyParamsSchema, + statusAction, statusParamsSchema, + watchAction, watchParamsSchema, -} from "./tool-schemas.js"; + type Runtime, +} from "cursor-cloud-core"; import { PACKAGE_NAME, VERSION } from "./version.js"; type JsonRpc = { diff --git a/packages/mcp/src/version.ts b/packages/mcp/src/version.ts new file mode 100644 index 0000000..a22919c --- /dev/null +++ b/packages/mcp/src/version.ts @@ -0,0 +1,6 @@ +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); + +export const PACKAGE_NAME = "cursor-cloud-mcp"; +export const VERSION: string = require("../package.json").version; diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 0000000..4a26fb1 --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/plugin/README.md b/packages/plugin/README.md new file mode 100644 index 0000000..515a470 --- /dev/null +++ b/packages/plugin/README.md @@ -0,0 +1,11 @@ +# openclaw-plugin-cursor-cloud + +OpenClaw plugin for `cursor_cloud_*` tools. Depends on `cursor-cloud-core`. + +```bash +npx -y openclaw-plugin-cursor-cloud@0.3.0 setup +# or from this repo +./scripts/install-gateway.sh +``` + +Then set `CURSOR_API_KEY` on the gateway and restart. diff --git a/examples/gateway-plugin.json b/packages/plugin/examples/gateway-plugin.json similarity index 100% rename from examples/gateway-plugin.json rename to packages/plugin/examples/gateway-plugin.json diff --git a/examples/instance-enable.batch.json b/packages/plugin/examples/instance-enable.batch.json similarity index 100% rename from examples/instance-enable.batch.json rename to packages/plugin/examples/instance-enable.batch.json diff --git a/openclaw.plugin.json b/packages/plugin/openclaw.plugin.json similarity index 99% rename from openclaw.plugin.json rename to packages/plugin/openclaw.plugin.json index 83aeea9..b7629d1 100644 --- a/openclaw.plugin.json +++ b/packages/plugin/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "cursor-cloud", "name": "Cursor Cloud", "description": "Launch and coordinate Cursor Cloud Agents on named saved environments. Does not run coding work on the OpenClaw host.", - "version": "0.2.0", + "version": "0.3.0", "configSchema": { "type": "object", "properties": { diff --git a/packages/plugin/package.json b/packages/plugin/package.json new file mode 100644 index 0000000..d40faad --- /dev/null +++ b/packages/plugin/package.json @@ -0,0 +1,66 @@ +{ + "name": "openclaw-plugin-cursor-cloud", + "version": "0.3.0", + "description": "OpenClaw plugin that launches Cursor Cloud Agents on named saved environments.", + "type": "module", + "license": "MIT", + "keywords": [ + "openclaw", + "plugin", + "cursor", + "cloud-agents", + "clawhub" + ], + "engines": { + "node": ">=22.22.3" + }, + "bin": { + "openclaw-cursor-cloud": "./dist/cli.js" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist", + "openclaw.plugin.json", + "README.md", + "skills", + "examples", + "scripts/install-gateway.sh" + ], + "scripts": { + "build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"", + "plugin:build": "npm run build -w cursor-cloud-core && npm run build && openclaw plugins build --entry ./dist/index.js && node scripts/preserve-manifest-skills.mjs", + "plugin:validate": "npm run build -w cursor-cloud-core && npm run build && openclaw plugins validate --entry ./dist/index.js" + }, + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "openclaw": ">=2026.5.17" + }, + "dependencies": { + "cursor-cloud-core": "0.3.0", + "cursor-cloud-mcp": "0.3.0" + }, + "devDependencies": { + "openclaw": "2026.9.2" + }, + "openclaw": { + "extensions": [ + "./dist/index.js" + ], + "compat": { + "pluginApi": ">=2026.5.17", + "minGatewayVersion": "2026.5.17" + }, + "install": { + "npmSpec": "openclaw-plugin-cursor-cloud@0.3.0", + "defaultChoice": "npm", + "minHostVersion": ">=2026.5.17" + }, + "build": { + "openclawVersion": "2026.9.2" + } + } +} diff --git a/packages/plugin/scripts/install-gateway.sh b/packages/plugin/scripts/install-gateway.sh new file mode 100755 index 0000000..3abb6c8 --- /dev/null +++ b/packages/plugin/scripts/install-gateway.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +plugin="$(cd "$(dirname "$0")/.." && pwd)" +root="$(cd "$plugin/../.." && pwd)" + +if [[ ! -f "$plugin/dist/cli.js" ]]; then + (cd "$root" && npm install && npm run plugin:build) +fi + +exec node "$plugin/dist/cli.js" setup --source link diff --git a/scripts/preserve-manifest-skills.mjs b/packages/plugin/scripts/preserve-manifest-skills.mjs similarity index 100% rename from scripts/preserve-manifest-skills.mjs rename to packages/plugin/scripts/preserve-manifest-skills.mjs diff --git a/skills/cursor-cloud/SKILL.md b/packages/plugin/skills/cursor-cloud/SKILL.md similarity index 100% rename from skills/cursor-cloud/SKILL.md rename to packages/plugin/skills/cursor-cloud/SKILL.md diff --git a/skills/cursor-cloud/references/smoke.md b/packages/plugin/skills/cursor-cloud/references/smoke.md similarity index 100% rename from skills/cursor-cloud/references/smoke.md rename to packages/plugin/skills/cursor-cloud/references/smoke.md diff --git a/packages/plugin/src/cli.ts b/packages/plugin/src/cli.ts new file mode 100644 index 0000000..3db0e37 --- /dev/null +++ b/packages/plugin/src/cli.ts @@ -0,0 +1,61 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { formatError } from "cursor-cloud-core"; +import { setupGateway } from "./setup.js"; +import { VERSION } from "./version.js"; + +async function main(argv = process.argv.slice(2)): Promise { + const command = argv.find((token) => !token.startsWith("-")) ?? ""; + if (!command || command === "help" || argv.includes("--help")) { + process.stdout.write( + `openclaw-cursor-cloud ${VERSION} + +Commands: + setup [--source link|npm|skip] + mcp (delegates to cursor-cloud-mcp) + * other commands delegate to cursor-cloud + +`, + ); + return; + } + if (command === "version" || argv.includes("--version")) { + console.log(VERSION); + return; + } + if (command === "setup") { + const sourceFlag = readFlag(argv, "source"); + const source = sourceFlag === "link" || sourceFlag === "npm" || sourceFlag === "skip" ? sourceFlag : undefined; + process.stdout.write(`${JSON.stringify(await setupGateway({ source }), null, 2)}\n`); + return; + } + const target = command === "mcp" ? "cursor-cloud-mcp/cli" : "cursor-cloud-core/cli"; + const cli = fileURLToPath(import.meta.resolve(target)); + const child = spawn(process.execPath, [cli, ...argv], { stdio: "inherit" }); + await new Promise((resolve, reject) => { + child.on("error", reject); + child.on("exit", (code) => { + process.exitCode = code ?? 0; + resolve(); + }); + }); +} + +function readFlag(argv: string[], name: string): string | undefined { + const idx = argv.findIndex((token) => token === `--${name}` || token.startsWith(`--${name}=`)); + if (idx === -1) { + return undefined; + } + const token = argv[idx]; + if (token.includes("=")) { + return token.split("=", 2)[1]; + } + const next = argv[idx + 1]; + return next && !next.startsWith("-") ? next : undefined; +} + +main().catch((error) => { + process.stdout.write(`${JSON.stringify(formatError(error), null, 2)}\n`); + process.exitCode = 1; +}); diff --git a/packages/plugin/src/harvest.ts b/packages/plugin/src/harvest.ts new file mode 100644 index 0000000..bc16231 --- /dev/null +++ b/packages/plugin/src/harvest.ts @@ -0,0 +1,28 @@ +import { ensureHarvested, createRuntime } from "cursor-cloud-core"; + +export type HarvestApi = { + pluginConfig?: unknown; + registerService: (service: { + id: string; + start: (ctx: { serviceHealth?: { clearFailure?: () => void; reportFailure?: (error: unknown) => void } }) => void; + stop: () => void; + }) => void; +}; + +export function registerHarvestService(api: HarvestApi): void { + api.registerService({ + id: "cursor-cloud-env-harvest", + start(ctx) { + void runInitHarvest(api.pluginConfig).then( + () => ctx.serviceHealth?.clearFailure?.(), + (error) => ctx.serviceHealth?.reportFailure?.(error), + ); + }, + stop() {}, + }); +} + +export async function runInitHarvest(pluginConfig: unknown, env: NodeJS.ProcessEnv = process.env): Promise { + const runtime = createRuntime(pluginConfig as never, env); + await ensureHarvested(runtime); +} diff --git a/src/index.test.ts b/packages/plugin/src/index.test.ts similarity index 100% rename from src/index.test.ts rename to packages/plugin/src/index.test.ts diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts new file mode 100644 index 0000000..3838a8a --- /dev/null +++ b/packages/plugin/src/index.ts @@ -0,0 +1,22 @@ +export { + CursorCloudClient, + agentsAction, + cancelAction, + createRuntime, + ensureHarvested, + envsAction, + launchAction, + listAction, + meAction, + modelsAction, + pluginConfigSchema, + presentModel, + replyAction, + resolveConfig, + resolveModelSelection, + statusAction, + watchAction, +} from "cursor-cloud-core"; +export { registerHarvestService } from "./harvest.js"; +export { setupGateway } from "./setup.js"; +export { default } from "./plugin.js"; diff --git a/src/plugin.ts b/packages/plugin/src/plugin.ts similarity index 96% rename from src/plugin.ts rename to packages/plugin/src/plugin.ts index ea816b1..26770e4 100644 --- a/src/plugin.ts +++ b/packages/plugin/src/plugin.ts @@ -1,32 +1,30 @@ import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin"; import { agentsAction, + agentsParamsSchema, cancelAction, + cancelParamsSchema, + createRuntime, + emptyParamsSchema, envsAction, + envsParamsSchema, + formatError, launchAction, + launchParamsSchema, listAction, + listParamsSchema, meAction, modelsAction, + pluginConfigSchema, replyAction, - statusAction, - watchAction, -} from "./actions.js"; -import { pluginConfigSchema } from "./config.js"; -import { formatError } from "./errors.js"; -import { registerHarvestService } from "./harvest.js"; -import type { SessionRef } from "./placement.js"; -import { createRuntime } from "./runtime.js"; -import { - agentsParamsSchema, - cancelParamsSchema, - emptyParamsSchema, - envsParamsSchema, - launchParamsSchema, - listParamsSchema, replyParamsSchema, + statusAction, statusParamsSchema, + watchAction, watchParamsSchema, -} from "./tool-schemas.js"; + type SessionRef, +} from "cursor-cloud-core"; +import { registerHarvestService } from "./harvest.js"; function toolResult(value: unknown, failed = false) { return { diff --git a/src/setup.test.ts b/packages/plugin/src/setup.test.ts similarity index 100% rename from src/setup.test.ts rename to packages/plugin/src/setup.test.ts diff --git a/src/setup.ts b/packages/plugin/src/setup.ts similarity index 100% rename from src/setup.ts rename to packages/plugin/src/setup.ts diff --git a/packages/plugin/src/version.ts b/packages/plugin/src/version.ts new file mode 100644 index 0000000..18cf2a2 --- /dev/null +++ b/packages/plugin/src/version.ts @@ -0,0 +1,6 @@ +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); + +export const PACKAGE_NAME = "openclaw-plugin-cursor-cloud"; +export const VERSION: string = require("../package.json").version; diff --git a/packages/plugin/tsconfig.json b/packages/plugin/tsconfig.json new file mode 100644 index 0000000..4a26fb1 --- /dev/null +++ b/packages/plugin/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/scripts/install-gateway.sh b/scripts/install-gateway.sh index 1253e0b..4aab21b 100755 --- a/scripts/install-gateway.sh +++ b/scripts/install-gateway.sh @@ -1,12 +1,3 @@ #!/usr/bin/env bash set -euo pipefail - -root="$(cd "$(dirname "$0")/.." && pwd)" -cd "$root" - -if [[ ! -f dist/cli.js ]]; then - npm install - npm run plugin:build -fi - -exec node "$root/dist/cli.js" setup --source link +exec "$(cd "$(dirname "$0")/.." && pwd)/packages/plugin/scripts/install-gateway.sh" diff --git a/scripts/release.sh b/scripts/release.sh index 5c11ef2..fbfcbfc 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Bump package metadata for a release. Does not publish or push. -# Usage: ./scripts/release.sh 0.1.1 +# Bump workspace package versions. Does not publish or push. +# Usage: ./scripts/release.sh 0.3.1 set -euo pipefail version="${1:-}" @@ -19,17 +19,39 @@ const version = process.env.VERSION; if (!version) { throw new Error("VERSION is required"); } -const pkg = JSON.parse(readFileSync("package.json", "utf8")); -const previous = pkg.version; -pkg.version = version; -pkg.openclaw = pkg.openclaw ?? {}; -pkg.openclaw.install = pkg.openclaw.install ?? {}; -pkg.openclaw.install.npmSpec = `${pkg.name}@${version}`; -writeFileSync("package.json", `${JSON.stringify(pkg, null, 2)}\n`); - -const manifest = JSON.parse(readFileSync("openclaw.plugin.json", "utf8")); + +function bump(path, mutate) { + const pkg = JSON.parse(readFileSync(path, "utf8")); + const previous = pkg.version; + pkg.version = version; + mutate?.(pkg); + writeFileSync(path, `${JSON.stringify(pkg, null, 2)}\n`); + return previous; +} + +const previous = bump("package.json"); +bump("packages/core/package.json"); +bump("packages/mcp/package.json", (pkg) => { + if (pkg.dependencies?.["cursor-cloud-core"]) { + pkg.dependencies["cursor-cloud-core"] = version; + } +}); +bump("packages/plugin/package.json", (pkg) => { + if (pkg.dependencies?.["cursor-cloud-core"]) { + pkg.dependencies["cursor-cloud-core"] = version; + } + if (pkg.dependencies?.["cursor-cloud-mcp"]) { + pkg.dependencies["cursor-cloud-mcp"] = version; + } + pkg.openclaw = pkg.openclaw ?? {}; + pkg.openclaw.install = pkg.openclaw.install ?? {}; + pkg.openclaw.install.npmSpec = `${pkg.name}@${version}`; +}); + +const manifestPath = "packages/plugin/openclaw.plugin.json"; +const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); manifest.version = version; -writeFileSync("openclaw.plugin.json", `${JSON.stringify(manifest, null, 2)}\n`); +writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); const date = new Date().toISOString().slice(0, 10); const changelog = readFileSync("CHANGELOG.md", "utf8"); @@ -47,4 +69,4 @@ EOF npm install --package-lock-only echo "Next: git add -u && git commit && git tag v${version} && git push --follow-tags" -echo "Then: npm publish --access public and/or clawhub package publish ." +echo "Then publish packages/core, packages/mcp, packages/plugin (in that order)." diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 5423c58..0000000 --- a/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -export { CursorCloudClient } from "./client.js"; -export { pluginConfigSchema, resolveConfig } from "./config.js"; -export { composePrompt, proofFromRun } from "./brief.js"; -export { resolveModelSelection } from "./model.js"; -export { FollowUpError, ModelLockedError } from "./errors.js"; -export { registerHarvestService, ensureHarvested } from "./harvest.js"; -export { resolvePlacement } from "./placement.js"; -export { catalogFromAgents, fetchEnvCatalog, mergeCatalogIntoConfig } from "./env-catalog.js"; -export { listRegisteredEnvs, resolveLaunchTarget } from "./registry.js"; -export { watchRun } from "./waiter.js"; -export { setupGateway } from "./setup.js"; -export { - agentsAction, - cancelAction, - envsAction, - launchAction, - ledgerAction, - listAction, - meAction, - modelsAction, - replyAction, - statusAction, - watchAction, -} from "./actions.js"; -export { default } from "./plugin.js"; diff --git a/tsconfig.json b/tsconfig.json index 4a26fb1..da51150 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,8 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "declaration": true, - "outDir": "dist", - "rootDir": "src", - "skipLibCheck": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["src/**/*.test.ts"] + "files": [], + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mcp" }, + { "path": "./packages/plugin" } + ] } diff --git a/vitest.config.ts b/vitest.config.ts index 9aeb760..05e2e20 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,15 @@ +import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", - include: ["src/**/*.test.ts"], + include: ["packages/*/src/**/*.test.ts"], restoreMocks: true, }, + resolve: { + alias: { + "cursor-cloud-core": fileURLToPath(new URL("./packages/core/src/index.ts", import.meta.url)), + }, + }, }); From f65a75349fbb80e0cd4fda75ce5c6c770d8cbf0a Mon Sep 17 00:00:00 2001 From: Yonathan Morales Date: Tue, 29 Sep 2026 07:23:14 +0000 Subject: [PATCH 2/2] refactor: resolve effort spellings from one alias table Input branches, the tool schema, and the models payload were a second copy of the spellings. The table now maps a spelling to a level and supplies the preferred catalog value. Co-authored-by: AIPAL GROK (4.6) Co-authored-by: Cursor --- README.md | 2 +- packages/core/src/actions.ts | 8 +-- packages/core/src/cli.ts | 3 +- packages/core/src/model.test.ts | 22 +++++- packages/core/src/model.ts | 72 ++++++++++++++------ packages/core/src/tool-schemas.ts | 30 ++++---- packages/plugin/skills/cursor-cloud/SKILL.md | 2 +- packages/plugin/src/plugin.ts | 2 +- 8 files changed, 94 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 1398073..f4464f6 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ cursor-cloud launch --env base --prompt "Smoke the env, then stop." cursor-cloud reply --agent-id bc-… --prompt "Continue on the same branch." ``` -Launch: `model` + optional `effort` (`low|med|high|xhigh`) + optional `fast`. `med` is sent as Cursor `medium`. Reply cannot change those (`model_locked`). +Launch: `model` plus optional `effort` and `fast`. Effort spellings go through one alias table; the table's preferred value is what the catalog receives (`med` prefers `medium`). Reply cannot change those (`model_locked`). ## Environments and tools diff --git a/packages/core/src/actions.ts b/packages/core/src/actions.ts index 55a8885..ebae82e 100644 --- a/packages/core/src/actions.ts +++ b/packages/core/src/actions.ts @@ -4,7 +4,7 @@ import { CursorCloudApiError, FollowUpError, ModelLockedError } from "./errors.j import { assertAgentId, assertRunId } from "./ids.js"; import { ensureHarvested } from "./harvest.js"; import { agentsForTarget, readLedger, upsertLedger } from "./ledger.js"; -import { parseAgentModel, presentModel, resolveModelSelection } from "./model.js"; +import { effortWireDefaults, parseAgentModel, presentModel, resolveModelSelection, TOOL_EFFORT_LEVELS } from "./model.js"; import { resolvePlacement, sessionLabel, type SessionRef } from "./placement.js"; import { isWatchLocked, tryAcquireWatchLock } from "./lock.js"; import { notifyValues, resolveWatchSession, runNotifyCommand } from "./notify.js"; @@ -432,9 +432,9 @@ export async function modelsAction(runtime: Runtime) { const models = await runtime.client.listModels(); return { ok: true as const, - next: "Pass model as the item id (grok-4.6). Grok 4.6 Med is model=grok-4.6 effort=med — the tool sends Cursor value medium. effort=xhigh is Extra High. Grok 4.7 uses reasoning_effort. Omit fast to keep the model default (often fast=true). Reply cannot change these (model_locked).", - effort: ["low", "med", "high", "xhigh"], - effortAliases: { med: "medium", xhigh: "xhigh" }, + next: "Pass model as an item id. effort spellings map through the alias table onto that model's catalog value. Omit fast to keep the model's default variant. Reply cannot change model, effort, or fast (model_locked).", + effort: TOOL_EFFORT_LEVELS, + effortAliases: effortWireDefaults(), fast: [false, true], items: (models.items ?? []).map(presentModel), }; diff --git a/packages/core/src/cli.ts b/packages/core/src/cli.ts index a1f3a05..fa1d6ee 100644 --- a/packages/core/src/cli.ts +++ b/packages/core/src/cli.ts @@ -13,6 +13,7 @@ import { watchForeground, } from "./actions.js"; import { formatError } from "./errors.js"; +import { TOOL_EFFORT_LEVELS } from "./model.js"; import { loadCliConfig } from "./load-config.js"; import { createRuntime } from "./runtime.js"; import { VERSION } from "./version.js"; @@ -214,7 +215,7 @@ function printHelp(): void { `cursor-cloud ${VERSION} Commands: - launch --env --prompt [--name] [--model] [--effort low|med|high|xhigh] [--fast] [--repo] [--watch] + launch --env --prompt [--name] [--model] [--effort ${TOOL_EFFORT_LEVELS.join("|")}] [--fast] [--repo] [--watch] reply --agent-id bc-… --prompt [--mode] [--watch] status --agent-id bc-… [--run-id run-…] cancel --agent-id bc-… [--run-id run-…] diff --git a/packages/core/src/model.test.ts b/packages/core/src/model.test.ts index 5ba5700..dff2b83 100644 --- a/packages/core/src/model.test.ts +++ b/packages/core/src/model.test.ts @@ -1,6 +1,14 @@ import { describe, expect, it } from "vitest"; import { ConfigError } from "./errors.js"; -import { decorateVariants, presentModel, resolveModelSelection, resolvedModelHint } from "./model.js"; +import { + decorateVariants, + EFFORT_ALIASES, + effortWireDefaults, + normalizeEffort, + presentModel, + resolveModelSelection, + resolvedModelHint, +} from "./model.js"; const grok46 = { id: "grok-4.6", @@ -116,7 +124,17 @@ describe("resolveModelSelection", () => { ).toThrow(ConfigError); }); - it("labels duplicate Grok 4.6 variant names from params", () => { + it("maps every alias in the table and uses the first alias as the wire default", () => { + for (const [level, aliases] of Object.entries(EFFORT_ALIASES)) { + for (const alias of aliases) { + expect(normalizeEffort(alias)).toBe(level); + } + } + expect(effortWireDefaults().med).toBe(EFFORT_ALIASES.med[0]); + expect(() => normalizeEffort("turbo")).toThrow(ConfigError); + }); + + it("labels duplicate variant names from params", () => { const names = decorateVariants(grok46).map((item) => item.displayName); expect(names).toContain("Grok 4.6 Medium"); expect(names).toContain("Grok 4.6 Extra High Fast"); diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 9bc6acc..76cb1a1 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -8,18 +8,56 @@ export type ModelChoice = { mode?: ResolvedModel["mode"]; }; -const TOOL_EFFORT: EffortLevel[] = ["low", "med", "high", "xhigh"]; - -const EFFORT_ALIASES: Record = { +/** Preferred catalog value first, then accepted spellings. Tool level is the key. */ +export const EFFORT_ALIASES: Record = { low: ["low", "minimal", "min"], med: ["medium", "med", "mid"], high: ["high"], - xhigh: ["xhigh", "extra_high", "extrahigh", "max"], + xhigh: ["xhigh", "extra_high", "extrahigh", "extra-high", "max"], +}; + +const EFFORT_PARAM_PATTERNS = { + exact: ["effort", "thinking", "reasoning"], + suffixes: ["_effort"], + includes: ["thinking"], }; +export const TOOL_EFFORT_LEVELS = Object.keys(EFFORT_ALIASES) as EffortLevel[]; + +export function effortInputValues(): string[] { + const seen = new Set(); + const values: string[] = []; + for (const aliases of Object.values(EFFORT_ALIASES)) { + for (const alias of aliases) { + if (!seen.has(alias)) { + seen.add(alias); + values.push(alias); + } + } + } + return values; +} + +export function effortWireDefaults(): Record { + return Object.fromEntries(TOOL_EFFORT_LEVELS.map((level) => [level, EFFORT_ALIASES[level][0]])) as Record< + EffortLevel, + string + >; +} + export function isEffortParamId(id: string): boolean { const raw = id.trim().toLowerCase(); - return raw === "effort" || raw === "thinking" || raw === "reasoning" || raw.endsWith("_effort") || raw.includes("thinking"); + return ( + EFFORT_PARAM_PATTERNS.exact.includes(raw) || + EFFORT_PARAM_PATTERNS.suffixes.some((suffix) => raw.endsWith(suffix)) || + EFFORT_PARAM_PATTERNS.includes.some((part) => raw.includes(part)) + ); +} + +function matchEffort(raw: string): EffortLevel | undefined { + return (Object.entries(EFFORT_ALIASES) as [EffortLevel, readonly string[]][]).find(([, aliases]) => + aliases.includes(raw), + )?.[0]; } export function normalizeEffort(value?: string): EffortLevel | undefined { @@ -27,16 +65,11 @@ export function normalizeEffort(value?: string): EffortLevel | undefined { if (!raw) { return undefined; } - if (raw === "low" || raw === "med" || raw === "high" || raw === "xhigh") { - return raw; - } - if (raw === "medium" || raw === "mid") { - return "med"; + const match = matchEffort(raw); + if (match) { + return match; } - if (raw === "extra_high" || raw === "extrahigh" || raw === "extra-high") { - return "xhigh"; - } - throw new ConfigError(`effort must be low, med, high, or xhigh (got '${value}')`); + throw new ConfigError(`effort must be ${TOOL_EFFORT_LEVELS.join(", ")} (got '${value}')`); } export function cursorEffortValue(effort: EffortLevel, available?: string[]): string { @@ -60,12 +93,7 @@ export function toolEffortFromCursor(value?: string): EffortLevel | undefined { if (!raw) { return undefined; } - for (const effort of TOOL_EFFORT) { - if (EFFORT_ALIASES[effort].includes(raw)) { - return effort; - } - } - return undefined; + return matchEffort(raw); } export function resolvedModelHint(model: string, effort?: EffortLevel, fast?: boolean): string { @@ -200,7 +228,7 @@ function mapFastParam(model: ModelRecord | undefined, fast: boolean): ModelParam return { id: param.id, value: want }; } -function pickValue(values: string[], preferred: string[]): string | undefined { +function pickValue(values: readonly string[], preferred: readonly string[]): string | undefined { for (const item of preferred) { const hit = values.find((value) => value === item); if (hit) { @@ -251,7 +279,7 @@ export function presentModel(model: ModelRecord) { const effortParam = model.parameters?.find((item) => isEffortParamId(item.id)); const effortValues = effortParam?.values?.map((item) => item.value); const toolEffort: Partial> = {}; - for (const effort of TOOL_EFFORT) { + for (const effort of TOOL_EFFORT_LEVELS) { try { toolEffort[effort] = cursorEffortValue(effort, effortValues); } catch { diff --git a/packages/core/src/tool-schemas.ts b/packages/core/src/tool-schemas.ts index 5c8d887..42a65ca 100644 --- a/packages/core/src/tool-schemas.ts +++ b/packages/core/src/tool-schemas.ts @@ -1,14 +1,19 @@ -import { Type } from "typebox"; +import { Type, type TLiteral } from "typebox"; +import { effortInputValues } from "./model.js"; const mode = Type.Optional(Type.Union([Type.Literal("agent"), Type.Literal("plan")])); -const effort = Type.Optional( - Type.Union( - [Type.Literal("low"), Type.Literal("med"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh")], - { - description: - "Reasoning effort. Tool spelling: low|med|high|xhigh. med is sent as Cursor medium when the catalog lists medium. Grok 4.7 uses reasoning_effort.", - }, - ), + +function effortUnion(description: string) { + const literals = effortInputValues().map((value) => Type.Literal(value)) as [ + TLiteral, + TLiteral, + ...TLiteral[], + ]; + return Type.Optional(Type.Union(literals, { description })); +} + +const effort = effortUnion( + "Effort spelling from the alias table. The table maps it to one level; that level's preferred value is what the catalog receives.", ); const fast = Type.Optional( Type.Boolean({ @@ -81,12 +86,7 @@ export const replyParamsSchema = Type.Object( description: "Not accepted mid-agent. POST /runs has no model field — fails with model_locked.", }), ), - effort: Type.Optional( - Type.Union( - [Type.Literal("low"), Type.Literal("med"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh")], - { description: "Not accepted mid-agent. Fails with model_locked. Launch a fresh agent to change effort." }, - ), - ), + effort: effortUnion("Not accepted mid-agent. Fails with model_locked. Launch a fresh agent to change effort."), fast: Type.Optional( Type.Boolean({ description: "Not accepted mid-agent. Fails with model_locked. Launch a fresh agent to change fast.", diff --git a/packages/plugin/skills/cursor-cloud/SKILL.md b/packages/plugin/skills/cursor-cloud/SKILL.md index c50f69d..146d1c8 100644 --- a/packages/plugin/skills/cursor-cloud/SKILL.md +++ b/packages/plugin/skills/cursor-cloud/SKILL.md @@ -22,7 +22,7 @@ instead. 3. `cursor_cloud_launch` with the user prompt. Do not pass `fresh`/`env`/`agentId` unless the user already chose. 4. If the result `phase` is `choose`, ask the user `ask.question` and the `ask.options` labels. Then recall `cursor_cloud_launch` with that option's `recall` fields. 5. When `phase` is `launched` or `reused`, give the human `agent.id` (`bc-…`) and `agent.url`. Do not wait in this turn. Watch notify is not the transcript. -6. For Grok 4.6 Med: `model=grok-4.6` and `effort=med` (the tool sends Cursor `medium`). `effort=xhigh` is Extra High. Optional `fast=true|false` (omit = Cursor default, often fast). Do not pass model/effort/fast on `cursor_cloud_reply` (`model_locked`). +6. Pass `effort` as a spelling from the alias table (`low`, `med`, `high`, `xhigh`, or a catalog alias such as `medium`). The table picks the value that model lists. Example: `model=grok-4.6` and `effort=med` sends `medium` when that value is listed. Omit `fast` to keep the model default. Do not pass model, effort, or fast on `cursor_cloud_reply` (`model_locked`). ## Placement (plugin-owned) diff --git a/packages/plugin/src/plugin.ts b/packages/plugin/src/plugin.ts index 26770e4..d049e1b 100644 --- a/packages/plugin/src/plugin.ts +++ b/packages/plugin/src/plugin.ts @@ -172,7 +172,7 @@ const plugin = defineToolPlugin({ name: "cursor_cloud_models", label: "Cursor Cloud Models", description: - "List model ids, parameters, and labeled variants. Grok 4.6 Med is grok-4.6 + effort=med (sent as medium). xhigh is Extra High.", + "List model ids, parameters, and labeled variants. Effort spellings map through the alias table onto each model's catalog value.", parameters: emptyParamsSchema, execute: (_params, config) => runTool(config, (runtime) => modelsAction(runtime)), }),