Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion adapters/cursor.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Cursor agent hooks -> Agent Floor events.
// Cursor writes a JSON payload to the hook's stdin. We only subscribe to hooks
// that observe (after*, stop, beforeSubmitPrompt) so Agent Floor can never change
// that observe (after*, stop, preCompact, beforeSubmitPrompt) so Agent Floor can never change
// a permission decision. See docs/integrations.md for the hooks.json snippet.
import { basename } from 'node:path';
import { shortId, teamFromCwd } from './common.mjs';
Expand Down Expand Up @@ -34,6 +34,8 @@ export function fromCursor(input) {
return [{ type: 'tool', ...base, tool: 'MCP', activity: `MCP: ${input.tool_name ?? 'tool'}` }];
case 'afterAgentResponse':
return [{ type: 'status', ...base, status: 'thinking', activity: `Replied: ${input.text ?? ''}` }];
case 'preCompact':
return [{ type: 'status', ...base, status: 'thinking', activity: 'Tidying up memory' }];
case 'stop':
return [
{
Expand Down
31 changes: 29 additions & 2 deletions bin/agent-floor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,34 @@ async function hook(kind) {
const { fromCursor, cursorResponse } = await import('../adapters/cursor.mjs');
const input = parseJson(await readStdin());
response = cursorResponse(input?.hook_event_name);
await postOrLaunch(fromCursor(input));
if (response) process.stdout.write(response);
const events = fromCursor(input);
await postOrLaunch(events);

if (input?.hook_event_name === 'preCompact' && events.length) {
const ev = events[0];
setTimeout(async () => {
try {
const res = await fetch(`${URL_BASE}/state`);
if (res.ok) {
const state = await res.json();
const a = state.agents?.[ev.agent];
if (a && a.status === 'thinking' && a.activity === 'Tidying up memory') {
await postOrLaunch([{
type: 'status',
agent: ev.agent,
team: ev.team,
source: ev.source,
status: 'idle',
activity: 'Waiting for you',
}]);
}
}
} catch {}
process.exit(0);
}, 10000);
return;
}
}
} catch {
// Floor not running, or bad payload. Stay silent.
Expand Down Expand Up @@ -251,7 +278,7 @@ function claudeHooks() {

function cursorHooks() {
const command = hookCmd('cursor');
const names = ['beforeSubmitPrompt', 'afterAgentThought', 'afterAgentResponse', 'afterFileEdit', 'afterShellExecution', 'afterMCPExecution', 'stop'];
const names = ['beforeSubmitPrompt', 'afterAgentThought', 'afterAgentResponse', 'afterFileEdit', 'afterShellExecution', 'afterMCPExecution', 'preCompact', 'stop'];
return { version: 1, hooks: Object.fromEntries(names.map((n) => [n, [{ command }]])) };
}

Expand Down
1 change: 1 addition & 0 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ If your Codex build supports Claude-style hooks with `hook_event_name` payloads
"afterMCPExecution": [{ "command": "agent-floor hook cursor" }],
"afterAgentResponse": [{ "command": "agent-floor hook cursor" }],
"afterAgentThought": [{ "command": "agent-floor hook cursor" }],
"preCompact": [{ "command": "agent-floor hook cursor" }],
"stop": [{ "command": "agent-floor hook cursor" }]
}
}
Expand Down
3 changes: 3 additions & 0 deletions test/protocol.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ test('codex notify payload', () => {
test('cursor adapter only answers beforeSubmitPrompt', () => {
const evs = fromCursor({ hook_event_name: 'afterFileEdit', conversation_id: 'c1', workspace_roots: ['/w/site'], file_path: '/w/site/a.md' });
assert.equal(evs[0].activity, 'Edited a.md');
const compact = fromCursor({ hook_event_name: 'preCompact', conversation_id: 'c1', workspace_roots: ['/w/site'] });
assert.equal(compact[0].activity, 'Tidying up memory');
assert.equal(compact[0].status, 'thinking');
assert.equal(cursorResponse('afterFileEdit'), '');
assert.deepEqual(JSON.parse(cursorResponse('beforeSubmitPrompt')), { continue: true });
});
Expand Down