Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ makepkg -si

### 1. Authenticate with Overleaf

Get your session cookie from Overleaf.com:
Use a session cookie from Overleaf.com:

1. Log into [overleaf.com](https://www.overleaf.com)
2. Open Developer Tools (F12 or Cmd+Option+I) → Application/Storage → Cookies
Expand All @@ -106,7 +106,13 @@ Store it with olcli:
olcli auth --cookie "your_session_cookie_value"
```

**Tip:** The cookie stays valid for weeks. Just refresh it when authentication fails.
Or use email/password login on instances that support the standard Overleaf login form:

```bash
olcli auth --email "you@example.com" --password "your_password"
```

Password login stores the account credentials in the local `olcli` config and refreshes the session cookie automatically when it expires.

### 2. List Your Projects

Expand Down Expand Up @@ -158,7 +164,7 @@ All commands auto-detect the project when run from a synced directory (contains

| Command | Description |
|---------|-------------|
| `olcli auth` | Set session cookie |
| `olcli auth` | Set session cookie or password login credentials |
| `olcli whoami` | Check authentication status |
| `olcli logout` | Clear stored credentials |
| `olcli list` | List all projects |
Expand Down Expand Up @@ -363,6 +369,7 @@ Credentials are stored in (checked in order):
1. `OVERLEAF_SESSION` environment variable
2. `.olauth` file in current directory
3. Global config: `~/.config/olcli-nodejs/config.json` (macOS/Linux)
4. Password login credentials in global config (`loginEmail` / `loginPassword`) for automatic session refresh

### .olauth File

Expand All @@ -388,6 +395,12 @@ olcli config set-url https://latex.example.org
olcli config set-cookie-name overleaf.sid
```

For self-hosted instances with the standard `/login` form, password login can persist the base URL and refresh future sessions:

```bash
olcli --base-url https://latex.example.org auth --email "you@example.com" --password "your_password"
```

## Examples

### Work on a thesis
Expand Down Expand Up @@ -456,6 +469,13 @@ import { OverleafClient } from '@aloth/olcli';
// Create a client from an Overleaf session cookie
const client = await OverleafClient.fromSessionCookie(cookie);

// Or log in with email/password
const passwordClient = await OverleafClient.fromPasswordLogin(
'you@example.com',
'your_password',
'https://latex.example.org'
);

// List all projects
const projects = await client.listProjects();
console.log(projects);
Expand Down Expand Up @@ -487,11 +507,13 @@ import {
// Types / interfaces
Project, ProjectInfo, FolderEntry, DocEntry, FileEntry,
CommentMessage, ProjectComment, CommentContext, CommentStatus,
ListCommentsOptions, AddCommentOptions, Credentials,
ListCommentsOptions, AddCommentOptions, Credentials, SessionCookiePair,

// Configuration utilities
getBaseUrl, setBaseUrl, getSessionCookie, setSessionCookie,
getSessionCookieName, setSessionCookieName, getCsrf, setCsrf,
getSessionCookieName, setSessionCookieName,
getPasswordCredentials, setPasswordCredentials, clearPasswordCredentials,
getCsrf, setCsrf,
getLastProject, setLastProject, clearConfig, getConfigPath, saveOlAuth,

// Ignore utilities
Expand Down Expand Up @@ -527,11 +549,12 @@ import {

### Authentication

The MCP server reads your session cookie in this order:
The MCP server reads credentials in this order:

1. **`OVERLEAF_SESSION` environment variable** — set in your MCP config (recommended)
2. **`.olauth` file in cwd** — written by `olcli auth`
3. **Stored config** — written by `olcli auth`
4. **Stored password login credentials** — written by `olcli auth --email <email> --password <password>`

### Claude Desktop

Expand Down Expand Up @@ -611,6 +634,8 @@ Add to `~/.codeium/windsurf/mcp_config.json`:

Or run `olcli auth` and then the MCP server will pick it up automatically.

If you used password login, the MCP server will reuse the stored session cookie first and refresh it with the saved password when needed.

### Self-hosted Overleaf

Set `OVERLEAF_BASE_URL` in your MCP env:
Expand All @@ -628,7 +653,7 @@ Set `OVERLEAF_BASE_URL` in your MCP env:

### Session expired

If you get authentication errors, your session cookie may have expired. Get a fresh one from the browser and run `olcli auth` again.
If you get authentication errors, your session cookie may have expired. Get a fresh one from the browser and run `olcli auth` again, or use password login so `olcli` can refresh the session automatically.

### Compilation fails

Expand Down
122 changes: 98 additions & 24 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ import {
getBaseUrl,
setBaseUrl,
getSessionCookieName,
setSessionCookieName
setSessionCookieName,
getPasswordCredentials,
setPasswordCredentials,
type PasswordCredentials
} from './config.js';

const program = new Command();
Expand All @@ -53,21 +56,53 @@ program
* Helper to get authenticated client
*/
async function getClient(cookieOpt?: string, baseUrlOpt?: string): Promise<OverleafClient> {
const cookie = cookieOpt || getSessionCookie();
if (!cookie) {
console.error(chalk.red('No session cookie found.'));
console.error('Set one with: olcli auth --cookie <session_cookie>');
console.error('Or set OVERLEAF_SESSION environment variable');
console.error('Or create .olauth file in current directory');
process.exit(1);
}
const baseUrl = baseUrlOpt || (program.opts().baseUrl as string | undefined) || getBaseUrl();
const cookieName = (program.opts().cookieName as string | undefined) || getSessionCookieName();
const client = await OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName);
const cookie = cookieOpt || getSessionCookie();
const passwordCredentials = cookieOpt ? undefined : getPasswordCredentials();

if (cookie) {
try {
const client = await OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName);
if (program.opts().verbose) client.setVerbose(true);
return client;
} catch (error) {
if (!passwordCredentials) throw error;
}
}

if (passwordCredentials) {
return loginWithSavedPassword(passwordCredentials, baseUrl, cookieName);
}

console.error(chalk.red('No session cookie or password credentials found.'));
console.error('Set one with: olcli auth --cookie <session_cookie>');
console.error('Or use: olcli auth --email <email> --password <password>');
console.error('Or set OVERLEAF_SESSION environment variable');
console.error('Or create .olauth file in current directory');
process.exit(1);
}

async function loginWithSavedPassword(
credentials: PasswordCredentials,
baseUrl: string,
cookieName: string
): Promise<OverleafClient> {
const client = await OverleafClient.fromPasswordLogin(credentials.email, credentials.password, baseUrl);
persistClientSession(client, cookieName);
if (program.opts().verbose) client.setVerbose(true);
return client;
}

function persistClientSession(client: OverleafClient, preferredCookieName: string): void {
const sessionCookie = client.getSessionCookiePair(preferredCookieName);
if (!sessionCookie) {
throw new Error('Password login succeeded, but no session cookie was returned.');
}
setSessionCookieName(sessionCookie.name);
setSessionCookie(sessionCookie.value);
}

/**
* Resolve project from argument or .olcli.json in current directory
*/
Expand Down Expand Up @@ -116,12 +151,15 @@ async function resolveProject(

program
.command('auth')
.description('Authenticate with Overleaf using session cookie')
.description('Authenticate with Overleaf using a session cookie or email/password')
.option('--cookie <session>', 'Session cookie (overleaf_session2 value)')
.option('--email <email>', 'Account email for password login')
.option('--password <password>', 'Account password for password login')
.option('--no-save-password', 'Do not persist email/password credentials')
.option('--save-local', 'Save to .olauth in current directory')
.action(async (options) => {
if (!options.cookie) {
console.log(chalk.yellow('To authenticate, provide your session cookie:'));
if (!options.cookie && !options.email && !options.password) {
console.log(chalk.yellow('To authenticate, provide a session cookie:'));
console.log();
console.log('1. Log into overleaf.com in your browser');
console.log('2. Open Developer Tools (F12) → Application → Cookies');
Expand All @@ -130,24 +168,51 @@ program
console.log();
console.log(chalk.cyan(' olcli auth --cookie "your_session_cookie_value"'));
console.log();
console.log('Or log in with email/password:');
console.log(chalk.cyan(' olcli auth --email "you@example.com" --password "your_password"'));
console.log();
console.log('Or set OVERLEAF_SESSION environment variable');
return;
}

if (options.cookie && (options.email || options.password)) {
console.error(chalk.red('Use either --cookie or --email/--password, not both.'));
process.exit(1);
}

if (!options.cookie && (!options.email || !options.password)) {
console.error(chalk.red('Both --email and --password are required for password login.'));
process.exit(1);
}

const spinner = ora('Verifying session...').start();
try {
const baseUrl = (program.opts().baseUrl as string | undefined) || getBaseUrl();
const cookieName = (program.opts().cookieName as string | undefined) || getSessionCookieName();
const client = await OverleafClient.fromSessionCookie(options.cookie, baseUrl, cookieName);
const projects = await client.listProjects();

setSessionCookie(options.cookie);
if (options.cookie) {
const client = await OverleafClient.fromSessionCookie(options.cookie, baseUrl, cookieName);
const projects = await client.listProjects();

setSessionCookie(options.cookie);

if (options.saveLocal) {
saveOlAuth(options.cookie);
spinner.succeed(`Authenticated! Found ${projects.length} projects. Saved to .olauth`);
if (options.saveLocal) {
saveOlAuth(options.cookie);
spinner.succeed(`Authenticated! Found ${projects.length} projects. Saved to .olauth`);
} else {
spinner.succeed(`Authenticated! Found ${projects.length} projects.`);
}
} else {
spinner.succeed(`Authenticated! Found ${projects.length} projects.`);
spinner.text = 'Logging in with email/password...';
const client = await OverleafClient.fromPasswordLogin(options.email, options.password, baseUrl);
const projects = await client.listProjects();
persistClientSession(client, cookieName);
setBaseUrl(baseUrl);
if (options.savePassword !== false) {
setPasswordCredentials(options.email, options.password);
}

spinner.succeed(`Authenticated! Found ${projects.length} projects. Password login saved.`);
}

console.log(chalk.dim(`Config saved to: ${getConfigPath()}`));
Expand All @@ -162,16 +227,15 @@ program
.description('Show current authentication status')
.action(async () => {
const cookie = getSessionCookie();
if (!cookie) {
const passwordCredentials = getPasswordCredentials();
if (!cookie && !passwordCredentials) {
console.log(chalk.yellow('Not authenticated'));
return;
}

const spinner = ora('Checking session...').start();
try {
const baseUrl = (program.opts().baseUrl as string | undefined) || getBaseUrl();
const cookieName = (program.opts().cookieName as string | undefined) || getSessionCookieName();
const client = await OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName);
const client = await getClient();
const projects = await client.listProjects();
spinner.succeed(`Authenticated with access to ${projects.length} projects`);
} catch (error: any) {
Expand Down Expand Up @@ -1467,6 +1531,7 @@ program
console.log(' 1. OVERLEAF_SESSION environment variable');
console.log(' 2. .olauth file in current directory');
console.log(' 3. Global config file');
console.log(' 4. Password login credentials in global config');
console.log();

const cookie = getSessionCookie();
Expand All @@ -1476,6 +1541,15 @@ program
} else {
console.log(chalk.yellow('✗ No session cookie found'));
}

const passwordCredentials = getPasswordCredentials();
if (passwordCredentials) {
console.log(chalk.green('✓ Password login credentials found'));
console.log(chalk.dim(` Email: ${passwordCredentials.email}`));
console.log(chalk.dim(' Password: [stored]'));
} else {
console.log(chalk.yellow('✗ No password login credentials found'));
}
});

program.parse(process.argv);
Loading