Skip to content

Commit e9e88a1

Browse files
G9PedroClawdious
andauthored
feat: add cursor automations bridge and CLI controls (#39)
Introduce a kernel-level cursor bridge that verifies webhook signatures, routes automation events into dispatch runs, and records event bridge telemetry for status visibility. Expose setup/status/dispatch commands in the CLI and wire the new module into public exports. Made-with: Cursor Co-authored-by: Clawdious <clawdious@agentmail.to>
1 parent e01f110 commit e9e88a1

File tree

7 files changed

+1119
-0
lines changed

7 files changed

+1119
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"zod": "^4.3.6"
9090
},
9191
"devDependencies": {
92+
"@versatly/workgraph-mcp-server": "workspace:*",
9293
"@types/node": "^20.11.0",
9394
"ajv": "^8.18.0",
9495
"ajv-formats": "^3.0.1",

packages/cli/src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { registerAdapterCommands } from './cli/commands/adapter.js';
77
import { registerAutonomyCommands } from './cli/commands/autonomy.js';
88
import { registerCapabilityCommands } from './cli/commands/capability.js';
99
import { registerConversationCommands } from './cli/commands/conversation.js';
10+
import { registerCursorCommands } from './cli/commands/cursor.js';
1011
import { registerDispatchCommands } from './cli/commands/dispatch.js';
1112
import { registerMcpCommands } from './cli/commands/mcp.js';
1213
import { registerSafetyCommands } from './cli/commands/safety.js';
@@ -2207,6 +2208,7 @@ addWorkspaceOption(
22072208

22082209
registerAdapterCommands(program, DEFAULT_ACTOR);
22092210
registerDispatchCommands(program, DEFAULT_ACTOR);
2211+
registerCursorCommands(program, DEFAULT_ACTOR);
22102212

22112213
// ============================================================================
22122214
// trigger
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { Command } from 'commander';
2+
import * as workgraph from '@versatly/workgraph-kernel';
3+
import {
4+
addWorkspaceOption,
5+
csv,
6+
resolveWorkspacePath,
7+
runCommand,
8+
} from '../core.js';
9+
10+
export function registerCursorCommands(program: Command, defaultActor: string): void {
11+
const cursorCmd = program
12+
.command('cursor')
13+
.description('Configure and run Cursor Automations bridge flows');
14+
15+
addWorkspaceOption(
16+
cursorCmd
17+
.command('setup')
18+
.description('Configure Cursor webhook + dispatch bridge defaults')
19+
.option('-a, --actor <name>', 'Dispatch actor for bridged runs', defaultActor)
20+
.option('--enabled <bool>', 'Enable bridge (true|false)')
21+
.option('--secret <value>', 'Webhook HMAC shared secret')
22+
.option('--event-types <patterns>', 'Comma-separated event patterns (supports *)')
23+
.option('--adapter <name>', 'Dispatch adapter default')
24+
.option('--execute <bool>', 'Execute dispatch run immediately (true|false)')
25+
.option('--agents <actors>', 'Comma-separated agent identities')
26+
.option('--max-steps <n>', 'Maximum scheduler steps')
27+
.option('--step-delay-ms <ms>', 'Delay between scheduler steps')
28+
.option('--space <spaceRef>', 'Restrict dispatch to one space')
29+
.option('--checkpoint <bool>', 'Create dispatch checkpoint (true|false)')
30+
.option('--timeout-ms <ms>', 'Execution timeout in milliseconds')
31+
.option('--dispatch-mode <mode>', 'direct|self-assembly')
32+
.option('--json', 'Emit structured JSON output'),
33+
).action((opts) =>
34+
runCommand(
35+
opts,
36+
() => {
37+
const workspacePath = resolveWorkspacePath(opts);
38+
const config = workgraph.cursorBridge.setupCursorBridge(workspacePath, {
39+
actor: opts.actor,
40+
enabled: parseOptionalBoolean(opts.enabled, 'enabled'),
41+
secret: opts.secret,
42+
allowedEventTypes: csv(opts.eventTypes),
43+
dispatch: {
44+
adapter: opts.adapter,
45+
execute: parseOptionalBoolean(opts.execute, 'execute'),
46+
agents: csv(opts.agents),
47+
maxSteps: parseOptionalInt(opts.maxSteps, 'max-steps'),
48+
stepDelayMs: parseOptionalInt(opts.stepDelayMs, 'step-delay-ms'),
49+
space: opts.space,
50+
createCheckpoint: parseOptionalBoolean(opts.checkpoint, 'checkpoint'),
51+
timeoutMs: parseOptionalInt(opts.timeoutMs, 'timeout-ms'),
52+
dispatchMode: parseDispatchMode(opts.dispatchMode),
53+
},
54+
});
55+
const status = workgraph.cursorBridge.getCursorBridgeStatus(workspacePath, {
56+
recentEventsLimit: 3,
57+
});
58+
return {
59+
config,
60+
status,
61+
};
62+
},
63+
(result) => [
64+
`Cursor bridge configured: ${result.status.configPath}`,
65+
`Enabled: ${result.config.enabled}`,
66+
`Webhook secret: ${result.status.webhook.hasSecret ? 'configured' : 'not set'}`,
67+
`Allowed events: ${result.config.webhook.allowedEventTypes.join(', ')}`,
68+
`Dispatch default: actor=${result.config.dispatch.actor} adapter=${result.config.dispatch.adapter} execute=${result.config.dispatch.execute}`,
69+
],
70+
),
71+
);
72+
73+
addWorkspaceOption(
74+
cursorCmd
75+
.command('status')
76+
.description('Show Cursor bridge configuration and recent bridge events')
77+
.option('--events <n>', 'Number of recent bridge events to show', '5')
78+
.option('--json', 'Emit structured JSON output'),
79+
).action((opts) =>
80+
runCommand(
81+
opts,
82+
() => {
83+
const workspacePath = resolveWorkspacePath(opts);
84+
return workgraph.cursorBridge.getCursorBridgeStatus(workspacePath, {
85+
recentEventsLimit: parseOptionalInt(opts.events, 'events') ?? 5,
86+
});
87+
},
88+
(result) => [
89+
`Configured: ${result.configured}`,
90+
`Enabled: ${result.enabled}`,
91+
`Provider: ${result.provider}`,
92+
`Config path: ${result.configPath}`,
93+
`Events path: ${result.eventsPath}`,
94+
`Webhook secret: ${result.webhook.hasSecret ? 'configured' : 'not set'}`,
95+
`Allowed events: ${result.webhook.allowedEventTypes.join(', ')}`,
96+
`Dispatch default: actor=${result.dispatch.actor} adapter=${result.dispatch.adapter} execute=${result.dispatch.execute}`,
97+
...(result.recentEvents.length === 0
98+
? ['Recent events: none']
99+
: [
100+
'Recent events:',
101+
...result.recentEvents.map((event) =>
102+
`- ${event.ts} ${event.eventType} run=${event.runId ?? 'none'} status=${event.runStatus ?? 'none'}${event.error ? ` error=${event.error}` : ''}`),
103+
]),
104+
],
105+
),
106+
);
107+
108+
addWorkspaceOption(
109+
cursorCmd
110+
.command('dispatch <objective>')
111+
.description('Dispatch one Cursor automation event through the bridge')
112+
.option('--event-type <type>', 'Cursor event type', 'cursor.automation.manual')
113+
.option('--event-id <id>', 'Cursor event id')
114+
.option('--actor <name>', 'Override dispatch actor')
115+
.option('--adapter <name>', 'Override dispatch adapter')
116+
.option('--execute <bool>', 'Execute dispatch run immediately (true|false)')
117+
.option('--context <json>', 'JSON object merged into dispatch context')
118+
.option('--idempotency-key <key>', 'Override idempotency key')
119+
.option('--agents <actors>', 'Comma-separated agent identities')
120+
.option('--max-steps <n>', 'Maximum scheduler steps')
121+
.option('--step-delay-ms <ms>', 'Delay between scheduler steps')
122+
.option('--space <spaceRef>', 'Restrict dispatch to one space')
123+
.option('--checkpoint <bool>', 'Create dispatch checkpoint (true|false)')
124+
.option('--timeout-ms <ms>', 'Execution timeout in milliseconds')
125+
.option('--dispatch-mode <mode>', 'direct|self-assembly')
126+
.option('--json', 'Emit structured JSON output'),
127+
).action((objective, opts) =>
128+
runCommand(
129+
opts,
130+
async () => {
131+
const workspacePath = resolveWorkspacePath(opts);
132+
const result = await workgraph.cursorBridge.dispatchCursorAutomationEvent(workspacePath, {
133+
source: 'cli-dispatch',
134+
eventType: opts.eventType,
135+
eventId: opts.eventId,
136+
objective,
137+
actor: opts.actor,
138+
adapter: opts.adapter,
139+
execute: parseOptionalBoolean(opts.execute, 'execute'),
140+
context: parseOptionalJsonObject(opts.context, 'context'),
141+
idempotencyKey: opts.idempotencyKey,
142+
agents: csv(opts.agents),
143+
maxSteps: parseOptionalInt(opts.maxSteps, 'max-steps'),
144+
stepDelayMs: parseOptionalInt(opts.stepDelayMs, 'step-delay-ms'),
145+
space: opts.space,
146+
createCheckpoint: parseOptionalBoolean(opts.checkpoint, 'checkpoint'),
147+
timeoutMs: parseOptionalInt(opts.timeoutMs, 'timeout-ms'),
148+
dispatchMode: parseDispatchMode(opts.dispatchMode),
149+
});
150+
return result;
151+
},
152+
(result) => [
153+
`Dispatched Cursor event: ${result.event.eventType}`,
154+
`Run: ${result.run.id} [${result.run.status}]`,
155+
`Adapter: ${result.run.adapter}`,
156+
...(result.run.output ? [`Output: ${result.run.output}`] : []),
157+
...(result.run.error ? [`Error: ${result.run.error}`] : []),
158+
],
159+
),
160+
);
161+
}
162+
163+
function parseOptionalBoolean(value: unknown, optionName: string): boolean | undefined {
164+
if (value === undefined) return undefined;
165+
if (typeof value === 'boolean') return value;
166+
const normalized = String(value).trim().toLowerCase();
167+
if (normalized === 'true' || normalized === '1' || normalized === 'yes') return true;
168+
if (normalized === 'false' || normalized === '0' || normalized === 'no') return false;
169+
throw new Error(`Invalid --${optionName}. Expected true|false.`);
170+
}
171+
172+
function parseOptionalInt(value: unknown, optionName: string): number | undefined {
173+
if (value === undefined) return undefined;
174+
const parsed = Number.parseInt(String(value), 10);
175+
if (!Number.isFinite(parsed)) {
176+
throw new Error(`Invalid --${optionName}. Expected an integer.`);
177+
}
178+
return parsed;
179+
}
180+
181+
function parseDispatchMode(value: unknown): 'direct' | 'self-assembly' | undefined {
182+
if (value === undefined) return undefined;
183+
const normalized = String(value).trim().toLowerCase();
184+
if (!normalized) return undefined;
185+
if (normalized === 'direct' || normalized === 'self-assembly') {
186+
return normalized;
187+
}
188+
throw new Error(`Invalid --dispatch-mode "${String(value)}". Expected direct|self-assembly.`);
189+
}
190+
191+
function parseOptionalJsonObject(value: unknown, optionName: string): Record<string, unknown> | undefined {
192+
if (value === undefined) return undefined;
193+
const text = String(value).trim();
194+
if (!text) return undefined;
195+
let parsed: unknown;
196+
try {
197+
parsed = JSON.parse(text);
198+
} catch {
199+
throw new Error(`Invalid --${optionName}. Expected valid JSON.`);
200+
}
201+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
202+
throw new Error(`Invalid --${optionName}. Expected a JSON object.`);
203+
}
204+
return parsed as Record<string, unknown>;
205+
}

0 commit comments

Comments
 (0)