@meetopenbot/cursor 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/config.js +44 -0
- package/dist/format.js +161 -0
- package/dist/index.js +132 -0
- package/dist/session.js +78 -0
- package/dist/state.js +21 -0
- package/dist/stream.js +76 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @meetopenbot/cursor
|
|
2
|
+
|
|
3
|
+
OpenBot agent plugin powered by the [Cursor TypeScript SDK](https://cursor.com/docs/sdk/typescript).
|
|
4
|
+
|
|
5
|
+
Cursor provides a full agent runtime with built-in tools (shell, read, edit, write, grep, and more), session management, and local or cloud execution. This plugin wraps Cursor as an OpenBot **agent runtime** — each `agent:invoke` runs a Cursor turn and streams progress back via `agent:output`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @meetopenbot/cursor
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Add the plugin to your agent in `AGENT.md`:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
plugins:
|
|
17
|
+
- id: '@meetopenbot/cursor'
|
|
18
|
+
config:
|
|
19
|
+
model: composer-2.5
|
|
20
|
+
thinking: high
|
|
21
|
+
runtime: local
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
| Option | Description |
|
|
27
|
+
|--------|-------------|
|
|
28
|
+
| `apiKey` | Cursor API key. Falls back to `CURSOR_API_KEY`. |
|
|
29
|
+
| `runtime` | `local` (default) or `cloud`. |
|
|
30
|
+
| `model` | Model id (default: `composer-2.5`). |
|
|
31
|
+
| `thinking` | Reasoning effort for models that support it (e.g. `low`, `high`). |
|
|
32
|
+
| `mode` | `agent` (default) or `plan`. |
|
|
33
|
+
| `cwd` | Working directory for local agents. Defaults to the OpenBot channel `cwd`, then `process.cwd()`. |
|
|
34
|
+
| `repoUrl` | Git repository URL for cloud agents. |
|
|
35
|
+
| `startingRef` | Git ref to clone for cloud agents (e.g. `main`). |
|
|
36
|
+
| `autoCreatePR` | Open a pull request when a cloud run finishes. |
|
|
37
|
+
| `workOnCurrentBranch` | Push cloud commits to the existing branch. |
|
|
38
|
+
| `settingSources` | Comma-separated local settings layers: `project`, `user`, `team`, `mdm`, `plugins`, `all`. |
|
|
39
|
+
| `sandbox` | Enable the local agent sandbox. |
|
|
40
|
+
| `autoReview` | Route local tool calls through Auto-review. |
|
|
41
|
+
|
|
42
|
+
## API Keys
|
|
43
|
+
|
|
44
|
+
Set `CURSOR_API_KEY` in the environment or pass `apiKey` in plugin config. Generate a key from [Cursor Dashboard → API Keys](https://cursor.com/dashboard/api).
|
|
45
|
+
|
|
46
|
+
## Sessions
|
|
47
|
+
|
|
48
|
+
Each OpenBot thread gets its own Cursor agent. The Cursor `agentId` is stored in thread state so conversations continue across turns. The plugin uses `Agent.resume()` to reconnect after restarts.
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const RUNTIMES = new Set(['local', 'cloud']);
|
|
2
|
+
const MODES = new Set(['agent', 'plan']);
|
|
3
|
+
const SETTING_SOURCES = new Set([
|
|
4
|
+
'project',
|
|
5
|
+
'user',
|
|
6
|
+
'team',
|
|
7
|
+
'mdm',
|
|
8
|
+
'plugins',
|
|
9
|
+
'all',
|
|
10
|
+
]);
|
|
11
|
+
export const CURSOR_API_KEY_ENV_VAR = 'CURSOR_API_KEY';
|
|
12
|
+
export const resolveConfig = (context, channelCwd) => {
|
|
13
|
+
const config = context.config;
|
|
14
|
+
const runtime = config.runtime && RUNTIMES.has(config.runtime) ? config.runtime : 'local';
|
|
15
|
+
const mode = config.mode && MODES.has(config.mode) ? config.mode : undefined;
|
|
16
|
+
const settingSources = config.settingSources
|
|
17
|
+
?.split(',')
|
|
18
|
+
.map((source) => source.trim())
|
|
19
|
+
.filter((source) => SETTING_SOURCES.has(source));
|
|
20
|
+
return {
|
|
21
|
+
apiKey: (typeof config.apiKey === 'string' && config.apiKey.trim()) ||
|
|
22
|
+
process.env[CURSOR_API_KEY_ENV_VAR],
|
|
23
|
+
runtime,
|
|
24
|
+
model: (typeof config.model === 'string' && config.model.trim()) || 'composer-2.5',
|
|
25
|
+
thinking: typeof config.thinking === 'string' && config.thinking.trim()
|
|
26
|
+
? config.thinking.trim()
|
|
27
|
+
: undefined,
|
|
28
|
+
mode,
|
|
29
|
+
cwd: channelCwd || config.cwd,
|
|
30
|
+
repoUrl: config.repoUrl?.trim() || undefined,
|
|
31
|
+
startingRef: config.startingRef?.trim() || undefined,
|
|
32
|
+
autoCreatePR: config.autoCreatePR === true,
|
|
33
|
+
workOnCurrentBranch: config.workOnCurrentBranch === true,
|
|
34
|
+
settingSources: settingSources?.length ? settingSources : undefined,
|
|
35
|
+
sandbox: config.sandbox === true,
|
|
36
|
+
autoReview: config.autoReview === true,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export const buildModelSelection = (config) => ({
|
|
40
|
+
id: config.model,
|
|
41
|
+
...(config.thinking
|
|
42
|
+
? { params: [{ id: 'thinking', value: config.thinking }] }
|
|
43
|
+
: {}),
|
|
44
|
+
});
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { CursorSdkError } from '@cursor/sdk';
|
|
2
|
+
export const createStreamState = () => ({
|
|
3
|
+
assistantText: '',
|
|
4
|
+
});
|
|
5
|
+
const truncate = (text, max = 80) => text.length > max ? `${text.slice(0, max - 3)}…` : text;
|
|
6
|
+
const quoteDetail = (detail) => detail.includes('`') ? `"${detail}"` : `\`${detail}\``;
|
|
7
|
+
const strArg = (args, ...keys) => {
|
|
8
|
+
if (!args || typeof args !== 'object')
|
|
9
|
+
return undefined;
|
|
10
|
+
const record = args;
|
|
11
|
+
for (const key of keys) {
|
|
12
|
+
const value = record[key];
|
|
13
|
+
if (typeof value === 'string' && value.trim())
|
|
14
|
+
return value.trim();
|
|
15
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
16
|
+
return String(value);
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
};
|
|
20
|
+
const formatToolDetail = (toolName, args) => {
|
|
21
|
+
switch (toolName) {
|
|
22
|
+
case 'Shell':
|
|
23
|
+
case 'shell': {
|
|
24
|
+
const command = strArg(args, 'command');
|
|
25
|
+
return command ? truncate(command.replace(/\s+/g, ' ')) : undefined;
|
|
26
|
+
}
|
|
27
|
+
case 'Read':
|
|
28
|
+
case 'read': {
|
|
29
|
+
const path = strArg(args, 'path', 'target_file', 'file_path');
|
|
30
|
+
return path ? truncate(path, 120) : undefined;
|
|
31
|
+
}
|
|
32
|
+
case 'Write':
|
|
33
|
+
case 'write':
|
|
34
|
+
case 'Edit':
|
|
35
|
+
case 'edit':
|
|
36
|
+
case 'StrReplace':
|
|
37
|
+
case 'strReplace': {
|
|
38
|
+
const path = strArg(args, 'path', 'target_file', 'file_path');
|
|
39
|
+
return path ? truncate(path, 120) : undefined;
|
|
40
|
+
}
|
|
41
|
+
case 'Grep':
|
|
42
|
+
case 'grep': {
|
|
43
|
+
const pattern = strArg(args, 'pattern');
|
|
44
|
+
const path = strArg(args, 'path') ?? '.';
|
|
45
|
+
if (!pattern)
|
|
46
|
+
return undefined;
|
|
47
|
+
return `${truncate(pattern)} in ${truncate(path, 60)}`;
|
|
48
|
+
}
|
|
49
|
+
case 'Glob':
|
|
50
|
+
case 'glob':
|
|
51
|
+
case 'GlobFileSearch':
|
|
52
|
+
case 'globFileSearch': {
|
|
53
|
+
const pattern = strArg(args, 'glob_pattern', 'pattern');
|
|
54
|
+
return pattern ? truncate(pattern) : undefined;
|
|
55
|
+
}
|
|
56
|
+
case 'LS':
|
|
57
|
+
case 'ls':
|
|
58
|
+
case 'ListDir':
|
|
59
|
+
case 'listDir': {
|
|
60
|
+
const path = strArg(args, 'path', 'target_directory') ?? '.';
|
|
61
|
+
return truncate(path, 120);
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const formatToolStart = (toolName, args) => {
|
|
68
|
+
const detail = formatToolDetail(toolName, args);
|
|
69
|
+
if (detail)
|
|
70
|
+
return `Running **${toolName}** (${quoteDetail(detail)})…`;
|
|
71
|
+
return `Running **${toolName}**…`;
|
|
72
|
+
};
|
|
73
|
+
const formatCloudStatus = (status, message, config) => {
|
|
74
|
+
const isCloud = config?.runtime === 'cloud';
|
|
75
|
+
switch (status) {
|
|
76
|
+
case 'CREATING':
|
|
77
|
+
return isCloud ? 'Starting cloud agent…' : 'Starting local agent…';
|
|
78
|
+
case 'RUNNING':
|
|
79
|
+
return message ? truncate(message) : undefined;
|
|
80
|
+
case 'ERROR':
|
|
81
|
+
return message
|
|
82
|
+
? `**Cursor error:** ${message}`
|
|
83
|
+
: isCloud
|
|
84
|
+
? '**Cursor error:** Cloud run failed.'
|
|
85
|
+
: '**Cursor error:** Local run failed.';
|
|
86
|
+
case 'CANCELLED':
|
|
87
|
+
return isCloud ? 'Cloud run cancelled.' : 'Local run cancelled.';
|
|
88
|
+
case 'EXPIRED':
|
|
89
|
+
return isCloud ? 'Cloud run expired.' : 'Local run expired.';
|
|
90
|
+
case 'FINISHED':
|
|
91
|
+
return undefined;
|
|
92
|
+
default:
|
|
93
|
+
return message ? truncate(message) : undefined;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/** Map Cursor SDK stream events to user-visible output chunks. */
|
|
97
|
+
export const formatCursorEvent = (event, state, config) => {
|
|
98
|
+
switch (event.type) {
|
|
99
|
+
case 'assistant': {
|
|
100
|
+
const text = event.message.content
|
|
101
|
+
.filter((block) => block.type === 'text')
|
|
102
|
+
.map((block) => block.text)
|
|
103
|
+
.join('');
|
|
104
|
+
if (text)
|
|
105
|
+
state.assistantText += text;
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
case 'tool_call': {
|
|
109
|
+
if (event.status === 'running') {
|
|
110
|
+
return formatToolStart(event.name, event.args);
|
|
111
|
+
}
|
|
112
|
+
if (event.status === 'error') {
|
|
113
|
+
const details = JSON.stringify(event);
|
|
114
|
+
return `Tool **${event.name}** failed. Details: \`${details}\``;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
case 'status': {
|
|
119
|
+
if (event.status === 'ERROR') {
|
|
120
|
+
const details = JSON.stringify(event);
|
|
121
|
+
const isCloud = config?.runtime === 'cloud';
|
|
122
|
+
const prefix = isCloud ? 'Cloud run failed' : 'Local run failed';
|
|
123
|
+
return `**Cursor error:** ${prefix}. Event details: \`${details}\``;
|
|
124
|
+
}
|
|
125
|
+
return formatCloudStatus(event.status, event.message, config);
|
|
126
|
+
}
|
|
127
|
+
case 'task': {
|
|
128
|
+
if (!event.text?.trim())
|
|
129
|
+
return undefined;
|
|
130
|
+
return truncate(event.text);
|
|
131
|
+
}
|
|
132
|
+
default:
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
export const formatCursorError = (error) => {
|
|
137
|
+
if (error instanceof CursorSdkError) {
|
|
138
|
+
const parts = [error.message];
|
|
139
|
+
if (error.requestId)
|
|
140
|
+
parts.push(`(requestId: ${error.requestId})`);
|
|
141
|
+
const helpUrl = error.helpUrl;
|
|
142
|
+
if (helpUrl)
|
|
143
|
+
parts.push(`See ${helpUrl}`);
|
|
144
|
+
return parts.join(' ');
|
|
145
|
+
}
|
|
146
|
+
if (error instanceof Error) {
|
|
147
|
+
const extra = { ...error };
|
|
148
|
+
if (Object.keys(extra).length > 0) {
|
|
149
|
+
return `${error.message} (details: ${JSON.stringify(extra)})`;
|
|
150
|
+
}
|
|
151
|
+
return error.message;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
return typeof error === 'object' && error !== null
|
|
155
|
+
? JSON.stringify(error)
|
|
156
|
+
: String(error);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return String(error);
|
|
160
|
+
}
|
|
161
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { agentOutput, definePlugin, shouldHandleInvoke, } from '@meetopenbot/plugin-sdk';
|
|
2
|
+
import { CURSOR_API_KEY_ENV_VAR, resolveConfig, } from './config.js';
|
|
3
|
+
import { formatCursorError } from './format.js';
|
|
4
|
+
import { getOrCreateCursorAgent } from './session.js';
|
|
5
|
+
import { streamCursorPrompt } from './stream.js';
|
|
6
|
+
export default definePlugin({
|
|
7
|
+
id: 'cursor',
|
|
8
|
+
name: 'Cursor',
|
|
9
|
+
description: 'Cursor coding agent — read, edit, and run code via the Cursor SDK.',
|
|
10
|
+
configSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
apiKey: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Cursor API key. Falls back to CURSOR_API_KEY.',
|
|
16
|
+
format: 'password',
|
|
17
|
+
},
|
|
18
|
+
runtime: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Agent runtime: local (default) or cloud.',
|
|
21
|
+
enum: ['local', 'cloud'],
|
|
22
|
+
default: 'local',
|
|
23
|
+
},
|
|
24
|
+
model: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Cursor model id (default: composer-2.5).',
|
|
27
|
+
default: 'composer-2.5',
|
|
28
|
+
},
|
|
29
|
+
thinking: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Reasoning effort for models that support it (e.g. low, high).',
|
|
32
|
+
},
|
|
33
|
+
mode: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Conversation mode: agent (default) or plan.',
|
|
36
|
+
enum: ['agent', 'plan'],
|
|
37
|
+
},
|
|
38
|
+
cwd: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Working directory for local agents.',
|
|
41
|
+
},
|
|
42
|
+
repoUrl: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Git repository URL for cloud agents.',
|
|
45
|
+
format: 'url',
|
|
46
|
+
},
|
|
47
|
+
startingRef: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Git ref to clone for cloud agents (e.g. main).',
|
|
50
|
+
},
|
|
51
|
+
autoCreatePR: {
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
description: 'Open a pull request when a cloud run finishes.',
|
|
54
|
+
default: false,
|
|
55
|
+
},
|
|
56
|
+
workOnCurrentBranch: {
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
description: 'Push cloud commits to the existing branch instead of a new one.',
|
|
59
|
+
default: false,
|
|
60
|
+
},
|
|
61
|
+
settingSources: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Comma-separated local settings layers: project,user,team,mdm,plugins,all.',
|
|
64
|
+
},
|
|
65
|
+
sandbox: {
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
description: 'Enable the local agent sandbox.',
|
|
68
|
+
default: false,
|
|
69
|
+
},
|
|
70
|
+
autoReview: {
|
|
71
|
+
type: 'boolean',
|
|
72
|
+
description: 'Route local tool calls through Auto-review.',
|
|
73
|
+
default: false,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
factory: (context) => (builder) => {
|
|
78
|
+
builder.on('agent:invoke', async function* (event, handlerCtx) {
|
|
79
|
+
if (!shouldHandleInvoke(event, context.agentId))
|
|
80
|
+
return;
|
|
81
|
+
const threadId = event.meta?.threadId ?? handlerCtx.state.threadId;
|
|
82
|
+
const prompt = (event.data.content ?? '').trim();
|
|
83
|
+
if (!prompt) {
|
|
84
|
+
yield agentOutput({
|
|
85
|
+
agentId: context.agentId,
|
|
86
|
+
content: 'Send a message to run Cursor in this workspace — for example, "Summarize this repo" or "Fix the failing test in src/foo.test.ts".',
|
|
87
|
+
threadId,
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const channelCwd = handlerCtx.state.channelDetails?.cwd;
|
|
92
|
+
const config = resolveConfig(context, channelCwd);
|
|
93
|
+
if (!config.apiKey) {
|
|
94
|
+
yield agentOutput({
|
|
95
|
+
agentId: context.agentId,
|
|
96
|
+
content: `Set a Cursor API key in plugin config (\`apiKey\`) or the \`${CURSOR_API_KEY_ENV_VAR}\` environment variable. Get a key from https://cursor.com/dashboard/api`,
|
|
97
|
+
threadId,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (config.runtime === 'local' && !config.cwd && !channelCwd) {
|
|
102
|
+
yield agentOutput({
|
|
103
|
+
agentId: context.agentId,
|
|
104
|
+
content: 'Local Cursor agents need a working directory. Set `cwd` in plugin config or configure a channel `cwd`.',
|
|
105
|
+
threadId,
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const { agent } = await getOrCreateCursorAgent({
|
|
111
|
+
config,
|
|
112
|
+
state: handlerCtx.state,
|
|
113
|
+
storage: context.storage,
|
|
114
|
+
});
|
|
115
|
+
for await (const chunk of streamCursorPrompt(agent, prompt, config)) {
|
|
116
|
+
yield agentOutput({
|
|
117
|
+
agentId: context.agentId,
|
|
118
|
+
content: chunk,
|
|
119
|
+
threadId,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
yield agentOutput({
|
|
125
|
+
agentId: context.agentId,
|
|
126
|
+
content: `**Cursor error:** ${formatCursorError(error)}`,
|
|
127
|
+
threadId,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
});
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Agent, } from '@cursor/sdk';
|
|
2
|
+
import { buildModelSelection, } from './config.js';
|
|
3
|
+
import { formatCursorError } from './format.js';
|
|
4
|
+
import { persistCursorState, readPersistedState } from './state.js';
|
|
5
|
+
const sessionCache = new Map();
|
|
6
|
+
const buildSessionKey = (state) => state.threadId ? `${state.channelId}:${state.threadId}` : state.channelId;
|
|
7
|
+
const buildAgentOptions = (config) => {
|
|
8
|
+
const options = {
|
|
9
|
+
apiKey: config.apiKey,
|
|
10
|
+
model: buildModelSelection(config),
|
|
11
|
+
...(config.mode ? { mode: config.mode } : {}),
|
|
12
|
+
};
|
|
13
|
+
if (config.runtime === 'cloud') {
|
|
14
|
+
options.cloud = {
|
|
15
|
+
...(config.repoUrl
|
|
16
|
+
? {
|
|
17
|
+
repos: [
|
|
18
|
+
{
|
|
19
|
+
url: config.repoUrl,
|
|
20
|
+
...(config.startingRef ? { startingRef: config.startingRef } : {}),
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
: {}),
|
|
25
|
+
autoCreatePR: config.autoCreatePR,
|
|
26
|
+
workOnCurrentBranch: config.workOnCurrentBranch,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
options.local = {
|
|
31
|
+
cwd: config.cwd || process.cwd(),
|
|
32
|
+
...(config.settingSources ? { settingSources: config.settingSources } : {}),
|
|
33
|
+
...(config.sandbox ? { sandboxOptions: { enabled: true } } : {}),
|
|
34
|
+
...(config.autoReview ? { autoReview: true } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return options;
|
|
38
|
+
};
|
|
39
|
+
const resumeOptions = (config) => ({
|
|
40
|
+
apiKey: config.apiKey,
|
|
41
|
+
model: buildModelSelection(config),
|
|
42
|
+
...(config.mode ? { mode: config.mode } : {}),
|
|
43
|
+
});
|
|
44
|
+
const createAgent = async (config) => Agent.create(buildAgentOptions(config));
|
|
45
|
+
const resumeAgent = async (cursorAgentId, config) => Agent.resume(cursorAgentId, resumeOptions(config));
|
|
46
|
+
export const getOrCreateCursorAgent = async (args) => {
|
|
47
|
+
const { config, state, storage } = args;
|
|
48
|
+
const sessionKey = buildSessionKey(state);
|
|
49
|
+
const cached = sessionCache.get(sessionKey);
|
|
50
|
+
if (cached)
|
|
51
|
+
return cached;
|
|
52
|
+
const persisted = readPersistedState(state);
|
|
53
|
+
let agent;
|
|
54
|
+
if (persisted.cursorAgentId) {
|
|
55
|
+
try {
|
|
56
|
+
agent = await resumeAgent(persisted.cursorAgentId, config);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.warn(`[cursor-plugin] Failed to resume ${persisted.cursorAgentId}: ${formatCursorError(error)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!agent) {
|
|
63
|
+
agent = await createAgent(config);
|
|
64
|
+
if (agent.agentId !== persisted.cursorAgentId) {
|
|
65
|
+
await persistCursorState(state, storage, { cursorAgentId: agent.agentId });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const handle = { agent, sessionKey };
|
|
69
|
+
sessionCache.set(sessionKey, handle);
|
|
70
|
+
return handle;
|
|
71
|
+
};
|
|
72
|
+
export const disposeCursorAgent = async (sessionKey) => {
|
|
73
|
+
const cached = sessionCache.get(sessionKey);
|
|
74
|
+
if (!cached)
|
|
75
|
+
return;
|
|
76
|
+
await cached.agent[Symbol.asyncDispose]();
|
|
77
|
+
sessionCache.delete(sessionKey);
|
|
78
|
+
};
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
2
|
+
? value
|
|
3
|
+
: {};
|
|
4
|
+
export const readPersistedState = (state) => {
|
|
5
|
+
const source = state.threadDetails?.state ?? state.channelDetails?.state;
|
|
6
|
+
const record = asRecord(source);
|
|
7
|
+
return typeof record.cursorAgentId === 'string'
|
|
8
|
+
? { cursorAgentId: record.cursorAgentId }
|
|
9
|
+
: {};
|
|
10
|
+
};
|
|
11
|
+
export const persistCursorState = async (state, storage, patch) => {
|
|
12
|
+
if (state.threadId) {
|
|
13
|
+
await storage.patchThreadState({
|
|
14
|
+
channelId: state.channelId,
|
|
15
|
+
threadId: state.threadId,
|
|
16
|
+
state: patch,
|
|
17
|
+
});
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
await storage.patchChannelState({ channelId: state.channelId, state: patch });
|
|
21
|
+
};
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { buildModelSelection } from './config.js';
|
|
2
|
+
import { createStreamState, formatCursorEvent } from './format.js';
|
|
3
|
+
/** Bridge Cursor SDK run events into an async generator of output chunks. */
|
|
4
|
+
export async function* streamCursorPrompt(agent, prompt, config) {
|
|
5
|
+
const state = createStreamState();
|
|
6
|
+
const pending = [];
|
|
7
|
+
let wake;
|
|
8
|
+
let finished = false;
|
|
9
|
+
let error;
|
|
10
|
+
const wakeUp = () => {
|
|
11
|
+
wake?.();
|
|
12
|
+
wake = undefined;
|
|
13
|
+
};
|
|
14
|
+
const run = await agent.send(prompt, {
|
|
15
|
+
...(config.mode ? { mode: config.mode } : {}),
|
|
16
|
+
model: buildModelSelection(config),
|
|
17
|
+
});
|
|
18
|
+
const streamTask = (async () => {
|
|
19
|
+
try {
|
|
20
|
+
for await (const event of run.stream()) {
|
|
21
|
+
const chunk = formatCursorEvent(event, state, config);
|
|
22
|
+
if (chunk) {
|
|
23
|
+
pending.push(chunk);
|
|
24
|
+
wakeUp();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const result = await run.wait();
|
|
28
|
+
const finalText = (result.result ?? state.assistantText).trim();
|
|
29
|
+
if (finalText) {
|
|
30
|
+
pending.push(finalText);
|
|
31
|
+
}
|
|
32
|
+
if (result.status === 'error') {
|
|
33
|
+
const details = JSON.stringify(result);
|
|
34
|
+
pending.push(`**Cursor error:** Run finished with error status. Details: \`${details}\``);
|
|
35
|
+
}
|
|
36
|
+
else if (result.status === 'cancelled') {
|
|
37
|
+
pending.push('**Cursor:** Run cancelled.');
|
|
38
|
+
}
|
|
39
|
+
if (result.git?.branches?.length) {
|
|
40
|
+
const branches = result.git.branches
|
|
41
|
+
.map((branch) => {
|
|
42
|
+
const parts = [branch.branch, branch.prUrl].filter(Boolean);
|
|
43
|
+
return parts.join(' — ');
|
|
44
|
+
})
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
if (branches.length) {
|
|
47
|
+
pending.push(`Git: ${branches.join('; ')}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
finished = true;
|
|
56
|
+
wakeUp();
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
try {
|
|
60
|
+
while (!finished || pending.length > 0) {
|
|
61
|
+
if (pending.length === 0) {
|
|
62
|
+
await new Promise((resolve) => {
|
|
63
|
+
wake = resolve;
|
|
64
|
+
});
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
yield pending.shift();
|
|
68
|
+
}
|
|
69
|
+
await streamTask;
|
|
70
|
+
if (error)
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
// no-op: keep the Cursor agent alive for follow-ups
|
|
75
|
+
}
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meetopenbot/cursor",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "OpenBot agent plugin powered by the Cursor SDK.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@cursor/sdk": "^1.0.18",
|
|
23
|
+
"@meetopenbot/plugin-sdk": "^0.1.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.9.2",
|
|
27
|
+
"typescript": "^6.0.3"
|
|
28
|
+
}
|
|
29
|
+
}
|