@rama_nigg/open-cursor 2.2.1 → 2.3.0

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.
Files changed (55) hide show
  1. package/package.json +9 -3
  2. package/src/acp/metrics.ts +83 -0
  3. package/src/acp/sessions.ts +107 -0
  4. package/src/acp/tools.ts +209 -0
  5. package/src/auth.ts +269 -0
  6. package/src/cli/discover.ts +53 -0
  7. package/src/cli/model-discovery.ts +50 -0
  8. package/src/cli/opencode-cursor.ts +620 -0
  9. package/src/client/simple.ts +277 -0
  10. package/src/commands/status.ts +39 -0
  11. package/src/index.ts +40 -0
  12. package/src/models/config.ts +64 -0
  13. package/src/models/discovery.ts +132 -0
  14. package/src/models/index.ts +3 -0
  15. package/src/models/types.ts +11 -0
  16. package/src/plugin-entry.ts +28 -0
  17. package/src/plugin-toggle.ts +67 -0
  18. package/src/plugin.ts +1918 -0
  19. package/src/provider/boundary.ts +161 -0
  20. package/src/provider/runtime-interception.ts +721 -0
  21. package/src/provider/tool-loop-guard.ts +644 -0
  22. package/src/provider/tool-schema-compat.ts +516 -0
  23. package/src/provider.ts +268 -0
  24. package/src/proxy/formatter.ts +42 -0
  25. package/src/proxy/handler.ts +29 -0
  26. package/src/proxy/prompt-builder.ts +171 -0
  27. package/src/proxy/server.ts +207 -0
  28. package/src/proxy/tool-loop.ts +317 -0
  29. package/src/proxy/types.ts +13 -0
  30. package/src/streaming/ai-sdk-parts.ts +105 -0
  31. package/src/streaming/delta-tracker.ts +33 -0
  32. package/src/streaming/line-buffer.ts +44 -0
  33. package/src/streaming/openai-sse.ts +114 -0
  34. package/src/streaming/parser.ts +22 -0
  35. package/src/streaming/types.ts +152 -0
  36. package/src/tools/core/executor.ts +25 -0
  37. package/src/tools/core/registry.ts +27 -0
  38. package/src/tools/core/types.ts +31 -0
  39. package/src/tools/defaults.ts +673 -0
  40. package/src/tools/discovery.ts +140 -0
  41. package/src/tools/executors/cli.ts +58 -0
  42. package/src/tools/executors/local.ts +25 -0
  43. package/src/tools/executors/mcp.ts +39 -0
  44. package/src/tools/executors/sdk.ts +39 -0
  45. package/src/tools/index.ts +8 -0
  46. package/src/tools/registry.ts +34 -0
  47. package/src/tools/router.ts +123 -0
  48. package/src/tools/schema.ts +58 -0
  49. package/src/tools/skills/loader.ts +61 -0
  50. package/src/tools/skills/resolver.ts +21 -0
  51. package/src/tools/types.ts +29 -0
  52. package/src/types.ts +8 -0
  53. package/src/utils/errors.ts +131 -0
  54. package/src/utils/logger.ts +146 -0
  55. package/src/utils/perf.ts +44 -0
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@rama_nigg/open-cursor",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "No prompt limits. No broken streams. Full thinking + tool support. Your Cursor subscription, properly integrated.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
+ "module": "src/plugin-entry.ts",
7
8
  "scripts": {
8
9
  "build": "bun build ./src/index.ts ./src/plugin-entry.ts ./src/cli/discover.ts ./src/cli/opencode-cursor.ts --outdir ./dist --target node",
9
10
  "dev": "bun build ./src/index.ts ./src/plugin-entry.ts ./src/cli/discover.ts ./src/cli/opencode-cursor.ts --outdir ./dist --target node --watch",
@@ -20,10 +21,15 @@
20
21
  "cursor-discover": "dist/cli/discover.js"
21
22
  },
22
23
  "exports": {
23
- ".": "./dist/index.js"
24
+ ".": {
25
+ "bun": "./src/plugin-entry.ts",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
28
+ }
24
29
  },
25
30
  "files": [
26
- "dist"
31
+ "dist",
32
+ "src"
27
33
  ],
28
34
  "dependencies": {
29
35
  "ai": "^6.0.55",
@@ -0,0 +1,83 @@
1
+ export interface SessionMetrics {
2
+ sessionId: string;
3
+ model: string;
4
+ promptTokens: number;
5
+ toolCalls: number;
6
+ duration: number;
7
+ timestamp: number;
8
+ }
9
+
10
+ export interface AggregateMetrics {
11
+ totalPrompts: number;
12
+ totalToolCalls: number;
13
+ totalDuration: number;
14
+ avgDuration: number;
15
+ }
16
+
17
+ export class MetricsTracker {
18
+ private sessions: Map<string, SessionMetrics> = new Map();
19
+
20
+ recordPrompt(sessionId: string, model: string, tokens: number): void {
21
+ const existing = this.sessions.get(sessionId);
22
+ if (existing) {
23
+ existing.promptTokens = tokens;
24
+ existing.model = model;
25
+ } else {
26
+ this.sessions.set(sessionId, {
27
+ sessionId,
28
+ model,
29
+ promptTokens: tokens,
30
+ toolCalls: 0,
31
+ duration: 0,
32
+ timestamp: Date.now()
33
+ });
34
+ }
35
+ }
36
+
37
+ recordToolCall(sessionId: string, toolName: string, duration: number): void {
38
+ const existing = this.sessions.get(sessionId);
39
+ if (existing) {
40
+ existing.toolCalls++;
41
+ existing.duration += duration;
42
+ }
43
+ // If no session exists, silently ignore (matches test expectations)
44
+ }
45
+
46
+ getSessionMetrics(sessionId: string): SessionMetrics | undefined {
47
+ return this.sessions.get(sessionId);
48
+ }
49
+
50
+ getAggregateMetrics(hours: number): AggregateMetrics {
51
+ const cutoff = Date.now() - (hours * 60 * 60 * 1000);
52
+ let totalPrompts = 0;
53
+ let totalToolCalls = 0;
54
+ let totalDuration = 0;
55
+
56
+ for (const metrics of this.sessions.values()) {
57
+ if (metrics.timestamp >= cutoff) {
58
+ totalPrompts++;
59
+ totalToolCalls += metrics.toolCalls;
60
+ totalDuration += metrics.duration;
61
+ }
62
+ }
63
+
64
+ return {
65
+ totalPrompts,
66
+ totalToolCalls,
67
+ totalDuration,
68
+ avgDuration: totalPrompts > 0 ? Math.round(totalDuration / totalPrompts) : 0
69
+ };
70
+ }
71
+
72
+ clearMetrics(sessionId?: string): void {
73
+ if (sessionId) {
74
+ this.sessions.delete(sessionId);
75
+ } else {
76
+ this.sessions.clear();
77
+ }
78
+ }
79
+
80
+ clearAll(): void {
81
+ this.sessions.clear();
82
+ }
83
+ }
@@ -0,0 +1,107 @@
1
+ import { mkdir, readFile, writeFile } from "fs/promises";
2
+ import { dirname, join } from "path";
3
+
4
+ export interface Session {
5
+ id: string;
6
+ cwd: string;
7
+ modeId?: string;
8
+ cancelled?: boolean;
9
+ resumeId?: string;
10
+ createdAt: number;
11
+ updatedAt: number;
12
+ }
13
+
14
+ export interface SessionCreateOptions {
15
+ cwd?: string;
16
+ modeId?: string;
17
+ }
18
+
19
+ export class SessionManager {
20
+ private sessions: Map<string, Session> = new Map();
21
+ private storagePath: string;
22
+
23
+ constructor(storagePath?: string) {
24
+ this.storagePath = storagePath || join(process.cwd(), ".opencode", "sessions.json");
25
+ }
26
+
27
+ async initialize(): Promise<void> {
28
+ // Load sessions from disk if storage file exists
29
+ try {
30
+ const data = await readFile(this.storagePath, "utf-8");
31
+ const sessions = JSON.parse(data) as Record<string, Session>;
32
+ this.sessions = new Map(Object.entries(sessions));
33
+ } catch {
34
+ // File doesn't exist or is invalid, start fresh
35
+ this.sessions.clear();
36
+ }
37
+ }
38
+
39
+ private async persist(): Promise<void> {
40
+ // Save sessions to disk
41
+ const dir = dirname(this.storagePath);
42
+ await mkdir(dir, { recursive: true });
43
+ const data = JSON.stringify(Object.fromEntries(this.sessions), null, 2);
44
+ await writeFile(this.storagePath, data, "utf-8");
45
+ }
46
+
47
+ async createSession(options: SessionCreateOptions): Promise<Session> {
48
+ const id = `session-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
49
+ const session: Session = {
50
+ id,
51
+ cwd: options.cwd || process.cwd(),
52
+ modeId: options.modeId,
53
+ cancelled: false,
54
+ createdAt: Date.now(),
55
+ updatedAt: Date.now()
56
+ };
57
+
58
+ this.sessions.set(id, session);
59
+ await this.persist();
60
+ return session;
61
+ }
62
+
63
+ async getSession(id: string): Promise<Session | null> {
64
+ return this.sessions.get(id) || null;
65
+ }
66
+
67
+ async updateSession(id: string, updates: Partial<Session>): Promise<void> {
68
+ const session = this.sessions.get(id);
69
+ if (session) {
70
+ Object.assign(session, updates, { updatedAt: Date.now() });
71
+ await this.persist();
72
+ }
73
+ }
74
+
75
+ async deleteSession(id: string): Promise<void> {
76
+ this.sessions.delete(id);
77
+ await this.persist();
78
+ }
79
+
80
+ isCancelled(id: string): boolean {
81
+ const session = this.sessions.get(id);
82
+ return session?.cancelled || false;
83
+ }
84
+
85
+ markCancelled(id: string): void {
86
+ const session = this.sessions.get(id);
87
+ if (session) {
88
+ session.cancelled = true;
89
+ session.updatedAt = Date.now();
90
+ this.persist().catch(() => {});
91
+ }
92
+ }
93
+
94
+ canResume(id: string): boolean {
95
+ const session = this.sessions.get(id);
96
+ return !!session?.resumeId;
97
+ }
98
+
99
+ setResumeId(id: string, resumeId: string): void {
100
+ const session = this.sessions.get(id);
101
+ if (session) {
102
+ session.resumeId = resumeId;
103
+ session.updatedAt = Date.now();
104
+ this.persist().catch(() => {});
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,209 @@
1
+ export interface ToolUpdate {
2
+ sessionId: string;
3
+ toolCallId: string;
4
+ title?: string;
5
+ kind?: 'read' | 'write' | 'edit' | 'search' | 'execute' | 'other';
6
+ status?: 'pending' | 'in_progress' | 'completed' | 'failed';
7
+ locations?: Array<{ path: string; line?: number }>;
8
+ content?: Array<{ type: string; [key: string]: unknown }>;
9
+ rawOutput?: string;
10
+ startTime?: number;
11
+ endTime?: number;
12
+ }
13
+
14
+ interface CursorEvent {
15
+ type: string;
16
+ call_id?: string;
17
+ tool_call_id?: string;
18
+ subtype?: string;
19
+ tool_call?: {
20
+ [key: string]: {
21
+ args?: Record<string, unknown>;
22
+ result?: Record<string, unknown>;
23
+ };
24
+ };
25
+ }
26
+
27
+ export class ToolMapper {
28
+ async mapCursorEventToAcp(event: CursorEvent, sessionId: string): Promise<ToolUpdate[]> {
29
+ if (event.type !== 'tool_call') {
30
+ return [];
31
+ }
32
+
33
+ const updates: ToolUpdate[] = [];
34
+ const toolCallId = event.call_id || event.tool_call_id || 'unknown';
35
+ const subtype = event.subtype || 'started';
36
+
37
+ // Completed/failed events return 1 update with results
38
+ if (subtype === 'completed' || subtype === 'failed') {
39
+ const result = this.extractResult(event.tool_call || {});
40
+ const locations = result.locations?.length ? result.locations : this.extractLocations(event.tool_call || {});
41
+
42
+ updates.push({
43
+ sessionId,
44
+ toolCallId,
45
+ title: this.buildToolTitle(event.tool_call || {}),
46
+ kind: this.inferToolType(event.tool_call || {}),
47
+ status: result.error ? 'failed' : 'completed',
48
+ content: result.content,
49
+ locations,
50
+ rawOutput: result.rawOutput,
51
+ endTime: Date.now()
52
+ });
53
+ } else {
54
+ // Started events return 2 updates: pending and in_progress
55
+ updates.push({
56
+ sessionId,
57
+ toolCallId,
58
+ title: this.buildToolTitle(event.tool_call || {}),
59
+ kind: this.inferToolType(event.tool_call || {}),
60
+ status: 'pending',
61
+ locations: this.extractLocations(event.tool_call || {}),
62
+ startTime: Date.now()
63
+ });
64
+
65
+ updates.push({
66
+ sessionId,
67
+ toolCallId,
68
+ status: 'in_progress'
69
+ });
70
+ }
71
+
72
+ return updates;
73
+ }
74
+
75
+ private inferToolType(toolCall: Record<string, unknown>): ToolUpdate['kind'] {
76
+ const keys = Object.keys(toolCall);
77
+ for (const key of keys) {
78
+ if (key.includes('read')) return 'read';
79
+ if (key.includes('write')) return 'edit';
80
+ if (key.includes('grep') || key.includes('glob')) return 'search';
81
+ if (key.includes('bash') || key.includes('shell')) return 'execute';
82
+ }
83
+ return 'other';
84
+ }
85
+
86
+ private buildToolTitle(toolCall: Record<string, unknown>): string {
87
+ const keys = Object.keys(toolCall);
88
+ for (const key of keys) {
89
+ const tool = toolCall[key] as { args?: Record<string, unknown> } | undefined;
90
+ const args = tool?.args || {};
91
+
92
+ if (key.includes('read') && args.path) return `Read ${args.path}`;
93
+ if (key.includes('write') && args.path) return `Write ${args.path}`;
94
+ if (key.includes('grep')) {
95
+ const pattern = args.pattern || 'pattern';
96
+ const path = args.path;
97
+ return path ? `Search ${path} for ${pattern}` : `Search for ${pattern}`;
98
+ }
99
+ if (key.includes('glob') && args.pattern) return `Glob ${args.pattern}`;
100
+ if ((key.includes('bash') || key.includes('shell')) && (args.command || args.cmd)) {
101
+ return `\`${args.command || args.cmd}\``;
102
+ }
103
+ if ((key.includes('bash') || key.includes('shell')) && args.commands && Array.isArray(args.commands)) {
104
+ return `\`${args.commands.join(' && ')}\``;
105
+ }
106
+ }
107
+ return 'other';
108
+ }
109
+
110
+ private extractLocations(toolCall: Record<string, unknown>): ToolUpdate['locations'] {
111
+ const keys = Object.keys(toolCall);
112
+ for (const key of keys) {
113
+ const tool = toolCall[key] as { args?: Record<string, unknown> } | undefined;
114
+ const args = tool?.args || {};
115
+
116
+ if (args.path) {
117
+ if (typeof args.path === 'string') {
118
+ return [{ path: args.path, line: args.line as number | undefined }];
119
+ }
120
+ if (Array.isArray(args.path)) {
121
+ return args.path.map((p: string | { path: string; line?: number }) =>
122
+ typeof p === 'string' ? { path: p } : { path: p.path, line: p.line }
123
+ );
124
+ }
125
+ }
126
+
127
+ if (args.paths && Array.isArray(args.paths)) {
128
+ return args.paths.map((p: string | { path: string; line?: number }) =>
129
+ typeof p === 'string' ? { path: p } : { path: p.path, line: p.line }
130
+ );
131
+ }
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ private extractResult(toolCall: Record<string, unknown>): {
137
+ error?: string;
138
+ content?: ToolUpdate['content'];
139
+ locations?: ToolUpdate['locations'];
140
+ rawOutput?: string;
141
+ } {
142
+ const keys = Object.keys(toolCall);
143
+ for (const key of keys) {
144
+ const tool = toolCall[key] as {
145
+ result?: Record<string, unknown>;
146
+ args?: Record<string, unknown>;
147
+ } | undefined;
148
+ const result = tool?.result || {};
149
+
150
+ if (result.error) {
151
+ return { error: result.error as string };
152
+ }
153
+
154
+ const locations: ToolUpdate['locations'] = [];
155
+ if (result.matches && Array.isArray(result.matches)) {
156
+ locations.push(...result.matches.map((m: { path: string; line?: number }) => ({
157
+ path: m.path,
158
+ line: m.line
159
+ })));
160
+ }
161
+ if (result.files && Array.isArray(result.files)) {
162
+ locations.push(...result.files.map((f: string) => ({ path: f })));
163
+ }
164
+ if (result.path) {
165
+ locations.push({ path: result.path as string, line: result.line as number | undefined });
166
+ }
167
+
168
+ const content: ToolUpdate['content'] = [];
169
+
170
+ // Handle write operations with diff generation
171
+ if (key.includes('write')) {
172
+ const oldText = result.oldText ?? null;
173
+ const newText = result.newText as string | undefined;
174
+ const path = (tool?.args?.path as string) || (result.path as string);
175
+ if (newText !== undefined || oldText !== undefined) {
176
+ content.push({
177
+ type: 'diff',
178
+ path,
179
+ oldText,
180
+ newText
181
+ });
182
+ }
183
+ }
184
+
185
+ if (result.content) {
186
+ content.push({
187
+ type: 'content',
188
+ content: { text: result.content as string }
189
+ });
190
+ }
191
+
192
+ if (result.output !== undefined || result.exitCode !== undefined) {
193
+ content.push({
194
+ type: 'content',
195
+ content: {
196
+ text: `Exit code: ${result.exitCode ?? 0}\n${result.output || '(no output)'}`
197
+ }
198
+ });
199
+ }
200
+
201
+ return {
202
+ content: content.length > 0 ? content : undefined,
203
+ locations: locations.length > 0 ? locations : undefined,
204
+ rawOutput: JSON.stringify(result)
205
+ };
206
+ }
207
+ return {};
208
+ }
209
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,269 @@
1
+ // src/auth.ts
2
+
3
+ import { spawn } from "child_process";
4
+ import { existsSync } from "fs";
5
+ import { homedir, platform } from "os";
6
+ import { join } from "path";
7
+ import { createLogger } from "./utils/logger";
8
+ import { stripAnsi } from "./utils/errors";
9
+
10
+ const log = createLogger("auth");
11
+
12
+ // Polling configuration for auth file detection
13
+ const AUTH_POLL_INTERVAL = 2000; // Check every 2 seconds
14
+ const AUTH_POLL_TIMEOUT = 5 * 60 * 1000; // 5 minutes total timeout
15
+ const URL_EXTRACTION_TIMEOUT = 10000; // Wait up to 10 seconds for URL
16
+
17
+ export interface AuthResult {
18
+ type: "success" | "failed";
19
+ provider?: string;
20
+ key?: string;
21
+ error?: string;
22
+ }
23
+
24
+ function getHomeDir(): string {
25
+ const override = process.env.CURSOR_ACP_HOME_DIR;
26
+ if (override && override.length > 0) {
27
+ return override;
28
+ }
29
+ return homedir();
30
+ }
31
+
32
+ export async function pollForAuthFile(
33
+ timeoutMs: number = AUTH_POLL_TIMEOUT,
34
+ intervalMs: number = AUTH_POLL_INTERVAL
35
+ ): Promise<boolean> {
36
+ const startTime = Date.now();
37
+ const possiblePaths = getPossibleAuthPaths();
38
+
39
+ return new Promise((resolve) => {
40
+ const check = () => {
41
+ const elapsed = Date.now() - startTime;
42
+
43
+ for (const authPath of possiblePaths) {
44
+ if (existsSync(authPath)) {
45
+ log.debug("Auth file detected", { path: authPath });
46
+ resolve(true);
47
+ return;
48
+ }
49
+ }
50
+
51
+ log.debug("Polling for auth file", {
52
+ checkedPaths: possiblePaths,
53
+ elapsed: `${elapsed}ms`,
54
+ timeout: `${timeoutMs}ms`,
55
+ });
56
+
57
+ if (elapsed >= timeoutMs) {
58
+ log.debug("Auth file polling timed out");
59
+ resolve(false);
60
+ return;
61
+ }
62
+
63
+ setTimeout(check, intervalMs);
64
+ };
65
+
66
+ check();
67
+ });
68
+ }
69
+
70
+ export async function startCursorOAuth(): Promise<{
71
+ url: string;
72
+ instructions: string;
73
+ callback: () => Promise<AuthResult>;
74
+ }> {
75
+ return new Promise((resolve, reject) => {
76
+ log.info("Starting cursor-cli login process");
77
+
78
+ const proc = spawn("cursor-agent", ["login"], {
79
+ stdio: ["pipe", "pipe", "pipe"],
80
+ });
81
+
82
+ let stdout = "";
83
+ let stderr = "";
84
+ let urlExtracted = false;
85
+
86
+ proc.stdout.on("data", (data) => {
87
+ stdout += data.toString();
88
+ });
89
+
90
+ proc.stderr.on("data", (data) => {
91
+ stderr += data.toString();
92
+ });
93
+
94
+ const extractUrl = () => {
95
+ // Step 1: Strip ANSI codes
96
+ let cleanOutput = stripAnsi(stdout);
97
+ // Step 2: Remove ALL whitespace (newlines, spaces, tabs)
98
+ // The URL is split across lines with continuation spaces
99
+ cleanOutput = cleanOutput.replace(/\s/g, "");
100
+ // Step 3: Now extract the continuous URL
101
+ const urlMatch = cleanOutput.match(/https:\/\/cursor\.com\/loginDeepControl[^\s]*/);
102
+ if (urlMatch) {
103
+ return urlMatch[0];
104
+ }
105
+ return null;
106
+ };
107
+
108
+ // Try to extract URL with polling instead of fixed timeout
109
+ const tryExtractUrl = () => {
110
+ const url = extractUrl();
111
+
112
+ if (url && !urlExtracted) {
113
+ urlExtracted = true;
114
+ log.debug("Captured stdout", { length: stdout.length });
115
+ log.debug("Extracted URL", { url: url.substring(0, 50) + "..." });
116
+ log.info("Got login URL, waiting for browser auth");
117
+
118
+ resolve({
119
+ url,
120
+ instructions: "Click 'Continue with Cursor' in your browser to authenticate",
121
+ callback: async () => {
122
+ // Wait for process to complete
123
+ return new Promise((resolve) => {
124
+ let resolved = false;
125
+
126
+ const resolveOnce = (result: AuthResult) => {
127
+ if (!resolved) {
128
+ resolved = true;
129
+ resolve(result);
130
+ }
131
+ };
132
+
133
+ proc.on("close", async (code) => {
134
+ log.debug("Login process closed", { code });
135
+
136
+ // If process exited successfully, poll for auth file
137
+ if (code === 0) {
138
+ log.info("Process exited successfully, polling for auth file...");
139
+ const isAuthenticated = await pollForAuthFile();
140
+
141
+ if (isAuthenticated) {
142
+ log.info("Authentication successful");
143
+ resolveOnce({
144
+ type: "success",
145
+ provider: "cursor-acp",
146
+ key: "cursor-auth",
147
+ });
148
+ } else {
149
+ log.warn("Auth file not found after polling");
150
+ resolveOnce({
151
+ type: "failed",
152
+ error: "Authentication was not completed. Please try again.",
153
+ });
154
+ }
155
+ } else {
156
+ log.warn("Login process failed", { code });
157
+ resolveOnce({
158
+ type: "failed",
159
+ error: stderr ? stripAnsi(stderr) : `Authentication failed with code ${code}`,
160
+ });
161
+ }
162
+ });
163
+
164
+ // Timeout after 5 minutes
165
+ setTimeout(() => {
166
+ log.warn("Authentication timed out after 5 minutes");
167
+ proc.kill();
168
+ resolveOnce({
169
+ type: "failed",
170
+ error: "Authentication timed out. Please try again.",
171
+ });
172
+ }, AUTH_POLL_TIMEOUT);
173
+ });
174
+ },
175
+ });
176
+ }
177
+ };
178
+
179
+ // Poll for URL extraction with timeout
180
+ const urlPollStart = Date.now();
181
+ const pollForUrl = () => {
182
+ if (urlExtracted) return;
183
+
184
+ const elapsed = Date.now() - urlPollStart;
185
+ if (elapsed >= URL_EXTRACTION_TIMEOUT) {
186
+ proc.kill();
187
+ const errorMsg = stderr ? stripAnsi(stderr) : "No login URL received within timeout";
188
+ log.error("Failed to extract login URL", { error: errorMsg, elapsed: `${elapsed}ms` });
189
+ reject(new Error(`Failed to get login URL: ${errorMsg}`));
190
+ return;
191
+ }
192
+
193
+ tryExtractUrl();
194
+
195
+ if (!urlExtracted) {
196
+ setTimeout(pollForUrl, 100); // Check every 100ms
197
+ }
198
+ };
199
+
200
+ // Start polling for URL
201
+ pollForUrl();
202
+ });
203
+ }
204
+
205
+ export function verifyCursorAuth(): boolean {
206
+ const possiblePaths = getPossibleAuthPaths();
207
+
208
+ for (const authPath of possiblePaths) {
209
+ if (existsSync(authPath)) {
210
+ log.debug("Auth file found", { path: authPath });
211
+ return true;
212
+ }
213
+ }
214
+
215
+ log.debug("No auth file found", { checkedPaths: possiblePaths });
216
+ return false;
217
+ }
218
+
219
+ /**
220
+ * Returns all possible auth file paths in priority order.
221
+ * Checks both auth.json (legacy) and cli-config.json (current cursor-agent format).
222
+ * - macOS: ~/.cursor/ (primary), ~/.config/cursor/ (fallback)
223
+ * - Linux: ~/.config/cursor/ (XDG), XDG_CONFIG_HOME/cursor/, ~/.cursor/
224
+ */
225
+ export function getPossibleAuthPaths(): string[] {
226
+ const home = getHomeDir();
227
+ const paths: string[] = [];
228
+ const isDarwin = platform() === "darwin";
229
+
230
+ const authFiles = ["cli-config.json", "auth.json"];
231
+
232
+ if (isDarwin) {
233
+ for (const file of authFiles) {
234
+ paths.push(join(home, ".cursor", file));
235
+ }
236
+ for (const file of authFiles) {
237
+ paths.push(join(home, ".config", "cursor", file));
238
+ }
239
+ } else {
240
+ for (const file of authFiles) {
241
+ paths.push(join(home, ".config", "cursor", file));
242
+ }
243
+
244
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
245
+ if (xdgConfig && xdgConfig !== join(home, ".config")) {
246
+ for (const file of authFiles) {
247
+ paths.push(join(xdgConfig, "cursor", file));
248
+ }
249
+ }
250
+
251
+ for (const file of authFiles) {
252
+ paths.push(join(home, ".cursor", file));
253
+ }
254
+ }
255
+
256
+ return paths;
257
+ }
258
+
259
+ export function getAuthFilePath(): string {
260
+ const possiblePaths = getPossibleAuthPaths();
261
+
262
+ for (const authPath of possiblePaths) {
263
+ if (existsSync(authPath)) {
264
+ return authPath;
265
+ }
266
+ }
267
+
268
+ return possiblePaths[0];
269
+ }