Skip to content

feat: Add support for Cursor Pro - #119

Merged
veerareddyvishal144 merged 2 commits into
mainfrom
scratch/cursor_integration
Sep 29, 2026
Merged

veerareddyvishal144 merged 2 commits into
mainfrom
scratch/cursor_integration

Conversation

@veerareddyvishal144

Copy link
Copy Markdown
Contributor

No description provided.

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 6 issue(s) in this PR.

  • ✅ Successfully posted inline: 6 comment(s)

Comment on lines +31 to +32
],
"frontier": [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other · low
Added "frontier" key to config/difficulty-anchors.json. Please verify the key name is intentionally "frontier" and not a typo (e.g., "frontier-level" or "frontier-tier") to maintain consistency with existing keys like "design", "code", etc., or ensure it aligns with the intended schema.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional. frontier is the anchor class src/routing/intent-score.js reads (sims.frontier, FRONTIER_MIN_SIM, the REASONING band) alongside trivial/substantive/heavyweight. No change.

Comment thread src/api/router.js Outdated
// wrapper text); the force/risk probes below get envelope-stripped text
// so Cursor's <user_info>/<rules> blocks can't fire triggers on "Hi".
const isSuggestionMode = _lastUserText.includes('[SUGGESTION MODE:');
const _lastUserAskClean = require("../routing/harness-envelope").stripHarnessEnvelope(_lastUserText);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · high
Dynamic require of harness-envelope on the hot request path (line 1416) may cause runtime failures if module paths or function signatures change. Consider hoisting to top-level require like other routing modules (e.g., complexity-analyzer at line 129).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4a56997: stripHarnessEnvelope is now a top-level require in router.js.

Comment thread src/cache/embeddings.js
// Already-degraded providers get no second chance (this attempt WAS the
// post-cooldown probe); healthy ones earn one retry before the flip.
if (embeddingProviderAvailable !== false) {
await new Promise((r) => setTimeout(r, TRANSIENT_RETRY_DELAY_MS));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
The retry delay uses Date.now() for timing. If the system clock is adjusted (e.g., NTP sync), the retry may execute sooner or later than expected. For production-grade code, consider using a monotonic clock or document this behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving as-is. The retry delay itself is a setTimeout; only the degraded-provider cooldown gate reads Date.now(), and a clock step there just shifts one re-probe slightly earlier or later, with no correctness impact.

Comment thread src/clients/databricks.js
const LYNKR_BADGE_PREFIX_RE = /^\*\[Lynkr\][^*\n]*\*\s*/;
// Mid-message badge LINES (tool narration, 2026-09-27) — strip anywhere in
// assistant content, swallowing surrounding blank lines so paragraphs reflow.
const LYNKR_BADGE_LINE_RE = /\n{0,2}\*\[Lynkr\][^*\n]*\*[ \t]*(?=\n|$)/g;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
LYNKR_BADGE_LINE_RE regex uses {0,2} quantifier which may cause backtracking on large inputs with many consecutive newlines. For better performance and predictability, consider using a simpler approach like \n{0,2}\*\[Lynkr\][^\n]*\* without optional trailing whitespace inside the group.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change. \n{0,2} is bounded and there's no nested or overlapping quantifier ([^*\n]* can't consume the * or newline that follows), so matching is linear with no catastrophic backtracking.

probes.set('cursor', async () => {
const { execFileSync } = require('node:child_process');
const binary = config.cursor?.binaryPath?.trim() || process.env.CURSOR_BINARY_PATH?.trim() || 'cursor-agent';
execFileSync(binary, ['--version'], { timeout: PROBE_TIMEOUT_MS, stdio: 'ignore' });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · low
The health probe uses execFileSync with stdio: 'ignore' and timeout. This is acceptable but ensure the binary path is validated (not user-controlled) to prevent command injection. The code uses config.cursor?.binaryPath which comes from config - verify this is validated at config load time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The probe uses execFileSync (no shell) and the path comes from operator config/env, so there's no injection here. The review did point at a real sibling issue: cursor-utils.isAvailable ran a shell-interpolated `which ${binary}`. Fixed in 4a56997 to use execFileSync("which", [binary]).

Comment on lines +45 to +47
const UNCLOSED_RES = ENVELOPE_TAGS.map(
(t) => new RegExp(`(?:^|\\n)<${t}(?:\\s[^>]*)?>[\\s\\S]*$`, 'i')
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
The UNCLOSED_RES regex lacks the 'g' flag. Without it, replace() only strips the first unclosed tag match per string. If multiple unclosed tags appear (e.g., on different lines after upstream truncation), subsequent tags remain unstripped, causing false positives in risk/force scoring.

Fix: Add the 'g' flag to the UNCLOSED_RES regex construction so all unclosed tags get replaced in one pass.

Suggestion:

Suggested change
const UNCLOSED_RES = ENVELOPE_TAGS.map(
(t) => new RegExp(`(?:^|\\n)<${t}(?:\\s[^>]*)?>[\\s\\S]*$`, 'i')
);
const UNCLOSED_RES = ENVELOPE_TAGS.map(
(t) => new RegExp(`(?:^|\\n)<${t}(?:\\s[^>]*)?>[\\s\\S]*$`, 'gi')
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bug. Each unclosed pattern ends in [\s\S]*$, so the first match swallows to the end of the string, including any later unclosed tags. There is nothing left for a second match, so g would be a no-op. Verified: "ask\n<rules>\na\n<rules>\nb\n<user_info>\nc" → "ask".

Comment thread src/routing/risk-analyzer.js Outdated
Comment on lines +164 to +168
.replace(/<user_info>[\s\S]*?<\/user_info>/g, ' ')
.replace(/<agent_transcripts>[\s\S]*?<\/agent_transcripts>/g, ' ')
.replace(/<always_applied_workspace_rules?>[\s\S]*?<\/always_applied_workspace_rules?>/g, ' ')
.replace(/<always_applied_workspace_rule\b[^>]*>[\s\S]*?<\/always_applied_workspace_rule>/g, ' ')
.replace(/<rules>[\s\S]*?<\/rules>/g, ' ')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
Risk analyzer duplicates harness-envelope regex logic (lines 164-168). This duplicates maintenance effort and risks inconsistency between risk scoring and other scorers that use stripHarnessEnvelope.

Fix: Replace manual regex chains with require('./harness-envelope').stripHarnessEnvelope(text) in stripSystemReminders.

Suggestion:

Suggested change
.replace(/<user_info>[\s\S]*?<\/user_info>/g, ' ')
.replace(/<agent_transcripts>[\s\S]*?<\/agent_transcripts>/g, ' ')
.replace(/<always_applied_workspace_rules?>[\s\S]*?<\/always_applied_workspace_rules?>/g, ' ')
.replace(/<always_applied_workspace_rule\b[^>]*>[\s\S]*?<\/always_applied_workspace_rule>/g, ' ')
.replace(/<rules>[\s\S]*?<\/rules>/g, ' ')
.replace(/<user_info>[\s\S]*?<\/user_info>/g, ' ')
.replace(/<agent_transcripts>[\s\S]*?<\/agent_transcripts>/g, ' ')
.replace(/<always_applied_workspace_rules?>[\s\S]*?<\/always_applied_workspace_rules?>/g, ' ')
.replace(/<always_applied_workspace_rule\b[^>]*>[\s\S]*?<\/always_applied_workspace_rule>/g, ' ')
.replace(/<rules>[\s\S]*?<\/rules>/g, ' ')
.replace(/<user_info>[\s\S]*?<\/user_info>/g, ' ')
.replace(/<agent_transcripts>[\s\S]*?<\/agent_transcripts>/g, ' ')
.replace(/<always_applied_workspace_rules?>[\s\S]*?<\/always_applied_workspace_rules?>/g, ' ')
.replace(/<always_applied_workspace_rule\b[^>]*>[\s\S]*?<\/always_applied_workspace_rule>/g, ' ')
.replace(/<rules>[\s\S]*?<\/rules>/g, ' ')
// ... then use stripHarnessEnvelope at the top of the function instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4a56997: stripSystemReminders now calls the shared stripHarnessEnvelope() first and the duplicated Cursor regexes are gone. Added always_applied_workspace_rules to ENVELOPE_TAGS so coverage is unchanged.

Comment thread src/routing/risk-analyzer.js Outdated
Comment on lines +164 to +168
.replace(/<user_info>[\s\S]*?<\/user_info>/g, ' ')
.replace(/<agent_transcripts>[\s\S]*?<\/agent_transcripts>/g, ' ')
.replace(/<always_applied_workspace_rules?>[\s\S]*?<\/always_applied_workspace_rules?>/g, ' ')
.replace(/<always_applied_workspace_rule\b[^>]*>[\s\S]*?<\/always_applied_workspace_rule>/g, ' ')
.replace(/<rules>[\s\S]*?<\/rules>/g, ' ')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
Risk analyzer duplicates harness-envelope regex logic (lines 164-168). This creates maintenance overhead and risks inconsistency when new harness tags are added.

Fix: Replace the manual regex chain with stripHarnessEnvelope(text) from harness-envelope.js for unified stripping.

Suggestion:

Suggested change
.replace(/<user_info>[\s\S]*?<\/user_info>/g, ' ')
.replace(/<agent_transcripts>[\s\S]*?<\/agent_transcripts>/g, ' ')
.replace(/<always_applied_workspace_rules?>[\s\S]*?<\/always_applied_workspace_rules?>/g, ' ')
.replace(/<always_applied_workspace_rule\b[^>]*>[\s\S]*?<\/always_applied_workspace_rule>/g, ' ')
.replace(/<rules>[\s\S]*?<\/rules>/g, ' ')
// Use unified harness-envelope stripping instead of duplicating regex logic
.replace(/<user_info>[\s\S]*?<\/user_info>/g, ' ')
.replace(/<agent_transcripts>[\s\S]*?<\/agent_transcripts>/g, ' ')
.replace(/<always_applied_workspace_rules?>[\s\S]*?<\/always_applied_workspace_rules?>/g, ' ')
.replace(/<always_applied_workspace_rule\b[^>]*>[\s\S]*?<\/always_applied_workspace_rule>/g, ' ')
.replace(/<rules>[\s\S]*?<\/rules>/g, ' ')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix as above, in 4a56997.

Comment thread src/clients/cursor-utils.js Outdated
// one-shot paths. NOTE: with shell allowed, the sandbox cwd is a default
// directory, not a security boundary. Flip to false to restore the
// deny-mutations policy (MCPs-only approval, no --force).
const CURSOR_AUTO_APPROVE = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
CURSOR_AUTO_APPROVE is hardcoded to true, automatically approving ALL agent permission requests including shell execution. As noted in the code comment, this bypasses security boundaries when sandbox cwd is not restricted. Consider making this configurable via env var for production deployments, or document the risk clearly in a config file.

Suggestion:

Suggested change
const CURSOR_AUTO_APPROVE = true;
// Configure via CURSOR_AUTO_APPROVE env var for production safety
const CURSOR_AUTO_APPROVE = process.env.CURSOR_AUTO_APPROVE === 'true';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made configurable in 4a56997: CURSOR_AUTO_APPROVE env var, default true (the existing operator decision), and CURSOR_AUTO_APPROVE=false restores the deny-mutations policy. Documented with the security caveat in .env.example.

try {
const binary = process.env.CURSOR_BINARY_PATH?.trim() || DEFAULT_BINARY;
run(`which ${binary}`, { stdio: "ignore" });
if (!whichFn) setImmediate(() => warmupCursorAgent().catch(() => {}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
Warmup failures are silently caught with catch(() => {}). The first user request will then incur full cold-start latency (~20s) without any warning. Consider at least logging the error at a higher level (info/warn) so ops can detect warmup issues.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warmupCursorAgent catches internally, so that .catch is only a guard. The internal log was at debug, though; raised to warn in 4a56997 so a failed warmup is visible.

Comment on lines +254 to +255
function convertCursorResponseToAnthropic(text, model, usage = null, thinking = "") {
const estimatedOutputTokens = Math.ceil(String(text || "").length / 4);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · low
Token estimation in convertCursorResponseToAnthropic uses text.length / 4. This heuristic may significantly differ from actual tokenizers (e.g., cl100k_base). For billing accuracy or rate-limiting, either use a proper tokenizer or document that this is an estimate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving as-is. The chars/4 value is only a fallback when the CLI reports no usage (ACP carries none); real usage from the CLI takes precedence. It's already named estimatedOutputTokens.

}

// --- resume cache: Lynkr session key → CLI session_id ------------------------
const _sessionCache = new Map();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test · medium
The global concurrency gate (_inFlight, _waiters) and session cache (_sessionCache, _acpClients) persist across test runs unless explicitly reset. Tests that spawn multiple instances or restart the module may leak state. Add cleanup helpers or document teardown requirements.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change. node --test runs each test file in its own process, so this module state doesn't leak between files, and the in-file tests inject execFn and bypass the ACP/session caches.

- risk-analyzer: reuse stripHarnessEnvelope instead of duplicating the
  Cursor tag regexes; add always_applied_workspace_rules to ENVELOPE_TAGS
- router: hoist harness-envelope require off the request path
- cursor-utils: CURSOR_AUTO_APPROVE is now env-configurable (default true);
  isAvailable uses execFileSync instead of a shell-interpolated which;
  warmup failures log at warn; replace raw NUL in the ACP client key with
  an escape so the file is no longer treated as binary

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread src/api/router.js
})(),
// This branch IS the flat-fee subscription: the gate's dollar
// break-even leg has no premium to amortize here.
flatRate: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
Hardcoded 'true' for flatRate may be misleading; ensure this is always accurate for passthrough routes or derive it from tier configuration.

Suggestion:

Suggested change
flatRate: true,
flatRate: tier?.flatRate ?? true,

Comment thread src/cache/embeddings.js
Comment on lines +201 to +202
if (embeddingProviderAvailable !== false) {
await new Promise((r) => setTimeout(r, TRANSIENT_RETRY_DELAY_MS));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
The code uses a Promise-based setTimeout (await new Promise((r) => setTimeout(r, TRANSIENT_RETRY_DELAY_MS))) but the project prefers async/await patterns. While function is not a security issue, it's inconsistent with async best practices.

Suggestion: Use a helper function like const delay = (ms) => new Promise((r) => setTimeout(r, ms)); and then call await delay(TRANSIENT_RETRY_DELAY_MS);

Suggestion:

Suggested change
if (embeddingProviderAvailable !== false) {
await new Promise((r) => setTimeout(r, TRANSIENT_RETRY_DELAY_MS));
function delay(ms) {
return new Promise((r) => setTimeout(r, ms));
}
// In _wrapProvider:
await delay(TRANSIENT_RETRY_DELAY_MS);

Comment on lines +74 to +79
} else if (agenticResult && agenticResult.isAgentic === false) {
// The detector already scored this turn NON-agentic using EFFECTIVE
// tools (harness baseline subtracted, WS3.2). Attached-tool dims must
// not out-vote that verdict: a bare "Hi" from a harness with 9 default
// tools is not a tool-orchestration workload. Cap, never raise.
toolUse = Math.min(toolUse, 0.25);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
The new else-if branch for non-agentic turns uses a hardcoded threshold (0.25). Consider extracting this to a named constant for better maintainability and easier tuning across the codebase.

Suggestion:

Suggested change
} else if (agenticResult && agenticResult.isAgentic === false) {
// The detector already scored this turn NON-agentic using EFFECTIVE
// tools (harness baseline subtracted, WS3.2). Attached-tool dims must
// not out-vote that verdict: a bare "Hi" from a harness with 9 default
// tools is not a tool-orchestration workload. Cap, never raise.
toolUse = Math.min(toolUse, 0.25);
const NON_AenticENTIC_CAP = 0.25;
// ...
} else if (agenticResult && agenticResult.isAgentic === false) {
toolUse = Math.min(toolUse, NON_AGENIC_CAP);

Comment on lines +56 to +59
function stripHarnessEnvelope(text) {
if (typeof text !== 'string' || text.length === 0) return typeof text === 'string' ? text : '';
try {
if (!text.includes('<')) return text;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
The stripHarnessEnvelope function uses regex matching on user input without explicit length validation. While the function has a try/catch block and basic type checking, very large inputs could potentially cause performance issues or ReDoS (Regular Expression Denial of Service) vulnerabilities through the multiple regex operations (USER_QUERY_RE, PAIRED_RES, UNCLOSED_RES).

Suggestion:

Suggested change
function stripHarnessEnvelope(text) {
if (typeof text !== 'string' || text.length === 0) return typeof text === 'string' ? text : '';
try {
if (!text.includes('<')) return text;
function stripHarnessEnvelope(text) {
if (typeof text !== 'string' || text.length === 0) return typeof text === 'string' ? text : '';
if (text.length > 10000) return text; // Limit regex processing to prevent ReDoS
try {
if (!text.includes('<')) return text;

Comment thread src/routing/index.js
Comment on lines +1439 to +1440
const _tierLadder = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
const _legacyIdx = Math.max(0, _tierLadder.indexOf(tier));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
When tier is not found in _tierLadder (e.g., an unknown or future tier value), indexOf returns -1 and Math.max(0, -1) makes _legacyIdx 0. This incorrectly treats unknown tiers as 'SIMPLE', potentially causing unexpected one-band-up caps.

Suggestion:

Suggested change
const _tierLadder = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
const _legacyIdx = Math.max(0, _tierLadder.indexOf(tier));
const _tierLadder = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
const _legacyIdx = _tierLadder.indexOf(tier);
if (_legacyIdx === -1) { /* unknown tier: skip cap logic or default to lowest */ }

toolCall: info.tool_call ?? false,
reasoning: info.reasoning ?? false,
vision: Array.isArray(info.input) && info.input.includes('image'),
vision: Array.isArray(info.modalities?.input) && info.modalities.input.includes('image'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
The vision property check on line 287 assumes info.modalities?.input exists, but the fallback info.input may still be used by some model definitions. If modalities is not present but input is, the model will incorrectly have vision:false.

Suggestion:

Suggested change
vision: Array.isArray(info.modalities?.input) && info.modalities.input.includes('image'),
vision: (Array.isArray(info.modalities?.input) && info.modalities.input.includes('image')) || (Array.isArray(info.input) && info.input.includes('image')),

@veerareddyvishal144

Copy link
Copy Markdown
Contributor Author

Re the non-inline risk-analyzer.js L144-147 finding: addressed in 4a56997. stripSystemReminders now delegates to stripHarnessEnvelope(). All other findings have inline replies.

@veerareddyvishal144
veerareddyvishal144 merged commit ac412ee into main Sep 29, 2026
3 of 4 checks passed
@veerareddyvishal144
veerareddyvishal144 deleted the scratch/cursor_integration branch September 29, 2026 05:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant