@meetopenbot/cursor 0.0.2 → 0.0.3

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/format.js CHANGED
@@ -93,7 +93,19 @@ const formatCloudStatus = (status, message, config) => {
93
93
  return message ? truncate(message) : undefined;
94
94
  }
95
95
  };
96
- /** Map Cursor SDK stream events to user-visible output chunks. */
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. */
97
109
  export const formatCursorEvent = (event, state, config) => {
98
110
  switch (event.type) {
99
111
  case 'assistant': {
@@ -107,27 +119,44 @@ export const formatCursorEvent = (event, state, config) => {
107
119
  }
108
120
  case 'tool_call': {
109
121
  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}\``;
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
+ };
115
129
  }
116
- return undefined;
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
+ };
117
142
  }
118
143
  case 'status': {
119
144
  if (event.status === 'ERROR') {
120
145
  const details = JSON.stringify(event);
121
146
  const isCloud = config?.runtime === 'cloud';
122
147
  const prefix = isCloud ? 'Cloud run failed' : 'Local run failed';
123
- return `**Cursor error:** ${prefix}. Event details: \`${details}\``;
148
+ return {
149
+ type: 'text',
150
+ content: `**Cursor error:** ${prefix}. Event details: \`${details}\``,
151
+ };
124
152
  }
125
- return formatCloudStatus(event.status, event.message, config);
153
+ const statusLine = formatCloudStatus(event.status, event.message, config);
154
+ return statusLine ? { type: 'status', statusLine } : undefined;
126
155
  }
127
156
  case 'task': {
128
157
  if (!event.text?.trim())
129
158
  return undefined;
130
- return truncate(event.text);
159
+ return { type: 'status', statusLine: truncate(event.text) };
131
160
  }
132
161
  default:
133
162
  return undefined;
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from '@meetopenbot/plugin-sdk';
2
+ declare const plugin: Plugin;
3
+ export { plugin };
4
+ export default plugin;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { agentOutput, definePlugin, shouldHandleInvoke, } from '@meetopenbot/plugin-sdk';
1
+ import { agentOutput, buildDiffWidget, definePlugin, diffFileFromMutationTool, resolveRunDiffFiles, shouldHandleInvoke, snapshotWorkspace, toolTraceWidget, uiWidget, } from '@meetopenbot/plugin-sdk';
2
2
  import { CURSOR_API_KEY_ENV_VAR, resolveConfig, } from './config.js';
3
- import { formatCursorError } from './format.js';
3
+ import { formatCursorError, formatToolResult } from './format.js';
4
4
  import { getOrCreateCursorAgent, StaleAgentSessionError } from './session.js';
5
5
  import { streamCursorPrompt } from './stream.js';
6
6
  export default definePlugin({
@@ -112,14 +112,103 @@ export default definePlugin({
112
112
  state: handlerCtx.state,
113
113
  storage: context.storage,
114
114
  });
115
- try {
116
- for await (const chunk of streamCursorPrompt(agentHandle.agent, prompt, config, agentHandle.wasResumed)) {
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()) {
117
189
  yield agentOutput({
118
190
  agentId: context.agentId,
119
- content: chunk,
191
+ content: fullTextContent.trim(),
120
192
  threadId,
121
193
  });
122
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);
123
212
  }
124
213
  catch (streamErr) {
125
214
  if (streamErr instanceof StaleAgentSessionError) {
@@ -130,13 +219,7 @@ export default definePlugin({
130
219
  storage: context.storage,
131
220
  forceNew: true,
132
221
  });
133
- for await (const chunk of streamCursorPrompt(agentHandle.agent, prompt, config, agentHandle.wasResumed)) {
134
- yield agentOutput({
135
- agentId: context.agentId,
136
- content: chunk,
137
- threadId,
138
- });
139
- }
222
+ yield* runTurn(agentHandle.wasResumed);
140
223
  }
141
224
  else {
142
225
  throw streamErr;
package/dist/stream.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { buildModelSelection } from './config.js';
2
- import { createStreamState, formatCursorEvent } from './format.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 output chunks. */
4
+ /** Bridge Cursor SDK run events into an async generator of structured chunks. */
5
5
  export async function* streamCursorPrompt(agent, prompt, config, wasResumed) {
6
6
  const state = createStreamState();
7
7
  const pending = [];
@@ -28,17 +28,20 @@ export async function* streamCursorPrompt(agent, prompt, config, wasResumed) {
28
28
  const result = await run.wait();
29
29
  const finalText = (result.result ?? state.assistantText).trim();
30
30
  if (finalText) {
31
- pending.push(finalText);
31
+ pending.push({ type: 'text', content: finalText });
32
32
  }
33
33
  if (result.status === 'error') {
34
34
  if (wasResumed && (result.durationMs ?? 0) < 2000) {
35
35
  throw new StaleAgentSessionError('Resumed Cursor agent session is stale or expired.');
36
36
  }
37
37
  const details = JSON.stringify(result);
38
- pending.push(`**Cursor error:** Run finished with error status. Details: \`${details}\``);
38
+ pending.push({
39
+ type: 'text',
40
+ content: `**Cursor error:** Run finished with error status. Details: \`${details}\``,
41
+ });
39
42
  }
40
43
  else if (result.status === 'cancelled') {
41
- pending.push('**Cursor:** Run cancelled.');
44
+ pending.push({ type: 'text', content: '**Cursor:** Run cancelled.' });
42
45
  }
43
46
  if (result.git?.branches?.length) {
44
47
  const branches = result.git.branches
@@ -48,7 +51,7 @@ export async function* streamCursorPrompt(agent, prompt, config, wasResumed) {
48
51
  })
49
52
  .filter(Boolean);
50
53
  if (branches.length) {
51
- pending.push(`Git: ${branches.join('; ')}`);
54
+ pending.push({ type: 'text', content: `Git: ${branches.join('; ')}` });
52
55
  }
53
56
  }
54
57
  }
package/package.json CHANGED
@@ -1,29 +1,35 @@
1
1
  {
2
2
  "name": "@meetopenbot/cursor",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "OpenBot agent plugin powered by the Cursor SDK.",
5
5
  "type": "module",
6
- "main": "dist/index.js",
6
+ "main": "./dist/index.js",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
10
10
  "exports": {
11
- ".": "./dist/index.js"
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
12
16
  },
13
17
  "files": [
14
18
  "dist"
15
19
  ],
16
- "scripts": {
17
- "build": "tsc -p tsconfig.json",
18
- "typecheck": "tsc -p tsconfig.json --noEmit"
19
- },
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
22
  "@cursor/sdk": "^1.0.18",
23
- "@meetopenbot/plugin-sdk": "^0.1.2"
23
+ "@meetopenbot/plugin-sdk": "^0.2.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^25.9.2",
27
27
  "typescript": "^6.0.3"
28
+ },
29
+ "types": "./dist/index.d.ts",
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.json && node ../../scripts/write-plugin-declaration.mjs",
32
+ "typecheck": "tsc -p tsconfig.json --noEmit",
33
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
28
34
  }
29
- }
35
+ }