overlord-cli 5.1.0 → 5.1.1

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.
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
 
10
10
  const CREDENTIALS_DIR = path.join(os.homedir(), '.ovld');
11
11
  const CLI_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.cli.json');
12
+ const DESKTOP_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.desktop.json');
12
13
  const LEGACY_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
13
14
  const LEGACY_ELECTRON_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'electron-credentials.json');
14
15
  const LEGACY_MIGRATION_MARKER = path.join(CREDENTIALS_DIR, '.cli-migrated');
@@ -24,7 +25,8 @@ const LOCAL_SECRET_HEADER = 'X-Overlord-Local-Secret';
24
25
  * refresh_token?: string,
25
26
  * organization_id?: number | null,
26
27
  * platform_url: string,
27
- * user_email?: string
28
+ * user_email?: string,
29
+ * updated_at?: string
28
30
  * }} Credentials
29
31
  */
30
32
 
@@ -93,6 +95,9 @@ function parseStoredCredentialsData(parsed, { requireAuthData = false } = {}) {
93
95
  ...(organizationId ? { organization_id: organizationId } : {}),
94
96
  ...(typeof parsed.user_email === 'string' && parsed.user_email.trim()
95
97
  ? { user_email: parsed.user_email.trim() }
98
+ : {}),
99
+ ...(typeof parsed.updated_at === 'string' && parsed.updated_at.trim()
100
+ ? { updated_at: parsed.updated_at.trim() }
96
101
  : {})
97
102
  };
98
103
  }
@@ -112,7 +117,8 @@ function normalizeCredentialsForSave(data) {
112
117
  ? { access_token_expires_at: parsed.access_token_expires_at }
113
118
  : {}),
114
119
  ...(parsed.organization_id ? { organization_id: parsed.organization_id } : {}),
115
- ...(parsed.user_email ? { user_email: parsed.user_email } : {})
120
+ ...(parsed.user_email ? { user_email: parsed.user_email } : {}),
121
+ ...(parsed.updated_at ? { updated_at: parsed.updated_at } : {})
116
122
  };
117
123
  }
118
124
 
@@ -140,13 +146,57 @@ function migrateLegacyCredentials() {
140
146
  return source;
141
147
  }
142
148
 
149
+ function resolveAccessTokenExpiry(credentials) {
150
+ if (!credentials?.access_token) return null;
151
+ if (credentials.access_token_expires_at) {
152
+ const parsed = Date.parse(credentials.access_token_expires_at);
153
+ if (Number.isFinite(parsed)) return parsed;
154
+ }
155
+ const jwtExp = decodeJwtExpiry(credentials.access_token);
156
+ return jwtExp ? jwtExp * 1000 : null;
157
+ }
158
+
159
+ function isAccessTokenFresh(credentials) {
160
+ const expiresAt = resolveAccessTokenExpiry(credentials);
161
+ if (expiresAt === null) return false;
162
+ return expiresAt - Date.now() > 60_000;
163
+ }
164
+
165
+ function credentialsUpdatedAt(credentials) {
166
+ const parsed = Date.parse(credentials?.updated_at ?? '');
167
+ return Number.isFinite(parsed) ? parsed : 0;
168
+ }
169
+
170
+ function selectStoredCredentials() {
171
+ const candidates = [
172
+ {
173
+ source: 'credentials.cli.json',
174
+ credentials: parseStoredCredentialsData(readJsonFile(CLI_CREDENTIALS_FILE), {
175
+ requireAuthData: true
176
+ })
177
+ },
178
+ {
179
+ source: 'credentials.desktop.json',
180
+ credentials: parseStoredCredentialsData(readJsonFile(DESKTOP_CREDENTIALS_FILE), {
181
+ requireAuthData: true
182
+ })
183
+ }
184
+ ].filter(candidate => candidate.credentials?.refresh_token);
185
+
186
+ if (candidates.length === 0) return null;
187
+
188
+ const fresh = candidates.filter(candidate => isAccessTokenFresh(candidate.credentials));
189
+ const pool = fresh.length > 0 ? fresh : candidates;
190
+ return pool.sort(
191
+ (left, right) =>
192
+ credentialsUpdatedAt(right.credentials) - credentialsUpdatedAt(left.credentials)
193
+ )[0];
194
+ }
195
+
143
196
  /** @returns {Credentials | null} */
144
197
  export function loadCredentials() {
145
- const cliCredentials = parseStoredCredentialsData(readJsonFile(CLI_CREDENTIALS_FILE), {
146
- requireAuthData: true
147
- });
148
-
149
- if (cliCredentials?.refresh_token) return cliCredentials;
198
+ const selected = selectStoredCredentials();
199
+ if (selected?.credentials) return selected.credentials;
150
200
 
151
201
  return migrateLegacyCredentials();
152
202
  }
@@ -161,6 +211,16 @@ export function saveCredentials(data) {
161
211
  writeJsonFileAtomic(CLI_CREDENTIALS_FILE, { ...credentials, updated_at: new Date().toISOString() });
162
212
  }
163
213
 
214
+ function saveCredentialsToSource(data, source) {
215
+ const credentials = normalizeCredentialsForSave(data);
216
+ if (!credentials) {
217
+ throw new Error('Cannot save empty Overlord credentials.');
218
+ }
219
+
220
+ const filePath = source === 'credentials.desktop.json' ? DESKTOP_CREDENTIALS_FILE : CLI_CREDENTIALS_FILE;
221
+ writeJsonFileAtomic(filePath, { ...credentials, updated_at: new Date().toISOString() });
222
+ }
223
+
164
224
  export function clearCredentials() {
165
225
  try {
166
226
  fs.unlinkSync(CLI_CREDENTIALS_FILE);
@@ -170,10 +230,8 @@ export function clearCredentials() {
170
230
  }
171
231
 
172
232
  function getCredentialFileSource() {
173
- const cliCredentials = parseStoredCredentialsData(readJsonFile(CLI_CREDENTIALS_FILE), {
174
- requireAuthData: true
175
- });
176
- if (cliCredentials?.refresh_token) return 'credentials.cli.json';
233
+ const selected = selectStoredCredentials();
234
+ if (selected) return selected.source;
177
235
 
178
236
  if (fileExists(LEGACY_CREDENTIALS_FILE)) {
179
237
  const legacyShared = parseStoredCredentialsData(readJsonFile(LEGACY_CREDENTIALS_FILE), {
@@ -338,22 +396,6 @@ function computeAccessTokenExpiry(data) {
338
396
  return jwtExp ? new Date(jwtExp * 1000).toISOString() : null;
339
397
  }
340
398
 
341
- function resolveAccessTokenExpiry(credentials) {
342
- if (!credentials?.access_token) return null;
343
- if (credentials.access_token_expires_at) {
344
- const parsed = Date.parse(credentials.access_token_expires_at);
345
- if (Number.isFinite(parsed)) return parsed;
346
- }
347
- const jwtExp = decodeJwtExpiry(credentials.access_token);
348
- return jwtExp ? jwtExp * 1000 : null;
349
- }
350
-
351
- function isAccessTokenFresh(credentials) {
352
- const expiresAt = resolveAccessTokenExpiry(credentials);
353
- if (expiresAt === null) return false;
354
- return expiresAt - Date.now() > 60_000;
355
- }
356
-
357
399
  function describeNetworkError(error, context) {
358
400
  const cause = error?.cause;
359
401
  const details = [cause?.code, cause?.message].filter(Boolean).join(': ');
@@ -483,7 +525,8 @@ function isLocalDevCli() {
483
525
  * Refreshes OAuth access tokens when possible.
484
526
  */
485
527
  export async function resolveAuth() {
486
- const creds = loadCredentials();
528
+ const selectedCredentials = selectStoredCredentials();
529
+ const creds = selectedCredentials?.credentials ?? migrateLegacyCredentials();
487
530
  const overlordUrlFromEnv = normalizePlatformUrl(process.env.OVERLORD_URL);
488
531
  const overlordUrlFromCreds = normalizeStoredPlatformUrl(creds?.platform_url);
489
532
 
@@ -540,7 +583,7 @@ export async function resolveAuth() {
540
583
  access_token_expires_at: refreshed.access_token_expires_at,
541
584
  refresh_token: refreshed.refresh_token || creds.refresh_token
542
585
  };
543
- saveCredentials(nextCredentials);
586
+ saveCredentialsToSource(nextCredentials, selectedCredentials?.source);
544
587
  } catch (refreshError) {
545
588
  throw new Error(
546
589
  `Stored Overlord session expired and refresh failed. ${refreshError instanceof Error ? refreshError.message : String(refreshError)} Run \`ovld auth login\` again.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {