@mushi-mushi/plugin-cursor-cloud 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +168 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kenji Sakuramoto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# @mushi-mushi/plugin-cursor-cloud
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@mushi-mushi/plugin-cursor-cloud)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](./src/index.ts)
|
|
6
|
+
|
|
7
|
+
> Mushi Mushi plugin: dispatch a [Cursor Cloud Agent](https://cursor.com/docs/cloud-agent/api/v0) when a critical bug is classified or a fix is requested. The agent investigates, opens a draft PR, and reports back through the Cursor REST API.
|
|
8
|
+
|
|
9
|
+
This is the **opt-in marketplace** path. Teams install it in the Mushi admin
|
|
10
|
+
console, configure their Cursor API key + workspace ID + repo URL, and pick a
|
|
11
|
+
severity threshold. For the **project-wide default** Cursor agent path (no
|
|
12
|
+
plugin install needed), see `@mushi-mushi/agents`'s `cursor-cloud` adapter.
|
|
13
|
+
|
|
14
|
+
## Why this exists
|
|
15
|
+
|
|
16
|
+
A `report.classified` event firing with `severity: critical` should not wait
|
|
17
|
+
for a human to triage. This plugin watches that event (and `fix.requested`)
|
|
18
|
+
and asks Cursor's hosted agent to take a first pass — investigate, open a
|
|
19
|
+
draft PR, and stop. A human reviews the PR before it merges.
|
|
20
|
+
|
|
21
|
+
The flow:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Mushi report → classify-report → report.classified (severity=critical)
|
|
25
|
+
→ @mushi-mushi/plugin-cursor-cloud
|
|
26
|
+
→ POST https://api.cursor.com/v0/agents
|
|
27
|
+
→ Cursor cloud agent investigates → opens draft PR
|
|
28
|
+
→ audit log to Mushi console
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Spend safety
|
|
32
|
+
|
|
33
|
+
This plugin spends real Cursor API credit. Three hard guards are baked in:
|
|
34
|
+
|
|
35
|
+
1. **Severity gate** — defaults to `critical` only. Set
|
|
36
|
+
`severityThreshold: 'low'` to widen, but think before doing so.
|
|
37
|
+
2. **Repo gate** — silently no-ops when no `repoUrl` is configured.
|
|
38
|
+
3. **Idempotent retries** — `withRetry` from `@mushi-mushi/plugin-sdk`
|
|
39
|
+
honours `Retry-After` and treats 4xx (other than 429) as non-retryable
|
|
40
|
+
so a bad config never burns money in a tight loop.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pnpm add @mushi-mushi/plugin-cursor-cloud @mushi-mushi/plugin-sdk
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Use
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createCursorCloudPlugin } from '@mushi-mushi/plugin-cursor-cloud'
|
|
52
|
+
|
|
53
|
+
const handler = createCursorCloudPlugin({
|
|
54
|
+
apiKey: process.env.CURSOR_API_KEY!,
|
|
55
|
+
workspaceId: process.env.CURSOR_WORKSPACE_ID!,
|
|
56
|
+
webhookSecret: process.env.MUSHI_PLUGIN_WEBHOOK_SECRET!,
|
|
57
|
+
repoUrl: 'https://github.com/your-org/your-repo',
|
|
58
|
+
severityThreshold: 'critical',
|
|
59
|
+
// Optional — default is composer-2.5.
|
|
60
|
+
model: 'composer-2.5',
|
|
61
|
+
// Optional — default is true (auto-open draft PR).
|
|
62
|
+
autoCreatePR: true,
|
|
63
|
+
// Optional — default is 1 (single iteration before reporting back).
|
|
64
|
+
maxIterations: 1,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Wire to your serverless platform — Mushi calls handler with a Standard
|
|
68
|
+
// Webhooks-signed POST body.
|
|
69
|
+
export default handler
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
| Field | Type | Required | Default | Description |
|
|
75
|
+
|---|---|---|---|---|
|
|
76
|
+
| `apiKey` | string | yes | — | Your Cursor API key (`cur_…`). |
|
|
77
|
+
| `workspaceId` | string | yes | — | Your Cursor workspace ID (`ws_…`). |
|
|
78
|
+
| `webhookSecret` | string | yes* | `MUSHI_PLUGIN_WEBHOOK_SECRET` | Standard Webhooks HMAC secret for inbound Mushi events. *Required for self-hosted installs. |
|
|
79
|
+
| `repoUrl` | string | recommended | — | Target repo URL. If omitted, the plugin silently no-ops. |
|
|
80
|
+
| `severityThreshold` | `'low' \| 'medium' \| 'high' \| 'critical'` | no | `'critical'` | Minimum severity that triggers a run. |
|
|
81
|
+
| `model` | string | no | `'composer-2.5'` | Cursor model slug. |
|
|
82
|
+
| `autoCreatePR` | boolean | no | `true` | Whether the agent should open a draft PR. |
|
|
83
|
+
| `maxIterations` | number | no | `1` | Max agent iterations per run. |
|
|
84
|
+
| `fetchImpl` | `typeof fetch` | no | global `fetch` | Override for tests. |
|
|
85
|
+
|
|
86
|
+
## Events handled
|
|
87
|
+
|
|
88
|
+
| Event | Action |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `report.classified` | Dispatch a run when `data.classification.severity` ≥ `severityThreshold`. |
|
|
91
|
+
| `fix.requested` | Always dispatch a run (the user explicitly asked for a fix). |
|
|
92
|
+
| `qa_story.failed` | Dispatch a run when a QA story run fails all its assertions. Requires `repoUrl`. |
|
|
93
|
+
|
|
94
|
+
Other events are ignored.
|
|
95
|
+
|
|
96
|
+
## ⚠️ Breaking change in v0.3.0 — `webhookSecret` required
|
|
97
|
+
|
|
98
|
+
Previous versions derived the HMAC secret from `workspaceId` internally,
|
|
99
|
+
which was trivially forgeable. From v0.3.0 onward you **must** supply
|
|
100
|
+
`webhookSecret` (or `MUSHI_PLUGIN_WEBHOOK_SECRET`):
|
|
101
|
+
|
|
102
|
+
```diff
|
|
103
|
+
const handler = createCursorCloudPlugin({
|
|
104
|
+
apiKey: process.env.CURSOR_API_KEY!,
|
|
105
|
+
workspaceId: process.env.CURSOR_WORKSPACE_ID!,
|
|
106
|
+
+ webhookSecret: process.env.MUSHI_PLUGIN_WEBHOOK_SECRET!,
|
|
107
|
+
repoUrl: 'https://github.com/your-org/your-repo',
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The secret is the same Standard Webhooks HMAC key Mushi signs outbound
|
|
112
|
+
events with. Find it in Admin → Integrations → Cursor Cloud → Webhook secret.
|
|
113
|
+
|
|
114
|
+
## Audit log
|
|
115
|
+
|
|
116
|
+
Every dispatch writes a structured audit line:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
[cursor-cloud] dispatched {"agentId":"agent_…","reportId":"…","model":"composer-2.5"}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Subscribe to the Mushi `plugin.dispatched` audit stream to track spend.
|
|
123
|
+
|
|
124
|
+
## Tests
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pnpm --filter @mushi-mushi/plugin-cursor-cloud test
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The test suite covers:
|
|
131
|
+
|
|
132
|
+
- Severity gate filters out anything below `severityThreshold` (no API call).
|
|
133
|
+
- `repoUrl` missing → silent no-op.
|
|
134
|
+
- `report.classified` with `severity: critical` → exactly one Cursor API call.
|
|
135
|
+
- `fix.requested` → exactly one Cursor API call regardless of severity.
|
|
136
|
+
- Retries on 503; bails on 401 (no money burned on bad keys).
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as _mushi_mushi_plugin_sdk from '@mushi-mushi/plugin-sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cursor Cloud Agent plugin for Mushi Mushi.
|
|
5
|
+
*
|
|
6
|
+
* Path A: Marketplace plugin — subscribes to `report.classified`,
|
|
7
|
+
* `qa_story.failed`, and `fix.requested`. When a qualifying event fires,
|
|
8
|
+
* this plugin calls Cursor's HTTP REST API directly (no @cursor/sdk —
|
|
9
|
+
* runs from Deno edge functions and Node alike) to create a Cloud Agent
|
|
10
|
+
* run, and logs the resulting `agentId` + `prUrl` to the Mushi console.
|
|
11
|
+
*
|
|
12
|
+
* This is the *opt-in* path: teams install it from the Marketplace and
|
|
13
|
+
* configure their Cursor API key + workspace ID in the plugin settings.
|
|
14
|
+
* For the project-wide *default* Cursor agent path, see Path B in
|
|
15
|
+
* packages/agents/src/adapters/cursor-cloud.ts.
|
|
16
|
+
*
|
|
17
|
+
* Cursor REST API docs: https://cursor.com/docs/cloud-agent/api/v0.md
|
|
18
|
+
*/
|
|
19
|
+
interface CursorCloudPluginConfig {
|
|
20
|
+
/** Cursor API key (crsr_…). */
|
|
21
|
+
apiKey: string;
|
|
22
|
+
/** @deprecated No longer required by Cursor Cloud Agents API. */
|
|
23
|
+
workspaceId?: string;
|
|
24
|
+
/** Cursor model slug. Defaults to `composer-2.5`. */
|
|
25
|
+
model?: string;
|
|
26
|
+
/** Whether the agent should automatically open a draft PR. Defaults to true. */
|
|
27
|
+
autoCreatePR?: boolean;
|
|
28
|
+
/** Maximum iterations for the agent. Defaults to 1. */
|
|
29
|
+
maxIterations?: number;
|
|
30
|
+
/** Lowest severity that triggers a Cursor agent run. Defaults to `critical`. */
|
|
31
|
+
severityThreshold?: 'low' | 'medium' | 'high' | 'critical';
|
|
32
|
+
/**
|
|
33
|
+
* URL of the target repository the Cursor agent will work in.
|
|
34
|
+
* If not set, the plugin will attempt to read it from the event payload.
|
|
35
|
+
*/
|
|
36
|
+
repoUrl?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Standard Webhooks secret for inbound Mushi events. Required for
|
|
39
|
+
* self-hosted installs — never derive this from workspaceId.
|
|
40
|
+
*/
|
|
41
|
+
webhookSecret?: string;
|
|
42
|
+
/** Override `fetch` for tests. */
|
|
43
|
+
fetchImpl?: typeof fetch;
|
|
44
|
+
}
|
|
45
|
+
interface CursorAgentRunResponse {
|
|
46
|
+
agentId: string;
|
|
47
|
+
status: string;
|
|
48
|
+
prUrl?: string;
|
|
49
|
+
}
|
|
50
|
+
interface CursorDispatchResult {
|
|
51
|
+
agentId: string;
|
|
52
|
+
reportId: string;
|
|
53
|
+
model: string;
|
|
54
|
+
}
|
|
55
|
+
/** Build the plugin handler and return a callable function. */
|
|
56
|
+
declare function createCursorCloudPlugin(cfg: CursorCloudPluginConfig): (input: _mushi_mushi_plugin_sdk.HandlePluginRequestInput) => Promise<_mushi_mushi_plugin_sdk.HandlePluginResult>;
|
|
57
|
+
|
|
58
|
+
export { type CursorAgentRunResponse, type CursorCloudPluginConfig, type CursorDispatchResult, createCursorCloudPlugin };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
createPluginHandler,
|
|
4
|
+
withRetry
|
|
5
|
+
} from "@mushi-mushi/plugin-sdk";
|
|
6
|
+
var CURSOR_API_BASE = "https://api.cursor.com/v0";
|
|
7
|
+
var SEVERITY_RANK = {
|
|
8
|
+
low: 1,
|
|
9
|
+
medium: 2,
|
|
10
|
+
high: 3,
|
|
11
|
+
critical: 4
|
|
12
|
+
};
|
|
13
|
+
async function createCursorAgentRun(cfg, opts, f) {
|
|
14
|
+
return withRetry(async () => {
|
|
15
|
+
const res = await f(`${CURSOR_API_BASE}/agents`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `Bearer ${cfg.apiKey}`
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
prompt: { text: opts.prompt },
|
|
23
|
+
model: cfg.model || "default",
|
|
24
|
+
source: {
|
|
25
|
+
repository: opts.repoUrl,
|
|
26
|
+
ref: "main"
|
|
27
|
+
},
|
|
28
|
+
target: {
|
|
29
|
+
autoCreatePr: cfg.autoCreatePR,
|
|
30
|
+
branchName: opts.branchName ?? `mushi/cursor-${Date.now()}`,
|
|
31
|
+
skipReviewerRequest: true
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw res;
|
|
37
|
+
}
|
|
38
|
+
const body = await res.json();
|
|
39
|
+
return {
|
|
40
|
+
agentId: body.agentId ?? body.id ?? "",
|
|
41
|
+
status: body.status ?? "queued",
|
|
42
|
+
prUrl: body.prUrl
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function buildPromptFromClassifiedReport(envelope, data) {
|
|
47
|
+
const lines = [
|
|
48
|
+
`A bug has been reported in your project and classified as ${data.classification.severity?.toUpperCase()}.`,
|
|
49
|
+
``,
|
|
50
|
+
`Report ID: ${data.report.id}`,
|
|
51
|
+
`Title: ${data.report.title ?? "(untitled)"}`,
|
|
52
|
+
`Category: ${data.report.category ?? data.classification.category}`,
|
|
53
|
+
`Severity: ${data.classification.severity}`,
|
|
54
|
+
`Project: ${envelope.projectId}`
|
|
55
|
+
];
|
|
56
|
+
if (data.classification.tags?.length) {
|
|
57
|
+
lines.push(`Tags: ${data.classification.tags.join(", ")}`);
|
|
58
|
+
}
|
|
59
|
+
lines.push(
|
|
60
|
+
``,
|
|
61
|
+
`Please investigate this bug, identify the root cause in the codebase, and open a draft PR with a minimal fix. Do not refactor unrelated code.`
|
|
62
|
+
);
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
function buildPromptFromFixRequested(envelope, fixData) {
|
|
66
|
+
return [
|
|
67
|
+
`A fix was requested for report ${fixData.report.id} in project ${envelope.projectId}.`,
|
|
68
|
+
``,
|
|
69
|
+
`Report title: ${fixData.report.title ?? "(untitled)"}`,
|
|
70
|
+
`Fix ID: ${fixData.fix.id}`,
|
|
71
|
+
``,
|
|
72
|
+
`Please investigate the reported issue, identify the root cause, and open a draft PR with a minimal fix. Do not refactor unrelated code.`
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
function buildPromptFromQaStoryFailed(envelope, data) {
|
|
76
|
+
return [
|
|
77
|
+
`A QA story failed in project ${envelope.projectId}.`,
|
|
78
|
+
``,
|
|
79
|
+
`Story ID: ${data.storyId ?? "unknown"}`,
|
|
80
|
+
`Title: ${data.title ?? "(untitled)"}`,
|
|
81
|
+
data.failureReason ? `Failure: ${data.failureReason}` : "",
|
|
82
|
+
``,
|
|
83
|
+
`Please investigate the failing assertion, identify the root cause in the codebase, and open a draft PR with a minimal fix. Do not refactor unrelated code.`
|
|
84
|
+
].filter((line) => line !== "").join("\n");
|
|
85
|
+
}
|
|
86
|
+
function createCursorCloudPlugin(cfg) {
|
|
87
|
+
const minRank = SEVERITY_RANK[cfg.severityThreshold ?? "critical"];
|
|
88
|
+
const model = cfg.model ?? "composer-2.5";
|
|
89
|
+
const autoCreatePR = cfg.autoCreatePR ?? true;
|
|
90
|
+
const maxIterations = cfg.maxIterations ?? 1;
|
|
91
|
+
const f = cfg.fetchImpl ?? fetch;
|
|
92
|
+
const resolvedCfg = { apiKey: cfg.apiKey, model, autoCreatePR, maxIterations };
|
|
93
|
+
const webhookSecret = cfg.webhookSecret ?? (typeof process !== "undefined" ? process.env.MUSHI_PLUGIN_WEBHOOK_SECRET : void 0);
|
|
94
|
+
if (!webhookSecret) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"Cursor Cloud plugin requires webhookSecret (or MUSHI_PLUGIN_WEBHOOK_SECRET) for inbound event verification."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return createPluginHandler({
|
|
100
|
+
secret: webhookSecret,
|
|
101
|
+
on: {
|
|
102
|
+
"report.classified": async (e) => {
|
|
103
|
+
const data = e.data;
|
|
104
|
+
const rank = SEVERITY_RANK[data.classification.severity ?? ""] ?? 0;
|
|
105
|
+
if (rank < minRank) return;
|
|
106
|
+
const repoUrl = cfg.repoUrl ?? "";
|
|
107
|
+
if (!repoUrl) {
|
|
108
|
+
console.warn("[cursor-cloud] report.classified skipped: repoUrl not configured. Set repoUrl in the plugin config so the agent knows which codebase to fix.");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const run = await createCursorAgentRun(
|
|
112
|
+
resolvedCfg,
|
|
113
|
+
{
|
|
114
|
+
repoUrl,
|
|
115
|
+
prompt: buildPromptFromClassifiedReport(e, data)
|
|
116
|
+
},
|
|
117
|
+
f
|
|
118
|
+
);
|
|
119
|
+
const result = { agentId: run.agentId, reportId: data.report.id, model };
|
|
120
|
+
console.warn("[cursor-cloud] dispatched", JSON.stringify(result));
|
|
121
|
+
},
|
|
122
|
+
"fix.requested": async (e) => {
|
|
123
|
+
const data = e.data;
|
|
124
|
+
const repoUrl = cfg.repoUrl ?? "";
|
|
125
|
+
if (!repoUrl) {
|
|
126
|
+
console.warn("[cursor-cloud] fix.requested skipped: repoUrl not configured.");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const run = await createCursorAgentRun(
|
|
130
|
+
resolvedCfg,
|
|
131
|
+
{
|
|
132
|
+
repoUrl,
|
|
133
|
+
prompt: buildPromptFromFixRequested(e, data)
|
|
134
|
+
},
|
|
135
|
+
f
|
|
136
|
+
);
|
|
137
|
+
const result = { agentId: run.agentId, reportId: data.report.id, model };
|
|
138
|
+
console.warn("[cursor-cloud] fix.requested dispatched", JSON.stringify(result));
|
|
139
|
+
},
|
|
140
|
+
"qa_story.failed": async (e) => {
|
|
141
|
+
const data = e.data;
|
|
142
|
+
const repoUrl = cfg.repoUrl ?? "";
|
|
143
|
+
if (!repoUrl) {
|
|
144
|
+
console.warn("[cursor-cloud] qa_story.failed skipped: repoUrl not configured.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const run = await createCursorAgentRun(
|
|
148
|
+
resolvedCfg,
|
|
149
|
+
{
|
|
150
|
+
repoUrl,
|
|
151
|
+
prompt: buildPromptFromQaStoryFailed(e, data)
|
|
152
|
+
},
|
|
153
|
+
f
|
|
154
|
+
);
|
|
155
|
+
const result = {
|
|
156
|
+
agentId: run.agentId,
|
|
157
|
+
reportId: data.storyId ?? "unknown",
|
|
158
|
+
model
|
|
159
|
+
};
|
|
160
|
+
console.warn("[cursor-cloud] qa_story.failed dispatched", JSON.stringify(result));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export {
|
|
166
|
+
createCursorCloudPlugin
|
|
167
|
+
};
|
|
168
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Cursor Cloud Agent plugin for Mushi Mushi.\n *\n * Path A: Marketplace plugin — subscribes to `report.classified`,\n * `qa_story.failed`, and `fix.requested`. When a qualifying event fires,\n * this plugin calls Cursor's HTTP REST API directly (no @cursor/sdk —\n * runs from Deno edge functions and Node alike) to create a Cloud Agent\n * run, and logs the resulting `agentId` + `prUrl` to the Mushi console.\n *\n * This is the *opt-in* path: teams install it from the Marketplace and\n * configure their Cursor API key + workspace ID in the plugin settings.\n * For the project-wide *default* Cursor agent path, see Path B in\n * packages/agents/src/adapters/cursor-cloud.ts.\n *\n * Cursor REST API docs: https://cursor.com/docs/cloud-agent/api/v0.md\n */\n\nimport {\n createPluginHandler,\n withRetry,\n type MushiEventEnvelope,\n type MushiReportClassifiedEvent,\n type MushiFixEvent,\n} from '@mushi-mushi/plugin-sdk'\n\nconst CURSOR_API_BASE = 'https://api.cursor.com/v0'\n\nconst SEVERITY_RANK: Record<string, number> = {\n low: 1,\n medium: 2,\n high: 3,\n critical: 4,\n}\n\nexport interface CursorCloudPluginConfig {\n /** Cursor API key (crsr_…). */\n apiKey: string\n /** @deprecated No longer required by Cursor Cloud Agents API. */\n workspaceId?: string\n /** Cursor model slug. Defaults to `composer-2.5`. */\n model?: string\n /** Whether the agent should automatically open a draft PR. Defaults to true. */\n autoCreatePR?: boolean\n /** Maximum iterations for the agent. Defaults to 1. */\n maxIterations?: number\n /** Lowest severity that triggers a Cursor agent run. Defaults to `critical`. */\n severityThreshold?: 'low' | 'medium' | 'high' | 'critical'\n /**\n * URL of the target repository the Cursor agent will work in.\n * If not set, the plugin will attempt to read it from the event payload.\n */\n repoUrl?: string\n /**\n * Standard Webhooks secret for inbound Mushi events. Required for\n * self-hosted installs — never derive this from workspaceId.\n */\n webhookSecret?: string\n /** Override `fetch` for tests. */\n fetchImpl?: typeof fetch\n}\n\nexport interface CursorAgentRunResponse {\n agentId: string\n status: string\n prUrl?: string\n}\n\nexport interface CursorDispatchResult {\n agentId: string\n reportId: string\n model: string\n}\n\n/**\n * Call Cursor's REST API to create a Cloud Agent run.\n * Returns the agentId so the caller can track the run in Mushi console.\n *\n * On non-2xx the raw Response is thrown so `withRetry` can inspect\n * `.status` and `Retry-After` and apply the documented retry policy:\n * - 429 / 5xx → retry with exponential back-off\n * - 4xx (not 429) → throw immediately (no money burned on bad keys)\n * - network error → retry\n */\nasync function createCursorAgentRun(\n cfg: Required<Pick<CursorCloudPluginConfig, 'apiKey' | 'model' | 'autoCreatePR' | 'maxIterations'>>,\n opts: { repoUrl: string; prompt: string; branchName?: string },\n f: typeof fetch,\n): Promise<CursorAgentRunResponse> {\n return withRetry(async () => {\n const res = await f(`${CURSOR_API_BASE}/agents`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${cfg.apiKey}`,\n },\n body: JSON.stringify({\n prompt: { text: opts.prompt },\n model: cfg.model || 'default',\n source: {\n repository: opts.repoUrl,\n ref: 'main',\n },\n target: {\n autoCreatePr: cfg.autoCreatePR,\n branchName: opts.branchName ?? `mushi/cursor-${Date.now()}`,\n skipReviewerRequest: true,\n },\n }),\n })\n if (!res.ok) {\n // Throw the Response itself so withRetry can read status / Retry-After.\n // This is the contract documented in @mushi-mushi/plugin-sdk/retry.ts.\n throw res\n }\n const body = (await res.json()) as { agentId?: string; id?: string; status?: string; prUrl?: string }\n return {\n agentId: body.agentId ?? body.id ?? '',\n status: body.status ?? 'queued',\n prUrl: body.prUrl,\n }\n })\n}\n\nfunction buildPromptFromClassifiedReport(\n envelope: MushiEventEnvelope,\n data: MushiReportClassifiedEvent,\n): string {\n const lines = [\n `A bug has been reported in your project and classified as ${data.classification.severity?.toUpperCase()}.`,\n ``,\n `Report ID: ${data.report.id}`,\n `Title: ${data.report.title ?? '(untitled)'}`,\n `Category: ${data.report.category ?? data.classification.category}`,\n `Severity: ${data.classification.severity}`,\n `Project: ${envelope.projectId}`,\n ]\n\n if (data.classification.tags?.length) {\n lines.push(`Tags: ${data.classification.tags.join(', ')}`)\n }\n\n lines.push(\n ``,\n `Please investigate this bug, identify the root cause in the codebase, ` +\n `and open a draft PR with a minimal fix. Do not refactor unrelated code.`,\n )\n\n return lines.join('\\n')\n}\n\nfunction buildPromptFromFixRequested(envelope: MushiEventEnvelope, fixData: MushiFixEvent): string {\n return [\n `A fix was requested for report ${fixData.report.id} in project ${envelope.projectId}.`,\n ``,\n `Report title: ${fixData.report.title ?? '(untitled)'}`,\n `Fix ID: ${fixData.fix.id}`,\n ``,\n `Please investigate the reported issue, identify the root cause, ` +\n `and open a draft PR with a minimal fix. Do not refactor unrelated code.`,\n ].join('\\n')\n}\n\nfunction buildPromptFromQaStoryFailed(\n envelope: MushiEventEnvelope,\n data: { storyId?: string; title?: string; failureReason?: string },\n): string {\n return [\n `A QA story failed in project ${envelope.projectId}.`,\n ``,\n `Story ID: ${data.storyId ?? 'unknown'}`,\n `Title: ${data.title ?? '(untitled)'}`,\n data.failureReason ? `Failure: ${data.failureReason}` : '',\n ``,\n `Please investigate the failing assertion, identify the root cause in the codebase, ` +\n `and open a draft PR with a minimal fix. Do not refactor unrelated code.`,\n ].filter((line) => line !== '').join('\\n')\n}\n\n/** Build the plugin handler and return a callable function. */\nexport function createCursorCloudPlugin(cfg: CursorCloudPluginConfig) {\n const minRank = SEVERITY_RANK[cfg.severityThreshold ?? 'critical']!\n const model = cfg.model ?? 'composer-2.5'\n const autoCreatePR = cfg.autoCreatePR ?? true\n const maxIterations = cfg.maxIterations ?? 1\n const f = cfg.fetchImpl ?? fetch\n\n const resolvedCfg = { apiKey: cfg.apiKey, model, autoCreatePR, maxIterations }\n\n const webhookSecret =\n cfg.webhookSecret ??\n (typeof process !== 'undefined' ? process.env.MUSHI_PLUGIN_WEBHOOK_SECRET : undefined)\n if (!webhookSecret) {\n throw new Error(\n 'Cursor Cloud plugin requires webhookSecret (or MUSHI_PLUGIN_WEBHOOK_SECRET) for inbound event verification.',\n )\n }\n\n return createPluginHandler({\n secret: webhookSecret,\n on: {\n 'report.classified': async (e) => {\n const data = e.data as MushiReportClassifiedEvent\n const rank = SEVERITY_RANK[data.classification.severity ?? ''] ?? 0\n if (rank < minRank) return\n\n const repoUrl = cfg.repoUrl ?? ''\n if (!repoUrl) {\n console.warn('[cursor-cloud] report.classified skipped: repoUrl not configured. ' +\n 'Set repoUrl in the plugin config so the agent knows which codebase to fix.')\n return\n }\n\n const run = await createCursorAgentRun(\n resolvedCfg,\n {\n repoUrl,\n prompt: buildPromptFromClassifiedReport(e, data),\n },\n f,\n )\n\n // Emit audit log — handler return type is void.\n const result: CursorDispatchResult = { agentId: run.agentId, reportId: data.report.id, model }\n console.warn('[cursor-cloud] dispatched', JSON.stringify(result))\n },\n\n 'fix.requested': async (e) => {\n const data = e.data as MushiFixEvent\n\n const repoUrl = cfg.repoUrl ?? ''\n if (!repoUrl) {\n console.warn('[cursor-cloud] fix.requested skipped: repoUrl not configured.')\n return\n }\n\n const run = await createCursorAgentRun(\n resolvedCfg,\n {\n repoUrl,\n prompt: buildPromptFromFixRequested(e, data),\n },\n f,\n )\n\n // Emit audit log — handler return type is void.\n const result: CursorDispatchResult = { agentId: run.agentId, reportId: data.report.id, model }\n console.warn('[cursor-cloud] fix.requested dispatched', JSON.stringify(result))\n },\n\n 'qa_story.failed': async (e) => {\n const data = e.data as { storyId?: string; title?: string; failureReason?: string }\n\n const repoUrl = cfg.repoUrl ?? ''\n if (!repoUrl) {\n console.warn('[cursor-cloud] qa_story.failed skipped: repoUrl not configured.')\n return\n }\n\n const run = await createCursorAgentRun(\n resolvedCfg,\n {\n repoUrl,\n prompt: buildPromptFromQaStoryFailed(e, data),\n },\n f,\n )\n\n const result: CursorDispatchResult = {\n agentId: run.agentId,\n reportId: data.storyId ?? 'unknown',\n model,\n }\n console.warn('[cursor-cloud] qa_story.failed dispatched', JSON.stringify(result))\n },\n },\n })\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mushi-mushi/plugin-cursor-cloud",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Mushi Mushi plugin: dispatch a Cursor Cloud Agent when a critical bug is classified or a QA story fails \u00e2\u20ac\u201d opens a signed draft PR automatically.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"lint": "eslint src/",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@mushi-mushi/plugin-sdk": "workspace:^"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@mushi-mushi/eslint-config": "workspace:*",
|
|
32
|
+
"@types/node": "^22.19.17",
|
|
33
|
+
"eslint": "^10.3.0",
|
|
34
|
+
"tsup": "^8.5.1",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"vitest": "^4.1.5"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/kensaurus/mushi-mushi.git",
|
|
41
|
+
"directory": "packages/plugin-cursor-cloud"
|
|
42
|
+
},
|
|
43
|
+
"author": "Kenji Sakuramoto",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"sideEffects": false,
|
|
48
|
+
"keywords": [
|
|
49
|
+
"mushi-mushi",
|
|
50
|
+
"cursor",
|
|
51
|
+
"cursor-cloud",
|
|
52
|
+
"plugin",
|
|
53
|
+
"agent",
|
|
54
|
+
"autofix",
|
|
55
|
+
"ai-agent",
|
|
56
|
+
"integration"
|
|
57
|
+
],
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20"
|
|
60
|
+
}
|
|
61
|
+
}
|