@meetopenbot/cursor 0.0.3 → 0.0.5

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/dist/config.js CHANGED
@@ -1,44 +1,63 @@
1
+ import { lookupSecret, trimmedString, } from '@meetopenbot/plugin-sdk';
1
2
  const RUNTIMES = new Set(['local', 'cloud']);
2
3
  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
4
  export const CURSOR_API_KEY_ENV_VAR = 'CURSOR_API_KEY';
5
+ export const pluginConfigSchema = {
6
+ type: 'object',
7
+ properties: {
8
+ runtime: {
9
+ type: 'string',
10
+ description: 'Agent runtime: local (default) or cloud.',
11
+ enum: ['local', 'cloud'],
12
+ default: 'local',
13
+ },
14
+ model: {
15
+ type: 'string',
16
+ description: 'Cursor model id (default: composer-2.5).',
17
+ default: 'composer-2.5',
18
+ },
19
+ mode: {
20
+ type: 'string',
21
+ description: 'Conversation mode: agent (default) or plan.',
22
+ enum: ['agent', 'plan'],
23
+ },
24
+ repoUrl: {
25
+ type: 'string',
26
+ description: 'Git repository URL for cloud agents.',
27
+ format: 'url',
28
+ },
29
+ startingRef: {
30
+ type: 'string',
31
+ description: 'Git ref to clone for cloud agents (e.g. main).',
32
+ },
33
+ autoCreatePR: {
34
+ type: 'boolean',
35
+ description: 'Open a pull request when a cloud run finishes.',
36
+ default: false,
37
+ },
38
+ workOnCurrentBranch: {
39
+ type: 'boolean',
40
+ description: 'Push cloud commits to the existing branch instead of a new one.',
41
+ default: false,
42
+ },
43
+ },
44
+ };
12
45
  export const resolveConfig = (context, channelCwd) => {
13
46
  const config = context.config;
14
47
  const runtime = config.runtime && RUNTIMES.has(config.runtime) ? config.runtime : 'local';
15
48
  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
49
  return {
21
- apiKey: (typeof config.apiKey === 'string' && config.apiKey.trim()) ||
22
- process.env[CURSOR_API_KEY_ENV_VAR],
50
+ apiKey: lookupSecret({ envKeys: [CURSOR_API_KEY_ENV_VAR] }),
23
51
  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,
52
+ model: trimmedString(config.model) || 'composer-2.5',
28
53
  mode,
29
- cwd: channelCwd || config.cwd,
30
- repoUrl: config.repoUrl?.trim() || undefined,
31
- startingRef: config.startingRef?.trim() || undefined,
54
+ cwd: channelCwd,
55
+ repoUrl: trimmedString(config.repoUrl),
56
+ startingRef: trimmedString(config.startingRef),
32
57
  autoCreatePR: config.autoCreatePR === true,
33
58
  workOnCurrentBranch: config.workOnCurrentBranch === true,
34
- settingSources: settingSources?.length ? settingSources : undefined,
35
- sandbox: config.sandbox === true,
36
- autoReview: config.autoReview === true,
37
59
  };
38
60
  };
39
61
  export const buildModelSelection = (config) => ({
40
62
  id: config.model,
41
- ...(config.thinking
42
- ? { params: [{ id: 'thinking', value: config.thinking }] }
43
- : {}),
44
63
  });
package/dist/index.js CHANGED
@@ -1,238 +1,12 @@
1
- import { agentOutput, buildDiffWidget, definePlugin, diffFileFromMutationTool, resolveRunDiffFiles, shouldHandleInvoke, snapshotWorkspace, toolTraceWidget, uiWidget, } from '@meetopenbot/plugin-sdk';
2
- import { CURSOR_API_KEY_ENV_VAR, resolveConfig, } from './config.js';
3
- import { formatCursorError, formatToolResult } from './format.js';
4
- import { getOrCreateCursorAgent, StaleAgentSessionError } from './session.js';
5
- import { streamCursorPrompt } from './stream.js';
6
- export default definePlugin({
7
- id: 'cursor',
1
+ import { defineAgentPlugin } from '@meetopenbot/plugin-sdk';
2
+ import { pluginConfigSchema } from './config.js';
3
+ import { mapCursorError, runCursorTurn } from './runtime.js';
4
+ export const plugin = await defineAgentPlugin({
8
5
  name: 'Cursor',
9
6
  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
- let agentHandle = await getOrCreateCursorAgent({
111
- config,
112
- state: handlerCtx.state,
113
- storage: context.storage,
114
- });
115
- const snapshot = snapshotWorkspace(config.cwd);
116
- const runTurn = async function* (wasResumed) {
117
- const toolInfoMap = new Map();
118
- const changedFiles = new Map();
119
- let fullTextContent = '';
120
- for await (const chunk of streamCursorPrompt(agentHandle.agent, prompt, config, wasResumed)) {
121
- switch (chunk.type) {
122
- case 'tool_start':
123
- toolInfoMap.set(chunk.toolCallId, {
124
- statusLine: chunk.statusLine,
125
- args: chunk.args,
126
- });
127
- yield uiWidget({
128
- agentId: context.agentId,
129
- threadId,
130
- widget: toolTraceWidget({
131
- widgetId: chunk.toolCallId,
132
- groupId: 'cursor:tools',
133
- title: chunk.statusLine,
134
- body: `**Input**\n\`\`\`json\n${JSON.stringify(chunk.args ?? {}, null, 2)}\n\`\`\``,
135
- }),
136
- });
137
- break;
138
- case 'tool_end': {
139
- const info = toolInfoMap.get(chunk.toolCallId);
140
- const args = chunk.args ?? info?.args;
141
- const inputMd = args
142
- ? `**Input**\n\`\`\`json\n${JSON.stringify(args, null, 2)}\n\`\`\`\n\n`
143
- : '';
144
- const outputMd = chunk.result != null
145
- ? `**Output**\n${formatToolResult(chunk.result)}`
146
- : '';
147
- yield uiWidget({
148
- agentId: context.agentId,
149
- threadId,
150
- widget: toolTraceWidget({
151
- widgetId: chunk.toolCallId,
152
- groupId: 'cursor:tools',
153
- title: chunk.statusLine || info?.statusLine || `Tool ${chunk.toolName} finished`,
154
- body: inputMd + outputMd,
155
- state: chunk.isError ? 'error' : 'submitted',
156
- }),
157
- });
158
- if (!chunk.isError) {
159
- const file = diffFileFromMutationTool({
160
- toolName: chunk.toolName,
161
- input: args,
162
- result: chunk.result,
163
- });
164
- if (file)
165
- changedFiles.set(file.path, file);
166
- }
167
- break;
168
- }
169
- case 'status':
170
- yield uiWidget({
171
- agentId: context.agentId,
172
- threadId,
173
- widget: toolTraceWidget({
174
- widgetId: `status-${Date.now()}`,
175
- groupId: 'cursor:tools',
176
- title: 'Status',
177
- body: chunk.statusLine,
178
- }),
179
- });
180
- break;
181
- case 'text':
182
- if (fullTextContent)
183
- fullTextContent += '\n\n';
184
- fullTextContent += chunk.content;
185
- break;
186
- }
187
- }
188
- if (fullTextContent.trim()) {
189
- yield agentOutput({
190
- agentId: context.agentId,
191
- content: fullTextContent.trim(),
192
- threadId,
193
- });
194
- }
195
- const diff = buildDiffWidget({
196
- widgetId: `cursor-diff:${threadId ?? 'run'}:${Date.now()}`,
197
- files: resolveRunDiffFiles({
198
- snapshot,
199
- fallback: changedFiles.values(),
200
- }),
201
- });
202
- if (diff) {
203
- yield uiWidget({
204
- agentId: context.agentId,
205
- threadId,
206
- widget: diff,
207
- });
208
- }
209
- };
210
- try {
211
- yield* runTurn(agentHandle.wasResumed);
212
- }
213
- catch (streamErr) {
214
- if (streamErr instanceof StaleAgentSessionError) {
215
- console.warn(`[cursor-plugin] Detected stale resumed agent. Re-creating a fresh agent session...`);
216
- agentHandle = await getOrCreateCursorAgent({
217
- config,
218
- state: handlerCtx.state,
219
- storage: context.storage,
220
- forceNew: true,
221
- });
222
- yield* runTurn(agentHandle.wasResumed);
223
- }
224
- else {
225
- throw streamErr;
226
- }
227
- }
228
- }
229
- catch (error) {
230
- yield agentOutput({
231
- agentId: context.agentId,
232
- content: `**Cursor error:** ${formatCursorError(error)}`,
233
- threadId,
234
- });
235
- }
236
- });
237
- },
7
+ configSchema: pluginConfigSchema,
8
+ emptyPrompt: 'Send a message to run Cursor in this workspace — for example, "Summarize this repo" or "Fix the failing test in src/foo.test.ts".',
9
+ run: runCursorTurn,
10
+ mapError: mapCursorError,
238
11
  });
12
+ export default plugin;
@@ -0,0 +1,39 @@
1
+ import { missingSecretMessage, textReply, } from '@meetopenbot/plugin-sdk';
2
+ import { CURSOR_API_KEY_ENV_VAR, resolveConfig } from './config.js';
3
+ import { getOrCreateCursorAgent, StaleAgentSessionError } from './session.js';
4
+ import { runCursorPrompt } from './stream.js';
5
+ export async function* runCursorTurn({ prompt, handlerCtx, context, }) {
6
+ const channelCwd = handlerCtx.state.channelDetails?.cwd;
7
+ const config = resolveConfig(context, channelCwd);
8
+ if (!config.apiKey) {
9
+ yield textReply(missingSecretMessage(CURSOR_API_KEY_ENV_VAR));
10
+ return;
11
+ }
12
+ if (config.runtime === 'local' && !config.cwd) {
13
+ yield textReply('Local Cursor agents need a working directory. Configure a channel `cwd`.');
14
+ return;
15
+ }
16
+ let agentHandle = await getOrCreateCursorAgent({
17
+ config,
18
+ state: handlerCtx.state,
19
+ storage: context.storage,
20
+ });
21
+ try {
22
+ yield* runCursorPrompt(agentHandle.agent, prompt, config, agentHandle.wasResumed);
23
+ }
24
+ catch (error) {
25
+ if (!(error instanceof StaleAgentSessionError))
26
+ throw error;
27
+ agentHandle = await getOrCreateCursorAgent({
28
+ config,
29
+ state: handlerCtx.state,
30
+ storage: context.storage,
31
+ forceNew: true,
32
+ });
33
+ yield* runCursorPrompt(agentHandle.agent, prompt, config, agentHandle.wasResumed);
34
+ }
35
+ }
36
+ export function mapCursorError(error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ return textReply(`Cursor error: ${message}`);
39
+ }
package/dist/session.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { Agent, } from '@cursor/sdk';
2
2
  import { buildModelSelection, } from './config.js';
3
- import { formatCursorError } from './format.js';
4
3
  import { persistCursorState, readPersistedState } from './state.js';
5
4
  export class StaleAgentSessionError extends Error {
6
5
  constructor(message) {
@@ -35,9 +34,6 @@ const buildAgentOptions = (config) => {
35
34
  else {
36
35
  options.local = {
37
36
  cwd: config.cwd || process.cwd(),
38
- ...(config.settingSources ? { settingSources: config.settingSources } : {}),
39
- ...(config.sandbox ? { sandboxOptions: { enabled: true } } : {}),
40
- ...(config.autoReview ? { autoReview: true } : {}),
41
37
  };
42
38
  }
43
39
  return options;
@@ -79,7 +75,7 @@ export const getOrCreateCursorAgent = async (args) => {
79
75
  wasResumed = true;
80
76
  }
81
77
  catch (error) {
82
- console.warn(`[cursor-plugin] Failed to resume ${persisted.cursorAgentId}: ${formatCursorError(error)}`);
78
+ console.warn(`[cursor-plugin] Failed to resume ${persisted.cursorAgentId}: ${error instanceof Error ? error.message : String(error)}`);
83
79
  }
84
80
  }
85
81
  if (!agent) {
package/dist/stream.js CHANGED
@@ -1,83 +1,39 @@
1
+ import { emitTextReplies, textReply, } from '@meetopenbot/plugin-sdk';
1
2
  import { buildModelSelection } from './config.js';
2
- import { createStreamState, formatCursorEvent, } from './format.js';
3
3
  import { StaleAgentSessionError } from './session.js';
4
- /** Bridge Cursor SDK run events into an async generator of structured chunks. */
5
- export async function* streamCursorPrompt(agent, prompt, config, wasResumed) {
6
- const state = createStreamState();
7
- const pending = [];
8
- let wake;
9
- let finished = false;
10
- let error;
11
- const wakeUp = () => {
12
- wake?.();
13
- wake = undefined;
14
- };
4
+ const assistantText = (event) => {
5
+ if (event.type !== 'assistant')
6
+ return '';
7
+ return event.message.content
8
+ .filter((block) => block.type === 'text')
9
+ .map((block) => block.text)
10
+ .join('');
11
+ };
12
+ export const classifyCursorEvent = (event) => {
13
+ if (event.type === 'assistant') {
14
+ const text = assistantText(event);
15
+ return text ? { type: 'delta', text } : { type: 'skip' };
16
+ }
17
+ // Tool payloads are skipped for now; still flush so prior text lands before the next turn.
18
+ if (event.type === 'tool_call' || event.type === 'usage')
19
+ return { type: 'flush' };
20
+ return { type: 'skip' };
21
+ };
22
+ /** Run a Cursor prompt and yield each completed assistant turn. */
23
+ export async function* runCursorPrompt(agent, prompt, config, wasResumed) {
15
24
  const run = await agent.send(prompt, {
16
25
  ...(config.mode ? { mode: config.mode } : {}),
17
26
  model: buildModelSelection(config),
18
27
  });
19
- const streamTask = (async () => {
20
- try {
21
- for await (const event of run.stream()) {
22
- const chunk = formatCursorEvent(event, state, config);
23
- if (chunk) {
24
- pending.push(chunk);
25
- wakeUp();
26
- }
27
- }
28
- const result = await run.wait();
29
- const finalText = (result.result ?? state.assistantText).trim();
30
- if (finalText) {
31
- pending.push({ type: 'text', content: finalText });
32
- }
33
- if (result.status === 'error') {
34
- if (wasResumed && (result.durationMs ?? 0) < 2000) {
35
- throw new StaleAgentSessionError('Resumed Cursor agent session is stale or expired.');
36
- }
37
- const details = JSON.stringify(result);
38
- pending.push({
39
- type: 'text',
40
- content: `**Cursor error:** Run finished with error status. Details: \`${details}\``,
41
- });
42
- }
43
- else if (result.status === 'cancelled') {
44
- pending.push({ type: 'text', content: '**Cursor:** Run cancelled.' });
45
- }
46
- if (result.git?.branches?.length) {
47
- const branches = result.git.branches
48
- .map((branch) => {
49
- const parts = [branch.branch, branch.prUrl].filter(Boolean);
50
- return parts.join(' — ');
51
- })
52
- .filter(Boolean);
53
- if (branches.length) {
54
- pending.push({ type: 'text', content: `Git: ${branches.join('; ')}` });
55
- }
56
- }
57
- }
58
- catch (err) {
59
- error = err instanceof Error ? err : new Error(String(err));
60
- }
61
- finally {
62
- finished = true;
63
- wakeUp();
64
- }
65
- })();
66
- try {
67
- while (!finished || pending.length > 0) {
68
- if (pending.length === 0) {
69
- await new Promise((resolve) => {
70
- wake = resolve;
71
- });
72
- continue;
73
- }
74
- yield pending.shift();
28
+ yield* emitTextReplies(run.stream(), classifyCursorEvent);
29
+ const result = await run.wait();
30
+ if (result.status === 'error') {
31
+ if (wasResumed && (result.durationMs ?? 0) < 2000) {
32
+ throw new StaleAgentSessionError('Resumed Cursor agent session is stale or expired.');
75
33
  }
76
- await streamTask;
77
- if (error)
78
- throw error;
34
+ yield textReply(`Cursor error: run finished with status ${result.status}.`);
79
35
  }
80
- finally {
81
- // no-op: keep the Cursor agent alive for follow-ups
36
+ else if (result.status === 'cancelled') {
37
+ yield textReply('Cursor run cancelled.');
82
38
  }
83
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/cursor",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "OpenBot agent plugin powered by the Cursor SDK.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
22
  "@cursor/sdk": "^1.0.18",
23
- "@meetopenbot/plugin-sdk": "^0.2.0"
23
+ "@meetopenbot/plugin-sdk": "^0.4.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^25.9.2",
package/dist/format.js DELETED
@@ -1,190 +0,0 @@
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
- export const formatToolResult = (result) => {
97
- if (result == null)
98
- return '';
99
- if (typeof result === 'string')
100
- return result;
101
- try {
102
- return JSON.stringify(result, null, 2);
103
- }
104
- catch {
105
- return String(result);
106
- }
107
- };
108
- /** Map Cursor SDK stream events to structured output chunks. */
109
- export const formatCursorEvent = (event, state, config) => {
110
- switch (event.type) {
111
- case 'assistant': {
112
- const text = event.message.content
113
- .filter((block) => block.type === 'text')
114
- .map((block) => block.text)
115
- .join('');
116
- if (text)
117
- state.assistantText += text;
118
- return undefined;
119
- }
120
- case 'tool_call': {
121
- if (event.status === 'running') {
122
- return {
123
- type: 'tool_start',
124
- toolCallId: event.call_id,
125
- toolName: event.name,
126
- args: event.args,
127
- statusLine: formatToolStart(event.name, event.args),
128
- };
129
- }
130
- const isError = event.status === 'error';
131
- return {
132
- type: 'tool_end',
133
- toolCallId: event.call_id,
134
- toolName: event.name,
135
- args: event.args,
136
- result: event.result,
137
- isError,
138
- statusLine: isError
139
- ? `Tool **${event.name}** failed.`
140
- : formatToolStart(event.name, event.args).replace(/…$/, ''),
141
- };
142
- }
143
- case 'status': {
144
- if (event.status === 'ERROR') {
145
- const details = JSON.stringify(event);
146
- const isCloud = config?.runtime === 'cloud';
147
- const prefix = isCloud ? 'Cloud run failed' : 'Local run failed';
148
- return {
149
- type: 'text',
150
- content: `**Cursor error:** ${prefix}. Event details: \`${details}\``,
151
- };
152
- }
153
- const statusLine = formatCloudStatus(event.status, event.message, config);
154
- return statusLine ? { type: 'status', statusLine } : undefined;
155
- }
156
- case 'task': {
157
- if (!event.text?.trim())
158
- return undefined;
159
- return { type: 'status', statusLine: truncate(event.text) };
160
- }
161
- default:
162
- return undefined;
163
- }
164
- };
165
- export const formatCursorError = (error) => {
166
- if (error instanceof CursorSdkError) {
167
- const parts = [error.message];
168
- if (error.requestId)
169
- parts.push(`(requestId: ${error.requestId})`);
170
- const helpUrl = error.helpUrl;
171
- if (helpUrl)
172
- parts.push(`See ${helpUrl}`);
173
- return parts.join(' ');
174
- }
175
- if (error instanceof Error) {
176
- const extra = { ...error };
177
- if (Object.keys(extra).length > 0) {
178
- return `${error.message} (details: ${JSON.stringify(extra)})`;
179
- }
180
- return error.message;
181
- }
182
- try {
183
- return typeof error === 'object' && error !== null
184
- ? JSON.stringify(error)
185
- : String(error);
186
- }
187
- catch {
188
- return String(error);
189
- }
190
- };