overlord-cli 4.23.0 → 5.1.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
@@ -25,6 +25,14 @@ overlord help
25
25
  The CLI exposes the same command set under both names.
26
26
  `ovld auth login` opens a browser when possible and also prints a verification URL/code so login can be completed from another machine over SSH.
27
27
 
28
+ Desktop-installed wrappers default `OVERLORD_URL` to `https://www.ovld.ai` unless you override it explicitly.
29
+ For local dev against the web app on port 3000, export the override before running auth or protocol commands:
30
+
31
+ ```bash
32
+ export OVERLORD_URL=http://localhost:3000
33
+ ovld auth login
34
+ ```
35
+
28
36
  Common commands:
29
37
 
30
38
  ```bash
package/bin/_cli/auth.mjs CHANGED
@@ -144,6 +144,17 @@ function snippet(value, max = 180) {
144
144
  return normalized.length <= max ? normalized : `${normalized.slice(0, max)}...`;
145
145
  }
146
146
 
147
+ function describeNetworkError(error, context) {
148
+ const cause = error?.cause;
149
+ const details = [cause?.code, cause?.message].filter(Boolean).join(': ');
150
+ if (details) {
151
+ return new Error(`${context}: ${details}`);
152
+ }
153
+
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ return new Error(`${context}: ${message}`);
156
+ }
157
+
147
158
  async function readJsonOrThrow(res, context, baseUrl) {
148
159
  const contentType = res.headers.get('content-type') ?? '';
149
160
  const bodyText = await res.text();
@@ -165,9 +176,14 @@ async function readJsonOrThrow(res, context, baseUrl) {
165
176
  }
166
177
 
167
178
  async function fetchAuthConfig(platformUrl, localSecret) {
168
- const res = await fetch(`${platformUrl}/api/auth/config`, {
169
- headers: buildAuthHeaders('', localSecret)
170
- });
179
+ let res;
180
+ try {
181
+ res = await fetch(`${platformUrl}/api/auth/config`, {
182
+ headers: buildAuthHeaders('', localSecret)
183
+ });
184
+ } catch (error) {
185
+ throw describeNetworkError(error, `Failed to fetch auth config from ${platformUrl}`);
186
+ }
171
187
  if (!res.ok) {
172
188
  throw new Error(
173
189
  `Failed to fetch auth config (${res.status}). Check that Overlord is running at ${platformUrl}.`
@@ -181,10 +197,18 @@ async function fetchAuthConfig(platformUrl, localSecret) {
181
197
  }
182
198
 
183
199
  async function requestDeviceAuthorization(platformUrl, localSecret) {
184
- const res = await fetch(`${platformUrl}/api/auth/device/request`, {
185
- method: 'POST',
186
- headers: buildAuthHeaders('', localSecret)
187
- });
200
+ let res;
201
+ try {
202
+ res = await fetch(`${platformUrl}/api/auth/device/request`, {
203
+ method: 'POST',
204
+ headers: buildAuthHeaders('', localSecret)
205
+ });
206
+ } catch (error) {
207
+ throw describeNetworkError(
208
+ error,
209
+ `Device authorization request failed for ${platformUrl}`
210
+ );
211
+ }
188
212
 
189
213
  if (!res.ok) {
190
214
  const text = await res.text();
@@ -195,14 +219,22 @@ async function requestDeviceAuthorization(platformUrl, localSecret) {
195
219
  }
196
220
 
197
221
  async function pollDeviceAuthorization(platformUrl, deviceCode, localSecret) {
198
- const res = await fetch(`${platformUrl}/api/auth/device/poll`, {
199
- method: 'POST',
200
- headers: {
201
- ...buildAuthHeaders('', localSecret),
202
- 'Content-Type': 'application/json'
203
- },
204
- body: JSON.stringify({ device_code: deviceCode })
205
- });
222
+ let res;
223
+ try {
224
+ res = await fetch(`${platformUrl}/api/auth/device/poll`, {
225
+ method: 'POST',
226
+ headers: {
227
+ ...buildAuthHeaders('', localSecret),
228
+ 'Content-Type': 'application/json'
229
+ },
230
+ body: JSON.stringify({ device_code: deviceCode })
231
+ });
232
+ } catch (error) {
233
+ throw describeNetworkError(
234
+ error,
235
+ `Device authorization poll failed for ${platformUrl}`
236
+ );
237
+ }
206
238
 
207
239
  const body = await readJsonOrThrow(res, 'Device authorization poll', platformUrl);
208
240
 
@@ -220,17 +252,25 @@ function sleep(ms) {
220
252
  }
221
253
 
222
254
  async function exchangeCodeForSupabaseTokens(supabaseUrl, clientId, code, codeVerifier, redirectUri) {
223
- const res = await fetch(`${supabaseUrl}/auth/v1/oauth/token`, {
224
- method: 'POST',
225
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
226
- body: new URLSearchParams({
227
- grant_type: 'authorization_code',
228
- code,
229
- client_id: clientId,
230
- redirect_uri: redirectUri,
231
- code_verifier: codeVerifier
232
- })
233
- });
255
+ let res;
256
+ try {
257
+ res = await fetch(`${supabaseUrl}/auth/v1/oauth/token`, {
258
+ method: 'POST',
259
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
260
+ body: new URLSearchParams({
261
+ grant_type: 'authorization_code',
262
+ code,
263
+ client_id: clientId,
264
+ redirect_uri: redirectUri,
265
+ code_verifier: codeVerifier
266
+ })
267
+ });
268
+ } catch (error) {
269
+ throw describeNetworkError(
270
+ error,
271
+ `Token exchange failed for ${new URL(supabaseUrl).host}`
272
+ );
273
+ }
234
274
 
235
275
  if (!res.ok) {
236
276
  const text = await res.text();
@@ -474,8 +514,8 @@ async function promptForOrganization(organizations, preselectedId = null) {
474
514
  // Public auth commands
475
515
  // ---------------------------------------------------------------------------
476
516
 
477
- export function resolveLoginPlatformUrl(runtime = null) {
478
- return process.env.OVERLORD_URL ?? runtime?.platform_url ?? getDefaultOverlordUrl();
517
+ export function resolveLoginPlatformUrl(runtime = null, storedPlatformUrl = null) {
518
+ return process.env.OVERLORD_URL ?? storedPlatformUrl ?? runtime?.platform_url ?? getDefaultOverlordUrl();
479
519
  }
480
520
 
481
521
  function parseOrganizationFlag(args) {
@@ -491,7 +531,8 @@ function parseOrganizationFlag(args) {
491
531
 
492
532
  export async function authLogin(args = []) {
493
533
  const preselectedOrganizationId = parseOrganizationFlag(args);
494
- const platformUrl = resolveLoginPlatformUrl();
534
+ const storedCredentials = loadCredentials();
535
+ const platformUrl = resolveLoginPlatformUrl(null, storedCredentials?.platform_url ?? null);
495
536
  const runtime = loadRuntime(platformUrl);
496
537
  const localSecret = runtime?.local_secret ?? process.env.OVERLORD_LOCAL_SECRET ?? '';
497
538
 
@@ -556,10 +597,13 @@ async function printVerboseAuthStatus() {
556
597
  console.log(` Error: ${status.error}`);
557
598
  }
558
599
  console.log(` Local secret: ${status.hasLocalSecret ? 'yes' : 'no'}`);
559
- console.log(` credentials.json: ${status.credentialsFileExists ? 'present' : 'missing'}`);
560
- console.log(
561
- ` electron-credentials.json: ${status.electronCredentialsFileExists ? 'present' : 'missing'}`
562
- );
600
+ console.log(` credentials.cli.json: ${status.credentialsFileExists ? 'present' : 'missing'}`);
601
+ if (status.legacyCredentialsFileExists) {
602
+ console.log(` credentials.json (legacy): present`);
603
+ }
604
+ if (status.electronCredentialsFileExists) {
605
+ console.log(` electron-credentials.json (legacy): present`);
606
+ }
563
607
  }
564
608
 
565
609
  export async function authStatus(args = []) {
@@ -8,8 +8,10 @@ import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
 
10
10
  const CREDENTIALS_DIR = path.join(os.homedir(), '.ovld');
11
- const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
12
- const ELECTRON_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'electron-credentials.json');
11
+ const CLI_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.cli.json');
12
+ const LEGACY_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
13
+ const LEGACY_ELECTRON_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'electron-credentials.json');
14
+ const LEGACY_MIGRATION_MARKER = path.join(CREDENTIALS_DIR, '.cli-migrated');
13
15
  const RUNTIME_FILE_PATTERN = /^runtime\..+\.json$/;
14
16
  const HOSTED_OVERLORD_URL = 'https://www.ovld.ai';
15
17
  const LOCAL_DEV_OVERLORD_URL = 'http://localhost:3000';
@@ -114,18 +116,39 @@ function normalizeCredentialsForSave(data) {
114
116
  };
115
117
  }
116
118
 
117
- /** @returns {Credentials | null} */
118
- export function loadCredentials() {
119
- const cliCredentials = parseStoredCredentialsData(readJsonFile(CREDENTIALS_FILE), {
119
+ function migrateLegacyCredentials() {
120
+ if (fileExists(LEGACY_MIGRATION_MARKER)) return null;
121
+
122
+ const legacyShared = parseStoredCredentialsData(readJsonFile(LEGACY_CREDENTIALS_FILE), {
123
+ requireAuthData: true
124
+ });
125
+ const legacyElectron = parseStoredCredentialsData(readJsonFile(LEGACY_ELECTRON_CREDENTIALS_FILE), {
120
126
  requireAuthData: true
121
127
  });
122
- const electronCredentials = parseStoredCredentialsData(readJsonFile(ELECTRON_CREDENTIALS_FILE), {
128
+
129
+ const source = legacyShared ?? legacyElectron;
130
+ if (!source) return null;
131
+
132
+ try {
133
+ writeJsonFileAtomic(CLI_CREDENTIALS_FILE, { ...source, updated_at: new Date().toISOString() });
134
+ ensureCredentialsDir();
135
+ fs.writeFileSync(LEGACY_MIGRATION_MARKER, new Date().toISOString(), { mode: 0o600 });
136
+ } catch {
137
+ // Best-effort migration
138
+ }
139
+
140
+ return source;
141
+ }
142
+
143
+ /** @returns {Credentials | null} */
144
+ export function loadCredentials() {
145
+ const cliCredentials = parseStoredCredentialsData(readJsonFile(CLI_CREDENTIALS_FILE), {
123
146
  requireAuthData: true
124
147
  });
125
148
 
126
149
  if (cliCredentials?.refresh_token) return cliCredentials;
127
- if (electronCredentials?.refresh_token) return electronCredentials;
128
- return cliCredentials ?? electronCredentials;
150
+
151
+ return migrateLegacyCredentials();
129
152
  }
130
153
 
131
154
  /** @param {Credentials} data */
@@ -135,45 +158,36 @@ export function saveCredentials(data) {
135
158
  throw new Error('Cannot save empty Overlord credentials.');
136
159
  }
137
160
 
138
- const sharedCredentials = { ...credentials, updated_at: new Date().toISOString() };
139
- writeJsonFileAtomic(CREDENTIALS_FILE, sharedCredentials);
140
-
141
- const existingElectronCredentials = readJsonFile(ELECTRON_CREDENTIALS_FILE);
142
- if (existingElectronCredentials && typeof existingElectronCredentials === 'object') {
143
- const electronPayload = { ...existingElectronCredentials };
144
- electronPayload.updated_at = sharedCredentials.updated_at;
145
- if (credentials.platform_url) electronPayload.platform_url = credentials.platform_url;
146
- if (credentials.access_token_expires_at) {
147
- electronPayload.access_token_expires_at = credentials.access_token_expires_at;
148
- }
149
- if (credentials.organization_id) electronPayload.organization_id = credentials.organization_id;
150
- if (credentials.user_email) electronPayload.user_email = credentials.user_email;
151
- delete electronPayload.supabase_refresh_token;
152
- writeJsonFileAtomic(ELECTRON_CREDENTIALS_FILE, electronPayload);
153
- }
161
+ writeJsonFileAtomic(CLI_CREDENTIALS_FILE, { ...credentials, updated_at: new Date().toISOString() });
154
162
  }
155
163
 
156
164
  export function clearCredentials() {
157
- for (const filePath of [CREDENTIALS_FILE, ELECTRON_CREDENTIALS_FILE]) {
158
- try {
159
- fs.unlinkSync(filePath);
160
- } catch {
161
- // Already gone
162
- }
165
+ try {
166
+ fs.unlinkSync(CLI_CREDENTIALS_FILE);
167
+ } catch {
168
+ // Already gone
163
169
  }
164
170
  }
165
171
 
166
172
  function getCredentialFileSource() {
167
- const cliCredentials = parseStoredCredentialsData(readJsonFile(CREDENTIALS_FILE), {
168
- requireAuthData: true
169
- });
170
- const electronCredentials = parseStoredCredentialsData(readJsonFile(ELECTRON_CREDENTIALS_FILE), {
173
+ const cliCredentials = parseStoredCredentialsData(readJsonFile(CLI_CREDENTIALS_FILE), {
171
174
  requireAuthData: true
172
175
  });
173
- if (cliCredentials?.refresh_token) return 'credentials.json';
174
- if (electronCredentials?.refresh_token) return 'electron-credentials.json';
175
- if (cliCredentials) return 'credentials.json';
176
- if (electronCredentials) return 'electron-credentials.json';
176
+ if (cliCredentials?.refresh_token) return 'credentials.cli.json';
177
+
178
+ if (fileExists(LEGACY_CREDENTIALS_FILE)) {
179
+ const legacyShared = parseStoredCredentialsData(readJsonFile(LEGACY_CREDENTIALS_FILE), {
180
+ requireAuthData: true
181
+ });
182
+ if (legacyShared?.refresh_token) return 'credentials.json (legacy)';
183
+ }
184
+
185
+ if (fileExists(LEGACY_ELECTRON_CREDENTIALS_FILE)) {
186
+ const legacyElectron = parseStoredCredentialsData(readJsonFile(LEGACY_ELECTRON_CREDENTIALS_FILE), {
187
+ requireAuthData: true
188
+ });
189
+ if (legacyElectron?.refresh_token) return 'electron-credentials.json (legacy)';
190
+ }
177
191
 
178
192
  return 'none';
179
193
  }
@@ -340,13 +354,29 @@ function isAccessTokenFresh(credentials) {
340
354
  return expiresAt - Date.now() > 60_000;
341
355
  }
342
356
 
357
+ function describeNetworkError(error, context) {
358
+ const cause = error?.cause;
359
+ const details = [cause?.code, cause?.message].filter(Boolean).join(': ');
360
+ if (details) {
361
+ return new Error(`${context}: ${details}`);
362
+ }
363
+
364
+ const message = error instanceof Error ? error.message : String(error);
365
+ return new Error(`${context}: ${message}`);
366
+ }
367
+
343
368
  const authConfigCache = new Map();
344
369
 
345
370
  async function fetchAuthConfig(platformUrl, localSecret) {
346
371
  if (authConfigCache.has(platformUrl)) return authConfigCache.get(platformUrl);
347
- const res = await fetch(`${platformUrl}/api/auth/config`, {
348
- headers: buildAuthHeaders('', localSecret)
349
- });
372
+ let res;
373
+ try {
374
+ res = await fetch(`${platformUrl}/api/auth/config`, {
375
+ headers: buildAuthHeaders('', localSecret)
376
+ });
377
+ } catch (error) {
378
+ throw describeNetworkError(error, `Failed to fetch auth config from ${platformUrl}`);
379
+ }
350
380
  if (!res.ok) {
351
381
  throw new Error(`Failed to fetch auth config (${res.status}).`);
352
382
  }
@@ -364,15 +394,23 @@ async function refreshOAuthAccessToken(platformUrl, refreshToken, localSecret) {
364
394
  throw new Error('OAuth is not configured for Overlord CLI auth.');
365
395
  }
366
396
 
367
- const res = await fetch(`${supabaseUrl}/auth/v1/oauth/token`, {
368
- method: 'POST',
369
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
370
- body: new URLSearchParams({
371
- grant_type: 'refresh_token',
372
- refresh_token: refreshToken,
373
- client_id: clientId
374
- })
375
- });
397
+ let res;
398
+ try {
399
+ res = await fetch(`${supabaseUrl}/auth/v1/oauth/token`, {
400
+ method: 'POST',
401
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
402
+ body: new URLSearchParams({
403
+ grant_type: 'refresh_token',
404
+ refresh_token: refreshToken,
405
+ client_id: clientId
406
+ })
407
+ });
408
+ } catch (error) {
409
+ throw describeNetworkError(
410
+ error,
411
+ `OAuth token refresh failed for ${new URL(supabaseUrl).host}`
412
+ );
413
+ }
376
414
 
377
415
  if (!res.ok) {
378
416
  const text = await res.text().catch(() => '');
@@ -504,9 +542,9 @@ export async function resolveAuth() {
504
542
  };
505
543
  saveCredentials(nextCredentials);
506
544
  } catch (refreshError) {
507
- if (!creds.access_token) throw refreshError;
508
- // Transient refresh failure keep the existing access token and let the server
509
- // reject it if it's truly expired/revoked.
545
+ throw new Error(
546
+ `Stored Overlord session expired and refresh failed. ${refreshError instanceof Error ? refreshError.message : String(refreshError)} Run \`ovld auth login\` again.`
547
+ );
510
548
  }
511
549
  }
512
550
 
@@ -570,7 +608,7 @@ export async function getAuthStatus() {
570
608
  }
571
609
 
572
610
  return {
573
- isLoggedIn: tokenSource !== 'fallback',
611
+ isLoggedIn: tokenSource !== 'fallback' && !error,
574
612
  platformUrl: resolved.platformUrl,
575
613
  platformUrlSource,
576
614
  tokenPresent: tokenSource !== 'fallback',
@@ -579,8 +617,9 @@ export async function getAuthStatus() {
579
617
  organizationId: resolved.organizationId ?? null,
580
618
  authMode: resolved.authMode,
581
619
  error,
582
- credentialsFileExists: fileExists(CREDENTIALS_FILE),
583
- electronCredentialsFileExists: fileExists(ELECTRON_CREDENTIALS_FILE)
620
+ credentialsFileExists: fileExists(CLI_CREDENTIALS_FILE),
621
+ legacyCredentialsFileExists: fileExists(LEGACY_CREDENTIALS_FILE),
622
+ electronCredentialsFileExists: fileExists(LEGACY_ELECTRON_CREDENTIALS_FILE)
584
623
  };
585
624
  }
586
625
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "4.23.0",
3
+ "version": "5.1.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {