Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
106e829
docs: refresh ARCHITECTURE-PROXY.md
vld-m Sep 10, 2026
4c1834e
fix: commit npm install articats
vld-m Sep 10, 2026
4921bc5
chore: add commands for proxy development
vld-m Sep 10, 2026
f2dcdab
feat(proxy): add --cursor-ide target to connect command
vld-m Sep 10, 2026
f65363b
feat(cli): add --agent flag and non-blocking exit gating to codemie hook
vld-m Sep 12, 2026
6a83d01
feat(agents): add cursor-ide agent plugin with hook transformer
vld-m Sep 12, 2026
4906020
feat(cli): route the 7 new internal hook events
vld-m Sep 12, 2026
625b337
feat(cli): add cursor-ide stdout response contract, never blocking exit
vld-m Sep 12, 2026
06fac64
feat(agents): capture cursor-ide hook events to project-local JSONL t…
vld-m Sep 12, 2026
c182409
feat(proxy): add cursor-ide.ts connector writing .cursor/hooks.json
vld-m Sep 12, 2026
4a238da
feat(proxy): add --analytics flag gating cursor-ide connect target
vld-m Sep 12, 2026
74a6bfa
feat(proxy): add --cursor-ide to proxy disconnect
vld-m Sep 12, 2026
763dbf6
docs: add cursor-ide-hooks-analytics planning artifacts
vld-m Sep 12, 2026
76b9a99
chore: fix stale sdlc-factory ignore patterns in .gitignore
vld-m Sep 12, 2026
de568ec
fix(proxy): warn on --analytics without --cursor-ide even with no target
vld-m Sep 14, 2026
801d543
fix(proxy): resolve cursor-ide hook command from package root
vld-m Sep 14, 2026
19c260b
feat(proxy): add cursor-ide OTLP hook forwarding
vld-m Sep 15, 2026
bcc4655
feat(proxy): add cli-analytics endpoint constants
vld-m Sep 16, 2026
dc9a8dd
feat(proxy): thread credentials and baseUrl into OtlpIngestInterceptor
vld-m Sep 16, 2026
bf6f1d9
feat(proxy): forward cursor-ide hook events to cli-analytics backend
vld-m Sep 16, 2026
6cd95f1
feat(proxy): scaffold metrics/logs/traces push stubs for cli-analytics
vld-m Sep 16, 2026
b352778
fix(proxy): validate agentName, restrict log dir permissions, guard n…
vld-m Sep 16, 2026
e375724
docs: add planning artifacts for cursor-ide-otlp-event-hooks
vld-m Sep 16, 2026
d7bed02
docs: add missed edge-case review lens artifact
vld-m Sep 16, 2026
b2c0479
fix(proxy): resolve active SSO profile without env override, add loca…
vld-m Sep 16, 2026
539877c
chore: drop POC branch tests to shrink blast radius
vld-m Sep 16, 2026
109ccdf
chore: remove SDLC task planning artifacts (1/5)
vld-m Sep 16, 2026
39ebff7
chore: remove SDLC task planning artifacts (2/5)
vld-m Sep 16, 2026
b11f8f3
chore: remove SDLC task planning artifacts (3/5)
vld-m Sep 16, 2026
4c00871
chore: remove SDLC task planning artifacts (4/5)
vld-m Sep 16, 2026
9940822
chore: remove SDLC task planning artifacts (5/5)
vld-m Sep 16, 2026
78bfc21
revert: restore .gitignore to main state
vld-m Sep 16, 2026
69c6112
revert: restore package-lock.json to main state
vld-m Sep 16, 2026
bd8f7ec
revert: restore non-branch SDLC task planning artifacts
vld-m Sep 17, 2026
fe0625a
revert: restore remaining non-branch SDLC task planning artifacts
vld-m Sep 17, 2026
af6cfc2
revert: restore proxy and registry tests dropped in POC cleanup
vld-m Sep 17, 2026
56d5b8d
feat: Implement OtlpIngestInterceptor logic
dzhalai Sep 17, 2026
6e34d49
refactor: split OtlpIngestInterceptor into OtlpDispatcher (SRP)
vld-m Sep 17, 2026
2899f27
feat: Populate 'gitBranch' and 'repoRemote'
dzhalai Sep 17, 2026
e951204
chore: enable preview environment
vld-m Sep 17, 2026
389ce80
fix: Fix span names for Cursor
dzhalai Sep 17, 2026
3b1f222
fix: Fix email populating
dzhalai Sep 17, 2026
4564586
Merge branch 'EPMCDME-14834/cursor-ide-otlp-integration' of https://g…
dzhalai Sep 17, 2026
987e12e
refactor: Remove code added for local development
dzhalai Sep 18, 2026
c1739fe
refactor: Revert codemie-cli.config.json
dzhalai Sep 18, 2026
b510256
refactor: Remove unused code
dzhalai Sep 18, 2026
a0f1e41
feat: Initialize project name
dzhalai Sep 18, 2026
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
760 changes: 501 additions & 259 deletions docs/ARCHITECTURE-PROXY.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/agents/__tests__/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('AgentRegistry', () => {
'codemie-code',
'claude',
'claude-acp',
'cursor-ide',
'gemini',
'opencode',
'codex',
Expand Down
29 changes: 29 additions & 0 deletions src/agents/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,35 @@ export interface AgentHookConfig {
* }
*/
eventNameMapping?: Record<string, InternalHookEventName>;


/**
* When true, the `codemie hook` CLI path never exits non-zero for this
* agent: JSON-parse failures and `validateHookEvent` failures degrade to a
* non-blocking failure (thrown internally, caught, and turned into a
* successful exit) instead of `process.exit(2)`. Set by agents whose host
* treats a non-zero exit as a hard block on the user's action.
*/
neverBlockingExit?: boolean;

/**
* Optional per-agent stdout response contract, invoked with the
* agent-native event name (`event.hook_event_name`, unmutated by
* `eventNameMapping`) after the hook has been routed. Lets a host that
* inspects stdout for a synchronous response (e.g. Cursor's
* `{"permission":"allow"}`/`{"continue":true}`) get one without adding an
* agent-name literal to `hook.ts` — set only by agents whose host reads a
* response from stdout.
*/
writeStdoutResponse?: (nativeEventName: string) => void;


/**
* When true, the hook action forwards every event as-is to the local proxy
* daemon (see forwardOtlpEvent) and returns immediately, before the shared
* transform/validate/route pipeline and its legacy analytics handlers run.
*/
otlpIngestion?: boolean;
}

/**
Expand Down
15 changes: 15 additions & 0 deletions src/agents/plugins/cursor-ide/cursor-ide.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Shared Cursor IDE identifiers.
*
* Kept apart from `cursor-ide.plugin.ts` so other modules (the connector, the
* hook transformer) can import identifiers without pulling in the full plugin.
*/

/** Internal agent key. Canonical everywhere: the `--cursor-ide` flag, `--agent cursor-ide`, `metadata.name`. */
export const CURSOR_IDE_AGENT_NAME = 'cursor-ide';

/** User-facing label shown in analytics output. */
export const CURSOR_IDE_DISPLAY_NAME = 'Cursor IDE';

/** Runtime client type recorded on ingested analytics events. */
export const CURSOR_IDE_CLIENT_TYPE = 'codemie-cursor-ide';
64 changes: 64 additions & 0 deletions src/agents/plugins/cursor-ide/cursor-ide.otlp-forwarder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { readState, isProcessAlive } from '../../../cli/commands/proxy/daemon-manager.js';
import { logger } from '../../../utils/logger.js';

interface OtlpEventPayload {
agentName: string;
timestamp: string;
raw: string;
}

/**
* Fire-and-forget forward of a raw cursor-ide hook event to the local proxy daemon.
*
* - Calls readState() to get daemon URL and gateway key
* - If no daemon or process is dead, logs at debug and returns (no error)
* - POSTs { agentName, timestamp, raw: rawInput } to daemon's /v1/otlp/hook-events route
* - Uses 1500ms timeout and swallows all errors (network, non-2xx status)
* - Never throws, never blocks cursor's exit, never affects hook's exit code
*/
export async function forwardOtlpEvent(rawInput: string, agentName: string): Promise<void> {
try {
const state = await readState();
if (!state) {
logger.debug('forwardOtlpEvent: no daemon state found');
return;
}

if (!isProcessAlive(state.pid)) {
logger.debug(`forwardOtlpEvent: daemon pid ${state.pid} not alive`);
return;
}

const payload: OtlpEventPayload = {
agentName,
timestamp: new Date().toISOString(),
raw: rawInput,
};

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1500);

try {
const response = await fetch(`${state.url}/v1/otlp/hook-events`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${state.gatewayKey}`,
},
body: JSON.stringify(payload),
signal: controller.signal,
});

if (!response.ok) {
logger.debug(`forwardOtlpEvent: received status ${response.status}`);
}
} finally {
clearTimeout(timeout);
}
} catch (err) {
// Swallow all errors: network failure, timeout, JSON errors, etc.
// Never let a POST failure affect cursor's exit or the hook's behavior
const msg = err instanceof Error ? err.message : String(err);
logger.debug(`forwardOtlpEvent: ${msg}`);
}
}
69 changes: 69 additions & 0 deletions src/agents/plugins/cursor-ide/cursor-ide.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { AgentMetadata } from '../../core/types.js';
import { BaseAgentAdapter } from '../../core/BaseAgentAdapter.js';
import { writeCursorResponse } from './cursor-ide.response.js';
import {
CURSOR_IDE_AGENT_NAME,
CURSOR_IDE_CLIENT_TYPE,
CURSOR_IDE_DISPLAY_NAME,
} from './cursor-ide.constants.js';

export {
CURSOR_IDE_AGENT_NAME,
CURSOR_IDE_CLIENT_TYPE,
CURSOR_IDE_DISPLAY_NAME,
} from './cursor-ide.constants.js';

export const CursorIdePluginMetadata: AgentMetadata = {
name: CURSOR_IDE_AGENT_NAME,
displayName: CURSOR_IDE_DISPLAY_NAME,
description: 'Cursor IDE - analytics-only hook ingestion, never installed or launched by CodeMie',

// Analytics-only: CodeMie never installs, updates, or launches Cursor.
npmPackage: null,
cliCommand: null,

envMapping: {
baseUrl: [],
apiKey: [],
model: [],
},
supportedProviders: [],

// The sole gate excluding this agent from `codemie install/list/uninstall/update`
// (src/agents/registry.ts:getManageableAgents, types.ts:analyticsOnly).
analyticsOnly: true,

ssoConfig: {
enabled: false,
clientType: CURSOR_IDE_CLIENT_TYPE,
},

hookConfig: {
// Exit code 2 is equivalent to `permission: "deny"` in Cursor and blocks
// the user's action - analytics ingestion must never be capable of that.
neverBlockingExit: true,
// Cursor reads a JSON response off stdout for a subset of its events
// (see cursor-ide.response.ts) - this is the sole gate that calls it,
// set only for this agent.
writeStdoutResponse: writeCursorResponse,
// Fire-and-forget forward raw events to the local proxy daemon's
// /v1/otlp/hook-events route, bypassing the shared transform/validate/route
// pipeline and its legacy analytics handlers.
otlpIngestion: true,
},
};

export class CursorIdePlugin extends BaseAgentAdapter {
constructor(metadata: AgentMetadata = CursorIdePluginMetadata) {
super(metadata);
}

override async isInstalled(): Promise<boolean> {
// Analytics-only: Cursor's installation state is irrelevant to CodeMie.
return true;
}

override async getVersion(): Promise<string | null> {
return null;
}
}
48 changes: 48 additions & 0 deletions src/agents/plugins/cursor-ide/cursor-ide.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Cursor IDE stdout response contract.
*
* Cursor reads a JSON object off the hook process's stdout to decide how to
* proceed for a subset of its events - `{"permission":"allow"}` for
* tool-permission events, `{"continue":true}` for `beforeSubmitPrompt`.
* Every other event reads nothing from stdout, so this writer emits nothing
* for them: an unexpected stdout payload is exactly as unsafe here as a
* missing one.
*
* This module never decides *whether* to run - it is wired in declaratively
* via `AgentHookConfig.writeStdoutResponse` (see cursor-ide.plugin.ts), so
* `hook.ts` never needs an `if (agentName === 'cursor-ide')` branch to call
* it only for this agent.
*
* See: https://cursor.com/docs/hooks
*/

const ALLOW_RESPONSE = JSON.stringify({ permission: 'allow' });
const CONTINUE_RESPONSE = JSON.stringify({ continue: true });

/**
* Cursor-native event names (not the internal names they map onto) that
* carry a response contract.
*/
const CURSOR_STDOUT_RESPONSES: Readonly<Record<string, string>> = {
preToolUse: ALLOW_RESPONSE,
beforeShellExecution: ALLOW_RESPONSE,
beforeMCPExecution: ALLOW_RESPONSE,
beforeReadFile: ALLOW_RESPONSE,
beforeTabFileRead: ALLOW_RESPONSE,
subagentStart: ALLOW_RESPONSE,
beforeSubmitPrompt: CONTINUE_RESPONSE,
};

/**
* Writes Cursor's expected stdout response for `nativeEventName`, if any.
* A no-op for every event outside the response matrix above.
*
* @param nativeEventName - The Cursor-native event name (`hook_event_name`
* on the raw/transformed payload - never the internal name it maps onto).
*/
export function writeCursorResponse(nativeEventName: string): void {
const response = CURSOR_STDOUT_RESPONSES[nativeEventName];
if (response) {
process.stdout.write(`${response}\n`);
}
}
2 changes: 2 additions & 0 deletions src/agents/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { KimiPlugin } from './plugins/kimi/kimi.plugin.js';
import { KimiAcpPlugin } from './plugins/kimi/kimi-acp.plugin.js';
import { OpenWikiPlugin } from './plugins/openwiki/openwiki.plugin.js';
import { CopilotCliPlugin } from './plugins/copilot-cli/index.js';
import { CursorIdePlugin } from './plugins/cursor-ide/cursor-ide.plugin.js';
import { AgentAdapter, AgentAnalyticsAdapter } from './core/types.js';

// Re-export for backwards compatibility
Expand Down Expand Up @@ -43,6 +44,7 @@ export class AgentRegistry {
AgentRegistry.registerPlugin(new KimiAcpPlugin());
AgentRegistry.registerPlugin(new OpenWikiPlugin());
AgentRegistry.registerPlugin(new CopilotCliPlugin());
AgentRegistry.registerPlugin(new CursorIdePlugin());

AgentRegistry.initialized = true;
}
Expand Down
Loading