-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.ts
More file actions
506 lines (450 loc) · 13 KB
/
cli.ts
File metadata and controls
506 lines (450 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
#!/usr/bin/env node
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import * as readline from "readline";
import { fileURLToPath } from "url";
import {
loadConfig,
saveConfig,
clearConfig,
maskApiKey,
setConfigValue,
clearState,
CONFIG_FILE,
} from "./config.js";
import { testConnection, createTestSession, getStats } from "./api.js";
import { handleHook } from "./hooks.js";
// Read version from package.json
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJsonPath = path.join(__dirname, "..", "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
const VERSION = packageJson.version;
const CURSOR_HOOKS_DIR = path.join(os.homedir(), ".cursor");
const CURSOR_HOOKS_FILE = path.join(CURSOR_HOOKS_DIR, "hooks.json");
/**
* Print styled output
*/
function print(message: string): void {
console.log(message);
}
function printSuccess(message: string): void {
console.log(`✓ ${message}`);
}
function printError(message: string): void {
console.error(`✗ ${message}`);
}
function printInfo(message: string): void {
console.log(` ${message}`);
}
/**
* Prompt for user input
*/
async function prompt(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
/**
* Login command - configure Convex URL and API key
*/
async function login(): Promise<void> {
print("\n cursor-sync Login\n");
const convexUrl = await prompt("Convex URL (e.g., https://your-project.convex.cloud): ");
if (!convexUrl) {
printError("Convex URL is required");
process.exit(1);
}
const apiKey = await prompt("API Key (starts with osk_): ");
if (!apiKey) {
printError("API Key is required");
process.exit(1);
}
if (!apiKey.startsWith("osk_")) {
printError("API Key should start with 'osk_'");
process.exit(1);
}
saveConfig({
convexUrl,
apiKey,
autoSync: true,
syncToolCalls: true,
syncThinking: false,
debug: false,
});
printSuccess("Credentials saved!");
print("");
printInfo(`Convex URL: ${convexUrl}`);
printInfo(`API Key: ${maskApiKey(apiKey)}`);
print("");
print("Next steps:");
print(" 1. Run 'cursor-sync setup' to configure Cursor hooks");
print(" 2. Run 'cursor-sync verify' to check your setup");
print("");
}
/**
* Setup command - configure Cursor hooks
*/
async function setup(): Promise<void> {
print("\n cursor-sync Setup\n");
// Check if config exists
const config = loadConfig();
if (!config) {
printError("Not authenticated. Run 'cursor-sync login' first.");
process.exit(1);
}
// Create hooks directory if needed
if (!fs.existsSync(CURSOR_HOOKS_DIR)) {
fs.mkdirSync(CURSOR_HOOKS_DIR, { recursive: true });
}
// Build hooks.json
const hooksConfig = {
version: 1,
hooks: {
beforeSubmitPrompt: [
{ command: "cursor-sync hook beforeSubmitPrompt" },
],
beforeShellExecution: [
{ command: "cursor-sync hook beforeShellExecution" },
],
beforeMCPExecution: [
{ command: "cursor-sync hook beforeMCPExecution" },
],
afterFileEdit: [
{ command: "cursor-sync hook afterFileEdit" },
],
afterAgentResponse: [
{ command: "cursor-sync hook afterAgentResponse" },
],
stop: [
{ command: "cursor-sync hook stop" },
],
},
};
// Check for existing hooks.json
if (fs.existsSync(CURSOR_HOOKS_FILE)) {
print("Existing hooks.json found at ~/.cursor/hooks.json");
const overwrite = await prompt("Overwrite? (y/n): ");
if (overwrite.toLowerCase() !== "y") {
print("");
print("To add cursor-sync manually, add these hooks to your hooks.json:");
print(JSON.stringify(hooksConfig.hooks, null, 2));
return;
}
}
fs.writeFileSync(CURSOR_HOOKS_FILE, JSON.stringify(hooksConfig, null, 2));
printSuccess("Hooks configured!");
print("");
printInfo(`Config file: ${CURSOR_HOOKS_FILE}`);
print("");
print("Hooks registered:");
Object.keys(hooksConfig.hooks).forEach((hook) => {
printInfo(`• ${hook}`);
});
print("");
print("Restart Cursor to activate the hooks.");
print("");
}
/**
* Verify command - check credentials and Cursor config
*/
async function verify(): Promise<void> {
print("\n OpenSync Setup Verification\n");
// Check credentials
const config = loadConfig();
if (!config) {
printError("Credentials: NOT CONFIGURED");
printInfo("Run 'cursor-sync login' to set up credentials");
process.exit(1);
}
printSuccess("Credentials: OK");
printInfo(`Convex URL: ${config.convexUrl}`);
printInfo(`API Key: ${maskApiKey(config.apiKey)}`);
print("");
// Test connection
const connectionResult = await testConnection();
if (connectionResult.success) {
printSuccess("Connection: OK");
} else {
printError(`Connection: FAILED - ${connectionResult.message}`);
}
print("");
// Check Cursor hooks config
if (fs.existsSync(CURSOR_HOOKS_FILE)) {
try {
const hooksContent = fs.readFileSync(CURSOR_HOOKS_FILE, "utf-8");
const hooks = JSON.parse(hooksContent);
const hasCursorSync = Object.values(hooks.hooks || {}).some((hookArray) =>
(hookArray as Array<{ command: string }>).some(
(h) => h.command && h.command.includes("cursor-sync")
)
);
if (hasCursorSync) {
printSuccess("Cursor Hooks: OK");
printInfo(`Config file: ${CURSOR_HOOKS_FILE}`);
printInfo("Hooks registered: cursor-sync");
} else {
printError("Cursor Hooks: cursor-sync not found in hooks.json");
printInfo("Run 'cursor-sync setup' to configure hooks");
}
} catch {
printError("Cursor Hooks: Invalid hooks.json");
}
} else {
printError("Cursor Hooks: NOT CONFIGURED");
printInfo("Run 'cursor-sync setup' to configure hooks");
}
print("");
if (config && connectionResult.success && fs.existsSync(CURSOR_HOOKS_FILE)) {
printSuccess("Ready! Start Cursor and sessions will sync automatically.");
}
print("");
}
/**
* Sync test command - test full sync flow
*/
async function syncTest(): Promise<void> {
print("\n cursor-sync Test\n");
const config = loadConfig();
if (!config) {
printError("Not authenticated. Run 'cursor-sync login' first.");
process.exit(1);
}
print("Testing connection...");
const connectionResult = await testConnection();
if (!connectionResult.success) {
printError(`Connection failed: ${connectionResult.message}`);
process.exit(1);
}
printSuccess("Connection OK");
print("Creating test session...");
const testResult = await createTestSession();
if (!testResult.success) {
printError(`Test session failed: ${testResult.error}`);
process.exit(1);
}
printSuccess(`Test session created: ${testResult.sessionId}`);
print("");
printSuccess("Sync test passed!");
print("Check your OpenSync dashboard to see the test session.");
print("");
}
/**
* Logout command - clear credentials
*/
function logout(): void {
clearConfig();
clearState();
printSuccess("Logged out. Credentials cleared.");
}
/**
* Status command - show current status
*/
async function status(): Promise<void> {
print("\n cursor-sync Status\n");
const config = loadConfig();
if (!config) {
printInfo("Status: Not authenticated");
printInfo("Run 'cursor-sync login' to set up credentials");
return;
}
printInfo("Status: Authenticated");
printInfo(`Convex URL: ${config.convexUrl}`);
printInfo(`API Key: ${maskApiKey(config.apiKey)}`);
print("");
const connectionResult = await testConnection();
printInfo(`Connection: ${connectionResult.success ? "OK" : "Failed"}`);
if (connectionResult.success) {
const statsResult = await getStats();
if (statsResult.success && statsResult.stats) {
print("");
printInfo(`Total sessions: ${statsResult.stats.totalSessions}`);
printInfo(`Total tokens: ${statsResult.stats.totalTokens.toLocaleString()}`);
printInfo(`Total cost: $${statsResult.stats.totalCost.toFixed(4)}`);
}
}
print("");
}
/**
* Config command - show current configuration
*/
function showConfig(asJson: boolean): void {
const config = loadConfig();
if (!config) {
if (asJson) {
console.log(JSON.stringify({ error: "Not configured" }));
} else {
printInfo("Not configured. Run 'cursor-sync login' first.");
}
return;
}
if (asJson) {
console.log(
JSON.stringify(
{
...config,
apiKey: maskApiKey(config.apiKey),
},
null,
2
)
);
} else {
print("\n cursor-sync Configuration\n");
printInfo(`Config file: ${CONFIG_FILE}`);
print("");
printInfo(`convexUrl: ${config.convexUrl}`);
printInfo(`apiKey: ${maskApiKey(config.apiKey)}`);
printInfo(`autoSync: ${config.autoSync}`);
printInfo(`syncToolCalls: ${config.syncToolCalls}`);
printInfo(`syncThinking: ${config.syncThinking}`);
printInfo(`debug: ${config.debug}`);
print("");
}
}
/**
* Set command - update a config value
*/
function setValue(key: string, value: string): void {
const config = loadConfig();
if (!config) {
printError("Not configured. Run 'cursor-sync login' first.");
process.exit(1);
}
const booleanKeys = ["autoSync", "syncToolCalls", "syncThinking", "debug"];
if (booleanKeys.includes(key)) {
const boolValue = value === "true" || value === "1";
setConfigValue(key as keyof typeof config, boolValue);
printSuccess(`Set ${key} = ${boolValue}`);
} else if (key === "convexUrl" || key === "apiKey") {
setConfigValue(key, value);
printSuccess(`Set ${key} = ${key === "apiKey" ? maskApiKey(value) : value}`);
} else {
printError(`Unknown config key: ${key}`);
print("Valid keys: convexUrl, apiKey, autoSync, syncToolCalls, syncThinking, debug");
process.exit(1);
}
}
/**
* Hook command - handle Cursor hook events
*/
async function hook(eventName: string): Promise<void> {
await handleHook(eventName);
}
/**
* Help command
*/
function showHelp(): void {
print(`
cursor-sync v${VERSION}
Sync your Cursor sessions to OpenSync dashboard
Usage:
cursor-sync <command> [options]
Commands:
login Configure Convex URL and API Key
setup Add hooks to Cursor settings
verify Verify credentials and Cursor config
synctest Test connectivity and create a test session
logout Clear stored credentials
status Show connection status
config Show current configuration
config --json Show config as JSON
set <key> <value> Update a config value
hook <event> Handle Cursor hook events (internal)
version Show version number
help Show this help message
Configuration Options:
autoSync Automatically sync sessions (default: true)
syncToolCalls Include tool call details (default: true)
syncThinking Include thinking traces (default: false)
debug Enable debug logging (default: false)
Environment Variables:
CURSOR_SYNC_CONVEX_URL Override Convex URL
CURSOR_SYNC_API_KEY Override API Key
CURSOR_SYNC_AUTO_SYNC Override autoSync
CURSOR_SYNC_TOOL_CALLS Override syncToolCalls
CURSOR_SYNC_THINKING Override syncThinking
CURSOR_SYNC_DEBUG Override debug
Examples:
cursor-sync login
cursor-sync setup
cursor-sync verify
cursor-sync set syncThinking true
cursor-sync status
Links:
OpenSync: https://opensync.dev
GitHub: https://github.com/waynesutton/cursor-cli-sync-plugin
npm: https://www.npmjs.com/package/cursor-opensync-plugin
`);
}
/**
* Main entry point
*/
async function main(): Promise<void> {
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case "login":
await login();
break;
case "setup":
await setup();
break;
case "verify":
await verify();
break;
case "synctest":
await syncTest();
break;
case "logout":
logout();
break;
case "status":
await status();
break;
case "config":
showConfig(args.includes("--json"));
break;
case "set":
if (args.length < 3) {
printError("Usage: cursor-sync set <key> <value>");
process.exit(1);
}
setValue(args[1], args[2]);
break;
case "hook":
if (args.length < 2) {
printError("Usage: cursor-sync hook <event>");
process.exit(1);
}
await hook(args[1]);
break;
case "version":
case "--version":
case "-v":
print(`cursor-sync v${VERSION}`);
break;
case "help":
case "--help":
case "-h":
case undefined:
showHelp();
break;
default:
printError(`Unknown command: ${command}`);
print("Run 'cursor-sync help' for usage information.");
process.exit(1);
}
}
main().catch((error) => {
printError(error.message);
process.exit(1);
});