@aloth/olcli 0.4.1 → 0.6.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.
package/README.md CHANGED
@@ -27,8 +27,10 @@ Work with Overleaf projects directly from your command line. Edit locally with y
27
27
  - 📄 **Compile** PDFs using Overleaf's remote compiler
28
28
  - 📦 **Download** individual files or full project archives
29
29
  - 📤 **Upload** files to projects
30
- - 💬 **Review comments** — list comments with source locations, add, resolve, reopen, and delete threads
30
+ - 💬 **Review comments** — list comments with source locations, add, resolve, reopen, delete threads, and **reply** to existing threads
31
31
  - 🗂️ **Preserve folder structure** when pushing nested files
32
+ - ⏱️ **Configurable timeout** — global `--timeout <ms>` flag, `OVERLEAF_TIMEOUT` env var, or `config set-timeout` for slow connections
33
+ - 🔑 **Password login** — `olcli auth --email --password` for self-hosted instances (no browser required)
32
34
  - ⚙️ **Support self-hosted Overleaf/ShareLaTeX instances** via configurable base URL and session cookie name
33
35
  - 📊 **Output** compile artifacts (`.bbl`, `.log`, `.aux` for arXiv submissions)
34
36
 
@@ -94,18 +96,24 @@ makepkg -si
94
96
 
95
97
  ### 1. Authenticate with Overleaf
96
98
 
97
- Get your session cookie from Overleaf.com:
99
+ **Option A: Session cookie** (overleaf.com and self-hosted)
98
100
 
99
101
  1. Log into [overleaf.com](https://www.overleaf.com)
100
102
  2. Open Developer Tools (F12 or Cmd+Option+I) → Application/Storage → Cookies
101
103
  3. Copy the value of `overleaf_session2`
102
104
 
103
- Store it with olcli:
104
-
105
105
  ```bash
106
106
  olcli auth --cookie "your_session_cookie_value"
107
107
  ```
108
108
 
109
+ **Option B: Email/password** (self-hosted instances without reCAPTCHA)
110
+
111
+ ```bash
112
+ olcli auth --email "you@example.com" --password "your_password"
113
+ ```
114
+
115
+ Credentials are stored securely and the session auto-refreshes on expiry.
116
+
109
117
  **Tip:** The cookie stays valid for weeks. Just refresh it when authentication fails.
110
118
 
111
119
  ### 2. List Your Projects
@@ -158,7 +166,7 @@ All commands auto-detect the project when run from a synced directory (contains
158
166
 
159
167
  | Command | Description |
160
168
  |---------|-------------|
161
- | `olcli auth` | Set session cookie |
169
+ | `olcli auth` | Set session cookie or login with email/password |
162
170
  | `olcli whoami` | Check authentication status |
163
171
  | `olcli logout` | Clear stored credentials |
164
172
  | `olcli list` | List all projects |
@@ -171,6 +179,7 @@ All commands auto-detect the project when run from a synced directory (contains
171
179
  | `olcli comments list [project]` | List comments with source text and file locations (`--status`, `--context`) |
172
180
  | `olcli comments add <file> <message> [project]` | Add a comment to selected text |
173
181
  | `olcli comments resolve <threadId> [project]` | Resolve a comment thread |
182
+ | `olcli comments reply <threadId> <body> [project]` | Reply to a comment thread |
174
183
  | `olcli comments reopen <threadId> [project]` | Reopen a resolved comment thread |
175
184
  | `olcli comments delete <threadId> [project]` | Permanently delete a comment thread |
176
185
  | `olcli delete <file> [project]` | Delete a remote file or folder by path (alias: `rm`) |
@@ -182,6 +191,8 @@ All commands auto-detect the project when run from a synced directory (contains
182
191
  | `olcli output [type]` | Download compile output files |
183
192
  | `olcli config set-url <url>` | Set a self-hosted Overleaf base URL |
184
193
  | `olcli config set-cookie-name <name>` | Set the session cookie name |
194
+ | `olcli config set-timeout <ms>` | Set default HTTP request timeout |
195
+ | `olcli config get-timeout` | Show current timeout setting |
185
196
  | `olcli check` | Show config paths and credential sources |
186
197
 
187
198
  ### Review comments
@@ -191,6 +202,7 @@ olcli comments list "My Paper" --status open --context 2
191
202
  olcli comments list "My Paper" --status resolved --json
192
203
  olcli comments add main.tex "Please clarify this definition" "My Paper" --text "A Skill is"
193
204
  olcli comments add main.tex "Check this sentence" "My Paper" --line 42 --column 1 --length 20 --json
205
+ olcli comments reply 6a1a5fedbf90b811e1000001 "Good point, I'll fix this" "My Paper"
194
206
  olcli comments resolve 6a1a5fedbf90b811e1000001 "My Paper" --json
195
207
  olcli comments reopen 6a1a5fedbf90b811e1000001 "My Paper"
196
208
  olcli comments delete 6a1a5fedbf90b811e1000001 "My Paper" --json
@@ -205,6 +217,7 @@ These flags work with **every** command and may be placed before or after the co
205
217
  | `--verbose` | Print every HTTP request, status, content-type, and (on errors) a response-body snippet to stderr. Useful for debugging failed compiles, 404s on `pdf`/`output`, auth issues, or unexpected upload behavior. |
206
218
  | `--base-url <url>` | Override the Overleaf instance base URL (also `OVERLEAF_BASE_URL` env var or `olcli config set-url`). |
207
219
  | `--cookie-name <name>` | Override the session cookie name (default `overleaf_session2`; older instances use `overleaf.sid`). |
220
+ | `--timeout <ms>` | Override the HTTP request timeout in milliseconds (also `OVERLEAF_TIMEOUT` env var or `olcli config set-timeout`). Default: 10000. |
208
221
 
209
222
  Examples:
210
223
 
@@ -388,6 +401,24 @@ olcli config set-url https://latex.example.org
388
401
  olcli config set-cookie-name overleaf.sid
389
402
  ```
390
403
 
404
+ ### Timeout configuration
405
+
406
+ For slow connections or large projects, increase the HTTP request timeout:
407
+
408
+ ```bash
409
+ # One-off: pass as a flag
410
+ olcli --timeout 60000 pull "Large Thesis"
411
+
412
+ # Persist as default
413
+ olcli config set-timeout 60000
414
+
415
+ # Or use environment variable
416
+ export OVERLEAF_TIMEOUT=60000
417
+ olcli pull "Large Thesis"
418
+ ```
419
+
420
+ Precedence: `--timeout` flag > `OVERLEAF_TIMEOUT` env > `config set-timeout` > default (10000ms).
421
+
391
422
  ## Examples
392
423
 
393
424
  ### Work on a thesis
@@ -438,6 +469,198 @@ olcli output bbl -o main.bbl
438
469
  olcli zip -o arxiv-submission.zip
439
470
  ```
440
471
 
472
+ ## Programmatic Usage (Library API)
473
+
474
+ `@aloth/olcli` exposes `OverleafClient` and all public interfaces as a proper library so you can use it in your own scripts, tools, and AI agents.
475
+
476
+ ### Install
477
+
478
+ ```bash
479
+ npm install @aloth/olcli
480
+ ```
481
+
482
+ ### Basic example
483
+
484
+ ```ts
485
+ import { OverleafClient } from '@aloth/olcli';
486
+
487
+ // Create a client from an Overleaf session cookie
488
+ const client = await OverleafClient.fromSessionCookie(cookie);
489
+
490
+ // List all projects
491
+ const projects = await client.listProjects();
492
+ console.log(projects);
493
+
494
+ // Get detailed info (file tree) for a project
495
+ const info = await client.getProjectInfo(projectId);
496
+
497
+ // Download project as a zip buffer
498
+ const zipBuf = await client.downloadProject(projectId);
499
+
500
+ // Compile and download PDF
501
+ const pdfBuf = await client.downloadPdf(projectId);
502
+
503
+ // Upload a file
504
+ import { readFileSync } from 'node:fs';
505
+ await client.uploadFile(projectId, null, 'main.tex', readFileSync('main.tex'));
506
+
507
+ // List review comments
508
+ const comments = await client.listComments(projectId, { status: 'open' });
509
+ ```
510
+
511
+ ### Available exports
512
+
513
+ ```ts
514
+ import {
515
+ // Core client
516
+ OverleafClient,
517
+
518
+ // Types / interfaces
519
+ Project, ProjectInfo, FolderEntry, DocEntry, FileEntry,
520
+ CommentMessage, ProjectComment, CommentContext, CommentStatus,
521
+ ListCommentsOptions, AddCommentOptions, Credentials, SessionCookiePair,
522
+
523
+ // Configuration utilities
524
+ getBaseUrl, setBaseUrl, getSessionCookie, setSessionCookie,
525
+ getSessionCookieName, setSessionCookieName, getCsrf, setCsrf,
526
+ getLastProject, setLastProject, clearConfig, getConfigPath, saveOlAuth,
527
+ getTimeout, setTimeout, getPasswordCredentials, setPasswordCredentials,
528
+ clearPasswordCredentials, type PasswordCredentials,
529
+
530
+ // Ignore utilities
531
+ DEFAULT_IGNORE_PATTERNS, loadIgnore, shouldIgnore, buildTexSiblingSet,
532
+ IgnoreContext, LoadIgnoreOptions,
533
+ } from '@aloth/olcli';
534
+ ```
535
+
536
+ ---
537
+
538
+ ## MCP Server
539
+
540
+ `@aloth/olcli` ships an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server so AI assistants like **Claude Desktop**, **Cursor**, and **Windsurf** can interact with your Overleaf projects directly.
541
+
542
+ ### MCP tools
543
+
544
+ | Tool | Description |
545
+ |------|-------------|
546
+ | `list_projects` | List all Overleaf projects |
547
+ | `get_project_info` | Get file tree and metadata for a project |
548
+ | `pull_project` | Download and extract a project to a local directory |
549
+ | `push_file` | Upload a local file to a project |
550
+ | `compile` | Compile a project and get the PDF URL |
551
+ | `download_pdf` | Compile a project and save the PDF locally |
552
+ | `list_comments` | List review comments (filter: all / open / resolved) |
553
+ | `get_entities` | Get a flat list of all files in a project |
554
+ | `download_file` | Download a specific file by its remote path |
555
+ | `add_comment` | Add a review comment to a document |
556
+ | `reply_to_comment` | Reply to an existing comment thread |
557
+ | `resolve_comment` | Mark a comment thread as resolved |
558
+ | `delete_entity` | Delete a file or document by path |
559
+ | `rename_entity` | Rename a file or document |
560
+ | `compile_with_outputs` | Compile and return all output files (PDF, BBL, logs…) |
561
+
562
+ ### Authentication
563
+
564
+ The MCP server reads credentials in this order:
565
+
566
+ 1. **`OVERLEAF_SESSION` environment variable** — set in your MCP config (recommended)
567
+ 2. **`OVERLEAF_EMAIL` + `OVERLEAF_PASSWORD` environment variables** — for password login (self-hosted)
568
+ 3. **`.olauth` file in cwd** — written by `olcli auth`
569
+ 4. **Stored config** — written by `olcli auth` (including saved password credentials)
570
+
571
+ When a session cookie expires and password credentials are available, the MCP server automatically re-authenticates.
572
+
573
+ ### Claude Desktop
574
+
575
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
576
+
577
+ ```json
578
+ {
579
+ "mcpServers": {
580
+ "overleaf": {
581
+ "command": "npx",
582
+ "args": ["-y", "@aloth/olcli-mcp"],
583
+ "env": {
584
+ "OVERLEAF_SESSION": "<your-overleaf-session-cookie>"
585
+ }
586
+ }
587
+ }
588
+ }
589
+ ```
590
+
591
+ Or if you have olcli installed globally (`npm install -g @aloth/olcli`):
592
+
593
+ ```json
594
+ {
595
+ "mcpServers": {
596
+ "overleaf": {
597
+ "command": "olcli-mcp",
598
+ "env": {
599
+ "OVERLEAF_SESSION": "<your-overleaf-session-cookie>"
600
+ }
601
+ }
602
+ }
603
+ }
604
+ ```
605
+
606
+ ### Cursor
607
+
608
+ Add to your Cursor MCP settings (`~/.cursor/mcp.json` or project `.cursor/mcp.json`):
609
+
610
+ ```json
611
+ {
612
+ "mcpServers": {
613
+ "overleaf": {
614
+ "command": "npx",
615
+ "args": ["-y", "@aloth/olcli-mcp"],
616
+ "env": {
617
+ "OVERLEAF_SESSION": "<your-overleaf-session-cookie>"
618
+ }
619
+ }
620
+ }
621
+ }
622
+ ```
623
+
624
+ ### Windsurf
625
+
626
+ Add to `~/.codeium/windsurf/mcp_config.json`:
627
+
628
+ ```json
629
+ {
630
+ "mcpServers": {
631
+ "overleaf": {
632
+ "command": "npx",
633
+ "args": ["-y", "@aloth/olcli-mcp"],
634
+ "env": {
635
+ "OVERLEAF_SESSION": "<your-overleaf-session-cookie>"
636
+ }
637
+ }
638
+ }
639
+ }
640
+ ```
641
+
642
+ ### Getting your session cookie
643
+
644
+ 1. Open Overleaf in your browser and log in
645
+ 2. Open DevTools → Application (Chrome) or Storage (Firefox) → Cookies
646
+ 3. Find `overleaf_session2` (or `sharelatex.sid` for self-hosted)
647
+ 4. Copy the value — that's your `OVERLEAF_SESSION`
648
+
649
+ Or run `olcli auth` and then the MCP server will pick it up automatically.
650
+
651
+ ### Self-hosted Overleaf
652
+
653
+ Set `OVERLEAF_BASE_URL` in your MCP env:
654
+
655
+ ```json
656
+ "env": {
657
+ "OVERLEAF_SESSION": "<cookie>",
658
+ "OVERLEAF_BASE_URL": "https://overleaf.yourcompany.com"
659
+ }
660
+ ```
661
+
662
+ ---
663
+
441
664
  ## Troubleshooting
442
665
 
443
666
  ### Session expired
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import { loadIgnore, shouldIgnore, buildTexSiblingSet, } from './ignore.js';
17
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
18
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
19
19
  const VERSION = pkg.version;
20
- import { getSessionCookie, setSessionCookie, setLastProject, getConfigPath, saveOlAuth, clearConfig, getBaseUrl, setBaseUrl, getSessionCookieName, setSessionCookieName } from './config.js';
20
+ import { getSessionCookie, setSessionCookie, setLastProject, getConfigPath, saveOlAuth, clearConfig, getBaseUrl, setBaseUrl, getSessionCookieName, setSessionCookieName, getTimeout, setTimeout, getPasswordCredentials, setPasswordCredentials } from './config.js';
21
21
  const program = new Command();
22
22
  program
23
23
  .name('olcli')
@@ -25,26 +25,57 @@ program
25
25
  .version(VERSION)
26
26
  .option('--base-url <url>', 'Overleaf instance base URL (overrides OVERLEAF_BASE_URL and config)')
27
27
  .option('--cookie-name <name>', 'Session cookie name (default: overleaf_session2, use overleaf.sid for older instances)')
28
+ .option('--timeout <ms>', 'HTTP request timeout in milliseconds', parseInt)
28
29
  .option('--verbose', 'Print every HTTP request, status, and error response body to stderr');
29
30
  /**
30
31
  * Helper to get authenticated client
31
32
  */
32
33
  async function getClient(cookieOpt, baseUrlOpt) {
33
- const cookie = cookieOpt || getSessionCookie();
34
- if (!cookie) {
35
- console.error(chalk.red('No session cookie found.'));
36
- console.error('Set one with: olcli auth --cookie <session_cookie>');
37
- console.error('Or set OVERLEAF_SESSION environment variable');
38
- console.error('Or create .olauth file in current directory');
39
- process.exit(1);
40
- }
41
34
  const baseUrl = baseUrlOpt || program.opts().baseUrl || getBaseUrl();
42
35
  const cookieName = program.opts().cookieName || getSessionCookieName();
43
- const client = await OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName);
36
+ const cookie = cookieOpt || getSessionCookie();
37
+ const passwordCredentials = cookieOpt ? undefined : getPasswordCredentials();
38
+ if (cookie) {
39
+ try {
40
+ const client = await OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName);
41
+ if (program.opts().verbose)
42
+ client.setVerbose(true);
43
+ const timeout = program.opts().timeout || getTimeout();
44
+ client.setGlobalTimeout(timeout);
45
+ return client;
46
+ }
47
+ catch (error) {
48
+ if (!passwordCredentials)
49
+ throw error;
50
+ }
51
+ }
52
+ if (passwordCredentials) {
53
+ return loginWithSavedPassword(passwordCredentials, baseUrl, cookieName);
54
+ }
55
+ console.error(chalk.red('No session cookie or password credentials found.'));
56
+ console.error('Set one with: olcli auth --cookie <session_cookie>');
57
+ console.error('Or use: olcli auth --email <email> --password <password>');
58
+ console.error('Or set OVERLEAF_SESSION environment variable');
59
+ console.error('Or create .olauth file in current directory');
60
+ process.exit(1);
61
+ }
62
+ async function loginWithSavedPassword(credentials, baseUrl, cookieName) {
63
+ const client = await OverleafClient.fromPasswordLogin(credentials.email, credentials.password, baseUrl);
64
+ persistClientSession(client, cookieName);
44
65
  if (program.opts().verbose)
45
66
  client.setVerbose(true);
67
+ const timeout = program.opts().timeout || getTimeout();
68
+ client.setGlobalTimeout(timeout);
46
69
  return client;
47
70
  }
71
+ function persistClientSession(client, preferredCookieName) {
72
+ const sessionCookie = client.getSessionCookiePair(preferredCookieName);
73
+ if (!sessionCookie) {
74
+ throw new Error('Password login succeeded, but no session cookie was returned.');
75
+ }
76
+ setSessionCookieName(sessionCookie.name);
77
+ setSessionCookie(sessionCookie.value);
78
+ }
48
79
  async function resolveProject(client, projectArg, dir = '.') {
49
80
  // If project argument provided, use it
50
81
  if (projectArg) {
@@ -76,12 +107,15 @@ async function resolveProject(client, projectArg, dir = '.') {
76
107
  // ─────────────────────────────────────────────────────────────────────────────
77
108
  program
78
109
  .command('auth')
79
- .description('Authenticate with Overleaf using session cookie')
110
+ .description('Authenticate with Overleaf using a session cookie or email/password')
80
111
  .option('--cookie <session>', 'Session cookie (overleaf_session2 value)')
112
+ .option('--email <email>', 'Account email for password login')
113
+ .option('--password <password>', 'Account password for password login')
114
+ .option('--no-save-password', 'Do not persist email/password credentials')
81
115
  .option('--save-local', 'Save to .olauth in current directory')
82
116
  .action(async (options) => {
83
- if (!options.cookie) {
84
- console.log(chalk.yellow('To authenticate, provide your session cookie:'));
117
+ if (!options.cookie && !options.email && !options.password) {
118
+ console.log(chalk.yellow('To authenticate, provide a session cookie:'));
85
119
  console.log();
86
120
  console.log('1. Log into overleaf.com in your browser');
87
121
  console.log('2. Open Developer Tools (F12) → Application → Cookies');
@@ -90,22 +124,46 @@ program
90
124
  console.log();
91
125
  console.log(chalk.cyan(' olcli auth --cookie "your_session_cookie_value"'));
92
126
  console.log();
127
+ console.log('Or log in with email/password:');
128
+ console.log(chalk.cyan(' olcli auth --email "you@example.com" --password "your_password"'));
129
+ console.log();
93
130
  console.log('Or set OVERLEAF_SESSION environment variable');
94
131
  return;
95
132
  }
133
+ if (options.cookie && (options.email || options.password)) {
134
+ console.error(chalk.red('Use either --cookie or --email/--password, not both.'));
135
+ process.exit(1);
136
+ }
137
+ if (!options.cookie && (!options.email || !options.password)) {
138
+ console.error(chalk.red('Both --email and --password are required for password login.'));
139
+ process.exit(1);
140
+ }
96
141
  const spinner = ora('Verifying session...').start();
97
142
  try {
98
143
  const baseUrl = program.opts().baseUrl || getBaseUrl();
99
144
  const cookieName = program.opts().cookieName || getSessionCookieName();
100
- const client = await OverleafClient.fromSessionCookie(options.cookie, baseUrl, cookieName);
101
- const projects = await client.listProjects();
102
- setSessionCookie(options.cookie);
103
- if (options.saveLocal) {
104
- saveOlAuth(options.cookie);
105
- spinner.succeed(`Authenticated! Found ${projects.length} projects. Saved to .olauth`);
145
+ if (options.cookie) {
146
+ const client = await OverleafClient.fromSessionCookie(options.cookie, baseUrl, cookieName);
147
+ const projects = await client.listProjects();
148
+ setSessionCookie(options.cookie);
149
+ if (options.saveLocal) {
150
+ saveOlAuth(options.cookie);
151
+ spinner.succeed(`Authenticated! Found ${projects.length} projects. Saved to .olauth`);
152
+ }
153
+ else {
154
+ spinner.succeed(`Authenticated! Found ${projects.length} projects.`);
155
+ }
106
156
  }
107
157
  else {
108
- spinner.succeed(`Authenticated! Found ${projects.length} projects.`);
158
+ spinner.text = 'Logging in with email/password...';
159
+ const client = await OverleafClient.fromPasswordLogin(options.email, options.password, baseUrl);
160
+ const projects = await client.listProjects();
161
+ persistClientSession(client, cookieName);
162
+ setBaseUrl(baseUrl);
163
+ if (options.savePassword !== false) {
164
+ setPasswordCredentials(options.email, options.password);
165
+ }
166
+ spinner.succeed(`Authenticated! Found ${projects.length} projects. Password login saved.`);
109
167
  }
110
168
  console.log(chalk.dim(`Config saved to: ${getConfigPath()}`));
111
169
  }
@@ -303,6 +361,30 @@ commentsCmd
303
361
  process.exit(1);
304
362
  }
305
363
  });
364
+ commentsCmd
365
+ .command('reply <threadId> <body> [project]')
366
+ .description('Reply to a comment thread with a message')
367
+ .option('--json', 'Output as JSON')
368
+ .option('--cookie <session>', 'Session cookie override')
369
+ .action(async (threadId, body, project, options) => {
370
+ const spinner = ora('Posting reply...').start();
371
+ try {
372
+ const client = await getClient(options.cookie);
373
+ const proj = await resolveProject(client, project);
374
+ const message = await client.postCommentMessage(proj.id, threadId, body);
375
+ if (options.json) {
376
+ spinner.stop();
377
+ console.log(JSON.stringify({ replied: true, message }, null, 2));
378
+ return;
379
+ }
380
+ spinner.succeed(`Replied to ${threadId}`);
381
+ setLastProject(proj.id);
382
+ }
383
+ catch (error) {
384
+ spinner.fail(`Failed: ${error.message}`);
385
+ process.exit(1);
386
+ }
387
+ });
306
388
  commentsCmd
307
389
  .command('resolve <threadId> [project]')
308
390
  .description('Resolve a comment thread')
@@ -1261,6 +1343,24 @@ configCmd
1261
1343
  .action(() => {
1262
1344
  console.log(getSessionCookieName());
1263
1345
  });
1346
+ configCmd
1347
+ .command('set-timeout <ms>')
1348
+ .description('Set the default HTTP request timeout in milliseconds')
1349
+ .action((ms) => {
1350
+ const timeout = parseInt(ms, 10);
1351
+ if (isNaN(timeout)) {
1352
+ console.error(chalk.red('Invalid timeout value. Must be a number.'));
1353
+ process.exit(1);
1354
+ }
1355
+ setTimeout(timeout);
1356
+ console.log(chalk.green(`Default timeout set to: ${timeout}ms`));
1357
+ });
1358
+ configCmd
1359
+ .command('get-timeout')
1360
+ .description('Get the current default HTTP request timeout')
1361
+ .action(() => {
1362
+ console.log(`${getTimeout()}ms`);
1363
+ });
1264
1364
  program
1265
1365
  .command('ignored [dir]')
1266
1366
  .description('Show ignore patterns currently in effect for a project directory')