overlord-cli 4.17.0 → 4.19.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.
@@ -58,11 +58,11 @@ function statusColor(status) {
58
58
 
59
59
  // ─── API ──────────────────────────────────────────────────────────────────────
60
60
 
61
- async function searchTickets(platformUrl, agentToken, localSecret, query) {
61
+ async function searchTickets(platformUrl, bearerToken, localSecret, organizationId, query) {
62
62
  const res = await fetch(`${platformUrl}/api/protocol/search-tickets`, {
63
63
  method: 'POST',
64
64
  headers: {
65
- ...buildAuthHeaders(agentToken, localSecret),
65
+ ...buildAuthHeaders(bearerToken, localSecret, organizationId),
66
66
  'Content-Type': 'application/json'
67
67
  },
68
68
  body: JSON.stringify({
@@ -287,7 +287,7 @@ function runInteractivePrompt({ label, items = [], search, prefix = '' }) {
287
287
  export async function runAttachCommand(args) {
288
288
  const [ticketIdArg, agentArg] = args;
289
289
 
290
- const { platformUrl, agentToken, localSecret } = resolveAuth();
290
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
291
291
 
292
292
  // ── Phase 1: Ticket selection ──────────────────────────────────────────────
293
293
 
@@ -301,7 +301,8 @@ export async function runAttachCommand(args) {
301
301
 
302
302
  const selectedTicket = await runInteractivePrompt({
303
303
  label: 'Search tickets',
304
- search: nextQuery => searchTickets(platformUrl, agentToken, localSecret, nextQuery)
304
+ search: nextQuery =>
305
+ searchTickets(platformUrl, bearerToken, localSecret, organizationId, nextQuery)
305
306
  });
306
307
 
307
308
  if (!selectedTicket) {
package/bin/_cli/auth.mjs CHANGED
@@ -240,23 +240,6 @@ async function exchangeCodeForSupabaseTokens(supabaseUrl, clientId, code, codeVe
240
240
  return readJsonOrThrow(res, 'Token exchange', supabaseUrl);
241
241
  }
242
242
 
243
- async function exchangeForAgentToken(platformUrl, supabaseAccessToken, localSecret) {
244
- const res = await fetch(`${platformUrl}/api/auth/token`, {
245
- method: 'POST',
246
- headers: {
247
- ...buildAuthHeaders('', localSecret),
248
- Authorization: `Bearer ${supabaseAccessToken}`
249
- }
250
- });
251
-
252
- if (!res.ok) {
253
- const text = await res.text();
254
- throw new Error(`Agent token exchange failed (${res.status}): ${snippet(text)}`);
255
- }
256
-
257
- return readJsonOrThrow(res, 'Agent token exchange', platformUrl);
258
- }
259
-
260
243
  function openBrowser(url) {
261
244
  try {
262
245
  const platform = process.platform;
@@ -331,6 +314,8 @@ export async function authLoginViaDeviceFlow(
331
314
  logger.log('\n');
332
315
  return {
333
316
  access_token: result.access_token,
317
+ access_token_expires_at: result.access_token_expires_at ?? null,
318
+ refresh_token: result.refresh_token,
334
319
  platform_url: result.platform_url ?? platformUrl
335
320
  };
336
321
  }
@@ -403,19 +388,88 @@ export async function authLoginViaOAuthLoopback(platformUrl, localSecret) {
403
388
  redirectUri
404
389
  );
405
390
 
406
- // 8. Exchange Supabase access token → Overlord agent_token
407
- const agentTokenData = await exchangeForAgentToken(
408
- resolvedPlatformUrl,
409
- supabaseTokens.access_token,
410
- localSecret
411
- );
412
-
413
391
  return {
414
- access_token: agentTokenData.access_token,
415
- platform_url: agentTokenData.platform_url ?? resolvedPlatformUrl
392
+ access_token: supabaseTokens.access_token,
393
+ access_token_expires_at:
394
+ typeof supabaseTokens.expires_in === 'number' && supabaseTokens.expires_in > 0
395
+ ? new Date(Date.now() + supabaseTokens.expires_in * 1000).toISOString()
396
+ : null,
397
+ refresh_token: supabaseTokens.refresh_token,
398
+ platform_url: resolvedPlatformUrl
416
399
  };
417
400
  }
418
401
 
402
+ async function fetchOrganizations(platformUrl, accessToken, localSecret) {
403
+ const res = await fetch(`${platformUrl}/api/auth/organizations`, {
404
+ headers: {
405
+ ...buildAuthHeaders('', localSecret),
406
+ Authorization: `Bearer ${accessToken}`
407
+ }
408
+ });
409
+
410
+ if (!res.ok) {
411
+ const text = await res.text().catch(() => '');
412
+ throw new Error(`Failed to load organizations (${res.status}): ${snippet(text)}`);
413
+ }
414
+
415
+ const data = await readJsonOrThrow(res, 'Organizations', platformUrl);
416
+ return Array.isArray(data.organizations) ? data.organizations : [];
417
+ }
418
+
419
+ async function promptForOrganization(organizations, preselectedId = null) {
420
+ if (!organizations.length) {
421
+ throw new Error('No organizations found. Please complete onboarding first.');
422
+ }
423
+
424
+ if (preselectedId !== null) {
425
+ const match = organizations.find(org => org.id === preselectedId);
426
+ if (!match) {
427
+ throw new Error(
428
+ `Organization ${preselectedId} is not available to this account. ` +
429
+ `Available: ${organizations.map(o => o.id).join(', ')}`
430
+ );
431
+ }
432
+ return match;
433
+ }
434
+
435
+ if (organizations.length === 1) {
436
+ return organizations[0];
437
+ }
438
+
439
+ if (!process.stdin.isTTY) {
440
+ throw new Error(
441
+ 'Multiple organizations available but stdin is not a TTY. ' +
442
+ 'Pass --organization-id <id> to select non-interactively.'
443
+ );
444
+ }
445
+
446
+ const rl = (await import('node:readline')).createInterface({
447
+ input: process.stdin,
448
+ output: process.stdout
449
+ });
450
+
451
+ try {
452
+ for (;;) {
453
+ console.log('\nOrganizations');
454
+ organizations.forEach((organization, index) => {
455
+ console.log(` ${index + 1}. ${organization.name} (${organization.id})`);
456
+ });
457
+
458
+ const answer = await new Promise(resolve => {
459
+ rl.question('\nSelect an organization by number: ', resolve);
460
+ });
461
+ const selected = Number.parseInt(String(answer).trim(), 10);
462
+ if (Number.isFinite(selected) && selected >= 1 && selected <= organizations.length) {
463
+ return organizations[selected - 1];
464
+ }
465
+
466
+ console.log(`Enter a number between 1 and ${organizations.length}.`);
467
+ }
468
+ } finally {
469
+ rl.close();
470
+ }
471
+ }
472
+
419
473
  // ---------------------------------------------------------------------------
420
474
  // Public auth commands
421
475
  // ---------------------------------------------------------------------------
@@ -424,7 +478,19 @@ export function resolveLoginPlatformUrl(runtime = null) {
424
478
  return process.env.OVERLORD_URL ?? runtime?.platform_url ?? getDefaultOverlordUrl();
425
479
  }
426
480
 
427
- export async function authLogin() {
481
+ function parseOrganizationFlag(args) {
482
+ const index = args.findIndex(arg => arg === '--organization-id' || arg.startsWith('--organization-id='));
483
+ if (index === -1) return null;
484
+ const raw = args[index].includes('=') ? args[index].split('=')[1] : args[index + 1];
485
+ const parsed = Number.parseInt(String(raw ?? ''), 10);
486
+ if (!Number.isFinite(parsed)) {
487
+ throw new Error('--organization-id must be a numeric id');
488
+ }
489
+ return parsed;
490
+ }
491
+
492
+ export async function authLogin(args = []) {
493
+ const preselectedOrganizationId = parseOrganizationFlag(args);
428
494
  const platformUrl = resolveLoginPlatformUrl();
429
495
  const runtime = loadRuntime(platformUrl);
430
496
  const localSecret = runtime?.local_secret ?? process.env.OVERLORD_LOCAL_SECRET ?? '';
@@ -454,16 +520,27 @@ export async function authLogin() {
454
520
  }
455
521
  }
456
522
 
523
+ const resolvedPlatformUrl = credentials.platform_url ?? platformUrl;
524
+ const organizations = await fetchOrganizations(
525
+ resolvedPlatformUrl,
526
+ credentials.access_token,
527
+ localSecret
528
+ );
529
+ const selectedOrganization = await promptForOrganization(organizations, preselectedOrganizationId);
530
+
457
531
  saveCredentials({
458
532
  access_token: credentials.access_token,
459
- platform_url: credentials.platform_url ?? platformUrl
533
+ access_token_expires_at: credentials.access_token_expires_at ?? undefined,
534
+ refresh_token: credentials.refresh_token,
535
+ organization_id: selectedOrganization.id,
536
+ platform_url: resolvedPlatformUrl
460
537
  });
461
538
 
462
539
  console.log('Logged in successfully!');
463
540
  }
464
541
 
465
- function printVerboseAuthStatus() {
466
- const status = getAuthStatus();
542
+ async function printVerboseAuthStatus() {
543
+ const status = await getAuthStatus();
467
544
  if (!status.isLoggedIn) {
468
545
  console.log('Not logged in. Run: ovld auth login');
469
546
  } else {
@@ -473,6 +550,11 @@ function printVerboseAuthStatus() {
473
550
  console.log(` Platform source: ${status.platformUrlSource}`);
474
551
  console.log(` Token source: ${status.tokenSource}`);
475
552
  console.log(` Token present: ${status.tokenPresent ? 'yes' : 'no'}`);
553
+ console.log(` Auth mode: ${status.authMode}`);
554
+ console.log(` Organization ID: ${status.organizationId ?? 'none'}`);
555
+ if (status.error) {
556
+ console.log(` Error: ${status.error}`);
557
+ }
476
558
  console.log(` Local secret: ${status.hasLocalSecret ? 'yes' : 'no'}`);
477
559
  console.log(` credentials.json: ${status.credentialsFileExists ? 'present' : 'missing'}`);
478
560
  console.log(
@@ -480,9 +562,9 @@ function printVerboseAuthStatus() {
480
562
  );
481
563
  }
482
564
 
483
- export function authStatus(args = []) {
565
+ export async function authStatus(args = []) {
484
566
  if (args.includes('--verbose') || args.includes('-v')) {
485
- printVerboseAuthStatus();
567
+ await printVerboseAuthStatus();
486
568
  return;
487
569
  }
488
570
 
@@ -510,7 +592,7 @@ export function authRepair() {
510
592
  } else {
511
593
  console.log(`Credentials not repaired: ${result.reason}`);
512
594
  }
513
- printVerboseAuthStatus();
595
+ void printVerboseAuthStatus();
514
596
  }
515
597
 
516
598
  export async function runAuthCommand(subcommand, args = []) {
@@ -527,12 +609,12 @@ Subcommands:
527
609
  }
528
610
 
529
611
  if (subcommand === 'login') {
530
- await authLogin();
612
+ await authLogin(args);
531
613
  return;
532
614
  }
533
615
 
534
616
  if (subcommand === 'status') {
535
- authStatus(args);
617
+ await authStatus(args);
536
618
  return;
537
619
  }
538
620
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /* global process, URL */
3
+ /* global Buffer, fetch, process, URL, URLSearchParams */
4
4
 
5
5
  import fs from 'node:fs';
6
6
  import os from 'node:os';
@@ -16,7 +16,15 @@ const LOCAL_DEV_OVERLORD_URL = 'http://localhost:3000';
16
16
  const LOCAL_SECRET_HEADER = 'X-Overlord-Local-Secret';
17
17
 
18
18
  /**
19
- * @typedef {{ access_token: string, platform_url: string, user_email?: string }} Credentials
19
+ * @typedef {{
20
+ * access_token?: string,
21
+ * access_token_expires_at?: string,
22
+ * refresh_token?: string,
23
+ * organization_id?: number | null,
24
+ * platform_url: string,
25
+ * user_email?: string,
26
+ * legacy_agent_token?: string
27
+ * }} Credentials
20
28
  */
21
29
 
22
30
  function ensureCredentialsDir() {
@@ -53,44 +61,74 @@ function writeJsonFileAtomic(filePath, data) {
53
61
  fs.chmodSync(filePath, 0o600);
54
62
  }
55
63
 
56
- function parseStoredCredentialsData(parsed, { requireAccessToken = false } = {}) {
64
+ function parseStoredCredentialsData(parsed, { requireAuthData = false } = {}) {
57
65
  if (!parsed || typeof parsed !== 'object') return null;
58
66
 
59
- const accessToken = normalizeAgentToken(parsed.access_token);
60
67
  const platformUrl = typeof parsed.platform_url === 'string' ? parsed.platform_url.trim() : '';
61
- if (requireAccessToken && !accessToken) return null;
62
- if (!accessToken && !platformUrl) return null;
68
+ const refreshToken =
69
+ typeof parsed.refresh_token === 'string'
70
+ ? parsed.refresh_token.trim()
71
+ : typeof parsed.supabase_refresh_token === 'string'
72
+ ? parsed.supabase_refresh_token.trim()
73
+ : '';
74
+ const accessToken = typeof parsed.access_token === 'string' ? parsed.access_token.trim() : '';
75
+ const accessTokenExpiresAt =
76
+ typeof parsed.access_token_expires_at === 'string'
77
+ ? parsed.access_token_expires_at.trim()
78
+ : '';
79
+ const organizationId =
80
+ typeof parsed.organization_id === 'number' && Number.isFinite(parsed.organization_id)
81
+ ? parsed.organization_id
82
+ : null;
83
+ const legacyAgentToken = accessToken && !refreshToken ? accessToken : '';
84
+
85
+ if (!platformUrl) return null;
86
+ if (requireAuthData && !refreshToken && !legacyAgentToken) return null;
63
87
 
64
88
  return {
65
- access_token: accessToken,
66
89
  platform_url: platformUrl,
90
+ ...(refreshToken ? { refresh_token: refreshToken } : {}),
91
+ ...(accessToken ? { access_token: accessToken } : {}),
92
+ ...(accessTokenExpiresAt ? { access_token_expires_at: accessTokenExpiresAt } : {}),
93
+ ...(organizationId ? { organization_id: organizationId } : {}),
67
94
  ...(typeof parsed.user_email === 'string' && parsed.user_email.trim()
68
95
  ? { user_email: parsed.user_email.trim() }
69
- : {})
96
+ : {}),
97
+ ...(legacyAgentToken ? { legacy_agent_token: legacyAgentToken } : {})
70
98
  };
71
99
  }
72
100
 
73
101
  function normalizeCredentialsForSave(data) {
74
- const parsed = parseStoredCredentialsData(data, { requireAccessToken: true });
102
+ const parsed = parseStoredCredentialsData(data, { requireAuthData: true });
75
103
  if (!parsed) return null;
76
104
 
77
105
  const platformUrl = normalizePlatformUrl(parsed.platform_url);
78
106
  if (!platformUrl) return null;
79
107
 
80
108
  return {
81
- ...parsed,
82
- platform_url: platformUrl
109
+ platform_url: platformUrl,
110
+ ...(parsed.refresh_token ? { refresh_token: parsed.refresh_token } : {}),
111
+ ...(parsed.access_token ? { access_token: parsed.access_token } : {}),
112
+ ...(parsed.access_token_expires_at
113
+ ? { access_token_expires_at: parsed.access_token_expires_at }
114
+ : {}),
115
+ ...(parsed.organization_id ? { organization_id: parsed.organization_id } : {}),
116
+ ...(parsed.user_email ? { user_email: parsed.user_email } : {})
83
117
  };
84
118
  }
85
119
 
86
120
  /** @returns {Credentials | null} */
87
121
  export function loadCredentials() {
88
- return (
89
- parseStoredCredentialsData(readJsonFile(ELECTRON_CREDENTIALS_FILE), {
90
- requireAccessToken: true
91
- }) ??
92
- parseStoredCredentialsData(readJsonFile(CREDENTIALS_FILE))
93
- );
122
+ const cliCredentials = parseStoredCredentialsData(readJsonFile(CREDENTIALS_FILE), {
123
+ requireAuthData: true
124
+ });
125
+ const electronCredentials = parseStoredCredentialsData(readJsonFile(ELECTRON_CREDENTIALS_FILE), {
126
+ requireAuthData: true
127
+ });
128
+
129
+ if (cliCredentials?.refresh_token) return cliCredentials;
130
+ if (electronCredentials?.refresh_token) return electronCredentials;
131
+ return cliCredentials ?? electronCredentials;
94
132
  }
95
133
 
96
134
  /** @param {Credentials} data */
@@ -100,16 +138,23 @@ export function saveCredentials(data) {
100
138
  throw new Error('Cannot save empty Overlord credentials.');
101
139
  }
102
140
 
103
- writeJsonFileAtomic(CREDENTIALS_FILE, credentials);
141
+ const sharedCredentials = { ...credentials, updated_at: new Date().toISOString() };
142
+ writeJsonFileAtomic(CREDENTIALS_FILE, sharedCredentials);
104
143
 
105
- // `electron-credentials.json` is now the shared desktop/CLI credential record.
106
- // Preserve Electron-only encrypted fields when CLI login refreshes the agent token.
107
144
  const existingElectronCredentials = readJsonFile(ELECTRON_CREDENTIALS_FILE);
108
- const electronPayload =
109
- existingElectronCredentials && typeof existingElectronCredentials === 'object'
110
- ? { ...existingElectronCredentials, ...credentials }
111
- : credentials;
112
- writeJsonFileAtomic(ELECTRON_CREDENTIALS_FILE, electronPayload);
145
+ if (existingElectronCredentials && typeof existingElectronCredentials === 'object') {
146
+ const electronPayload = { ...existingElectronCredentials };
147
+ electronPayload.updated_at = sharedCredentials.updated_at;
148
+ if (credentials.platform_url) electronPayload.platform_url = credentials.platform_url;
149
+ if (credentials.access_token_expires_at) {
150
+ electronPayload.access_token_expires_at = credentials.access_token_expires_at;
151
+ }
152
+ if (credentials.organization_id) electronPayload.organization_id = credentials.organization_id;
153
+ if (credentials.user_email) electronPayload.user_email = credentials.user_email;
154
+ delete electronPayload.legacy_agent_token;
155
+ delete electronPayload.supabase_refresh_token;
156
+ writeJsonFileAtomic(ELECTRON_CREDENTIALS_FILE, electronPayload);
157
+ }
113
158
  }
114
159
 
115
160
  export function clearCredentials() {
@@ -123,13 +168,16 @@ export function clearCredentials() {
123
168
  }
124
169
 
125
170
  function getCredentialFileSource() {
171
+ const cliCredentials = parseStoredCredentialsData(readJsonFile(CREDENTIALS_FILE), {
172
+ requireAuthData: true
173
+ });
126
174
  const electronCredentials = parseStoredCredentialsData(readJsonFile(ELECTRON_CREDENTIALS_FILE), {
127
- requireAccessToken: true
175
+ requireAuthData: true
128
176
  });
129
- if (electronCredentials) return 'electron-credentials.json';
130
-
131
- const cliCredentials = parseStoredCredentialsData(readJsonFile(CREDENTIALS_FILE));
177
+ if (cliCredentials?.refresh_token) return 'credentials.json';
178
+ if (electronCredentials?.refresh_token) return 'electron-credentials.json';
132
179
  if (cliCredentials) return 'credentials.json';
180
+ if (electronCredentials) return 'electron-credentials.json';
133
181
 
134
182
  return 'none';
135
183
  }
@@ -261,6 +309,88 @@ function isRunningPid(pid) {
261
309
  }
262
310
  }
263
311
 
312
+ function decodeJwtExpiry(accessToken) {
313
+ try {
314
+ const payload = JSON.parse(Buffer.from(accessToken.split('.')[1] ?? '', 'base64url').toString('utf8'));
315
+ return typeof payload.exp === 'number' ? payload.exp : null;
316
+ } catch {
317
+ return null;
318
+ }
319
+ }
320
+
321
+ function computeAccessTokenExpiry(data) {
322
+ const expiresIn = Number.parseInt(String(data?.expires_in ?? ''), 10);
323
+ if (Number.isFinite(expiresIn) && expiresIn > 0) {
324
+ return new Date(Date.now() + expiresIn * 1000).toISOString();
325
+ }
326
+
327
+ const jwtExp = typeof data?.access_token === 'string' ? decodeJwtExpiry(data.access_token) : null;
328
+ return jwtExp ? new Date(jwtExp * 1000).toISOString() : null;
329
+ }
330
+
331
+ function resolveAccessTokenExpiry(credentials) {
332
+ if (!credentials?.access_token) return null;
333
+ if (credentials.access_token_expires_at) {
334
+ const parsed = Date.parse(credentials.access_token_expires_at);
335
+ if (Number.isFinite(parsed)) return parsed;
336
+ }
337
+ const jwtExp = decodeJwtExpiry(credentials.access_token);
338
+ return jwtExp ? jwtExp * 1000 : null;
339
+ }
340
+
341
+ function isAccessTokenFresh(credentials) {
342
+ const expiresAt = resolveAccessTokenExpiry(credentials);
343
+ if (expiresAt === null) return false;
344
+ return expiresAt - Date.now() > 60_000;
345
+ }
346
+
347
+ const authConfigCache = new Map();
348
+
349
+ async function fetchAuthConfig(platformUrl, localSecret) {
350
+ if (authConfigCache.has(platformUrl)) return authConfigCache.get(platformUrl);
351
+ const res = await fetch(`${platformUrl}/api/auth/config`, {
352
+ headers: buildAuthHeaders('', localSecret)
353
+ });
354
+ if (!res.ok) {
355
+ throw new Error(`Failed to fetch auth config (${res.status}).`);
356
+ }
357
+ const data = await res.json();
358
+ authConfigCache.set(platformUrl, data);
359
+ return data;
360
+ }
361
+
362
+ async function refreshOAuthAccessToken(platformUrl, refreshToken, localSecret) {
363
+ const config = await fetchAuthConfig(platformUrl, localSecret);
364
+ const clientId = config.cli_client_id ?? config.electron_client_id;
365
+ const supabaseUrl = config.supabase_url;
366
+
367
+ if (!supabaseUrl || !clientId) {
368
+ throw new Error('OAuth is not configured for Overlord CLI auth.');
369
+ }
370
+
371
+ const res = await fetch(`${supabaseUrl}/auth/v1/oauth/token`, {
372
+ method: 'POST',
373
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
374
+ body: new URLSearchParams({
375
+ grant_type: 'refresh_token',
376
+ refresh_token: refreshToken,
377
+ client_id: clientId
378
+ })
379
+ });
380
+
381
+ if (!res.ok) {
382
+ const text = await res.text().catch(() => '');
383
+ throw new Error(`OAuth token refresh failed (${res.status}): ${text}`);
384
+ }
385
+
386
+ const data = await res.json();
387
+ return {
388
+ access_token: data.access_token,
389
+ refresh_token: data.refresh_token,
390
+ access_token_expires_at: computeAccessTokenExpiry(data)
391
+ };
392
+ }
393
+
264
394
  /** @returns {{ platform_url?: string, local_secret?: string } | null} */
265
395
  export function loadRuntime(targetUrl) {
266
396
  if (targetUrl) {
@@ -284,9 +414,10 @@ export function loadRuntime(targetUrl) {
284
414
  /**
285
415
  * @param {string} token
286
416
  * @param {string} [localSecret]
417
+ * @param {number | null | undefined} [organizationId]
287
418
  * @returns {Record<string, string>}
288
419
  */
289
- export function buildAuthHeaders(token, localSecret) {
420
+ export function buildAuthHeaders(token, localSecret, organizationId) {
290
421
  const headers = {};
291
422
 
292
423
  if (token) {
@@ -297,6 +428,10 @@ export function buildAuthHeaders(token, localSecret) {
297
428
  headers[LOCAL_SECRET_HEADER] = localSecret;
298
429
  }
299
430
 
431
+ if (organizationId && Number.isFinite(organizationId)) {
432
+ headers['x-organization-id'] = String(organizationId);
433
+ }
434
+
300
435
  return headers;
301
436
  }
302
437
 
@@ -310,23 +445,17 @@ function isLocalDevCli() {
310
445
  }
311
446
 
312
447
  /**
313
- * Resolve the overlord URL and agent token from credentials file or env vars.
314
- * @returns {{ platformUrl: string, agentToken: string }}
448
+ * Resolve the Overlord auth session from env vars or shared credentials.
449
+ * Refreshes OAuth access tokens when possible.
315
450
  */
316
- export function resolveAuth() {
451
+ export async function resolveAuth() {
317
452
  const creds = loadCredentials();
318
453
  const overlordUrlFromEnv = normalizePlatformUrl(process.env.OVERLORD_URL);
319
454
  const overlordUrlFromCreds = normalizeStoredPlatformUrl(creds?.platform_url);
320
455
 
321
- const runtime = overlordUrlFromEnv && isLocalhostUrl(overlordUrlFromEnv)
322
- ? loadRuntime(overlordUrlFromEnv)
323
- : null;
456
+ const platformUrl = overlordUrlFromEnv ?? overlordUrlFromCreds ?? getDefaultOverlordUrl();
457
+ const runtime = isLocalhostUrl(platformUrl) ? loadRuntime(platformUrl) : null;
324
458
  const runtimeOverlordUrl = runtime?.platform_url;
325
-
326
- const platformUrl =
327
- overlordUrlFromEnv ??
328
- overlordUrlFromCreds ??
329
- getDefaultOverlordUrl();
330
459
  const localSecret =
331
460
  runtime &&
332
461
  runtime.local_secret &&
@@ -336,25 +465,111 @@ export function resolveAuth() {
336
465
  ? runtime.local_secret
337
466
  : '';
338
467
 
468
+ const envAgentToken = normalizeAgentToken(process.env.AGENT_TOKEN);
469
+ if (envAgentToken) {
470
+ return {
471
+ platformUrl,
472
+ bearerToken: envAgentToken,
473
+ localSecret,
474
+ organizationId:
475
+ typeof process.env.OVERLORD_ORGANIZATION_ID === 'string'
476
+ ? Number.parseInt(process.env.OVERLORD_ORGANIZATION_ID, 10)
477
+ : null,
478
+ authMode: 'legacy_agent_token'
479
+ };
480
+ }
481
+
482
+ if (!creds) {
483
+ return {
484
+ platformUrl,
485
+ bearerToken: 'overlord-local-dev-token',
486
+ localSecret,
487
+ organizationId: null,
488
+ authMode: 'local_fallback'
489
+ };
490
+ }
491
+
492
+ if (creds.refresh_token) {
493
+ let nextCredentials = creds;
494
+ if (!isAccessTokenFresh(creds)) {
495
+ try {
496
+ const refreshed = await refreshOAuthAccessToken(platformUrl, creds.refresh_token, localSecret);
497
+ nextCredentials = {
498
+ ...creds,
499
+ access_token: refreshed.access_token,
500
+ access_token_expires_at: refreshed.access_token_expires_at,
501
+ refresh_token: refreshed.refresh_token || creds.refresh_token
502
+ };
503
+ saveCredentials(nextCredentials);
504
+ } catch (refreshError) {
505
+ if (!creds.access_token) throw refreshError;
506
+ // Transient refresh failure — keep the existing access token and let the server
507
+ // reject it if it's truly expired/revoked.
508
+ }
509
+ }
510
+
511
+ if (!Number.isFinite(nextCredentials.organization_id)) {
512
+ throw new Error('Overlord login is missing an organization selection. Run `ovld auth login` again.');
513
+ }
514
+
515
+ if (!nextCredentials.access_token) {
516
+ throw new Error('No OAuth access token is available. Run `ovld auth login` again.');
517
+ }
518
+
519
+ return {
520
+ platformUrl,
521
+ bearerToken: nextCredentials.access_token,
522
+ localSecret,
523
+ organizationId: nextCredentials.organization_id,
524
+ authMode: 'oauth'
525
+ };
526
+ }
527
+
528
+ if (creds.legacy_agent_token) {
529
+ return {
530
+ platformUrl,
531
+ bearerToken: creds.legacy_agent_token,
532
+ localSecret,
533
+ organizationId: creds.organization_id ?? null,
534
+ authMode: 'legacy_agent_token'
535
+ };
536
+ }
537
+
339
538
  return {
340
539
  platformUrl,
341
- agentToken:
342
- normalizeAgentToken(process.env.AGENT_TOKEN) ||
343
- normalizeAgentToken(creds?.access_token) ||
344
- 'overlord-local-dev-token',
345
- localSecret
540
+ bearerToken: 'overlord-local-dev-token',
541
+ localSecret,
542
+ organizationId: null,
543
+ authMode: 'local_fallback'
346
544
  };
347
545
  }
348
546
 
349
- export function getAuthStatus() {
547
+ export async function getAuthStatus() {
350
548
  const creds = loadCredentials();
351
- const resolved = resolveAuth();
549
+ let resolved;
550
+ let error = null;
551
+ try {
552
+ resolved = await resolveAuth();
553
+ } catch (resolveError) {
554
+ error = resolveError instanceof Error ? resolveError.message : String(resolveError);
555
+ resolved = {
556
+ platformUrl:
557
+ normalizePlatformUrl(process.env.OVERLORD_URL) ??
558
+ normalizeStoredPlatformUrl(creds?.platform_url) ??
559
+ getDefaultOverlordUrl(),
560
+ localSecret: '',
561
+ organizationId: creds?.organization_id ?? null,
562
+ authMode: 'error'
563
+ };
564
+ }
352
565
 
353
566
  let tokenSource = 'fallback';
354
567
  if (normalizeAgentToken(process.env.AGENT_TOKEN)) {
355
568
  tokenSource = 'AGENT_TOKEN';
356
- } else if (normalizeAgentToken(creds?.access_token)) {
569
+ } else if (creds?.refresh_token) {
357
570
  tokenSource = getCredentialFileSource();
571
+ } else if (creds?.legacy_agent_token) {
572
+ tokenSource = `${getCredentialFileSource()} (legacy)`;
358
573
  }
359
574
 
360
575
  let platformUrlSource = 'default';
@@ -371,6 +586,9 @@ export function getAuthStatus() {
371
586
  tokenPresent: tokenSource !== 'fallback',
372
587
  tokenSource,
373
588
  hasLocalSecret: Boolean(resolved.localSecret),
589
+ organizationId: resolved.organizationId ?? null,
590
+ authMode: resolved.authMode,
591
+ error,
374
592
  credentialsFileExists: fileExists(CREDENTIALS_FILE),
375
593
  electronCredentialsFileExists: fileExists(ELECTRON_CREDENTIALS_FILE)
376
594
  };
@@ -378,12 +596,12 @@ export function getAuthStatus() {
378
596
 
379
597
  export function repairCredentials() {
380
598
  const creds = loadCredentials();
381
- if (!creds || !normalizeAgentToken(creds.access_token)) {
599
+ if (!creds) {
382
600
  ensureCredentialsDir();
383
601
  return {
384
602
  repaired: false,
385
- reason: 'No valid stored credentials with an access token were found.',
386
- status: getAuthStatus()
603
+ reason: 'No valid stored credentials were found.',
604
+ status: null
387
605
  };
388
606
  }
389
607
 
@@ -391,7 +609,7 @@ export function repairCredentials() {
391
609
 
392
610
  return {
393
611
  repaired: true,
394
- status: getAuthStatus()
612
+ status: null
395
613
  };
396
614
  }
397
615
 
@@ -43,7 +43,7 @@ function getInstructionMode(agent) {
43
43
  return 'legacy';
44
44
  }
45
45
 
46
- async function fetchContext(platformUrl, agentToken, localSecret, ticketId, agent) {
46
+ async function fetchContext(platformUrl, bearerToken, localSecret, organizationId, ticketId, agent) {
47
47
  const params = new URLSearchParams({
48
48
  context: 'cli',
49
49
  agent,
@@ -51,7 +51,7 @@ async function fetchContext(platformUrl, agentToken, localSecret, ticketId, agen
51
51
  });
52
52
  const url = `${platformUrl}/api/protocol/context/${ticketId}?${params.toString()}`;
53
53
  const response = await fetch(url, {
54
- headers: buildAuthHeaders(agentToken, localSecret)
54
+ headers: buildAuthHeaders(bearerToken, localSecret, organizationId)
55
55
  });
56
56
 
57
57
  if (!response.ok) {
@@ -116,8 +116,15 @@ async function runAgent(agent, mode = 'run') {
116
116
  process.exit(1);
117
117
  }
118
118
 
119
- const { platformUrl, agentToken, localSecret } = resolveAuth();
120
- const context = await fetchContext(platformUrl, agentToken, localSecret, ticketId, agent);
119
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
120
+ const context = await fetchContext(
121
+ platformUrl,
122
+ bearerToken,
123
+ localSecret,
124
+ organizationId,
125
+ ticketId,
126
+ agent
127
+ );
121
128
 
122
129
  const childEnv = { ...process.env, AGENT_IDENTIFIER: agentIdentifierMap[agent] };
123
130
 
@@ -199,8 +206,15 @@ async function printContext() {
199
206
  process.exit(1);
200
207
  }
201
208
 
202
- const { platformUrl, agentToken, localSecret } = resolveAuth();
203
- const context = await fetchContext(platformUrl, agentToken, localSecret, ticketId);
209
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
210
+ const context = await fetchContext(
211
+ platformUrl,
212
+ bearerToken,
213
+ localSecret,
214
+ organizationId,
215
+ ticketId,
216
+ 'claude'
217
+ );
204
218
  process.stdout.write(context);
205
219
  }
206
220
 
@@ -131,9 +131,9 @@ async function promptForSelection({ items, label, prompt, renderItem }) {
131
131
  }
132
132
  }
133
133
 
134
- async function fetchProjects(platformUrl, agentToken, localSecret) {
134
+ async function fetchProjects(platformUrl, bearerToken, localSecret, organizationId) {
135
135
  const res = await fetch(`${platformUrl}/api/protocol/projects`, {
136
- headers: buildAuthHeaders(agentToken, localSecret)
136
+ headers: buildAuthHeaders(bearerToken, localSecret, organizationId)
137
137
  });
138
138
 
139
139
  const data = await res.json().catch(() => ({}));
@@ -146,11 +146,11 @@ async function fetchProjects(platformUrl, agentToken, localSecret) {
146
146
  return Array.isArray(data.projects) ? sortProjects(data.projects) : [];
147
147
  }
148
148
 
149
- async function createTicket(platformUrl, agentToken, localSecret, body) {
149
+ async function createTicket(platformUrl, bearerToken, localSecret, organizationId, body) {
150
150
  const res = await fetch(`${platformUrl}/api/protocol/tickets`, {
151
151
  method: 'POST',
152
152
  headers: {
153
- ...buildAuthHeaders(agentToken, localSecret),
153
+ ...buildAuthHeaders(bearerToken, localSecret, organizationId),
154
154
  'Content-Type': 'application/json'
155
155
  },
156
156
  body: JSON.stringify(body)
@@ -224,8 +224,8 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
224
224
  const objective = String(flags.objective ?? positionals.join(' ')).trim();
225
225
  ensureObjective(commandName, objective);
226
226
 
227
- const { platformUrl, agentToken, localSecret } = resolveAuth();
228
- const projects = await fetchProjects(platformUrl, agentToken, localSecret);
227
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
228
+ const projects = await fetchProjects(platformUrl, bearerToken, localSecret, organizationId);
229
229
 
230
230
  if (!projects.length) {
231
231
  throw new Error('No projects available. Create a project first.');
@@ -253,7 +253,7 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
253
253
  const modelIdentifier = resolveTicketCreationModelIdentifier(flags);
254
254
  const ticketDelegate = resolveTicketCreationDelegate(flags, selectedAgent, modelIdentifier);
255
255
 
256
- const ticket = await createTicket(platformUrl, agentToken, localSecret, {
256
+ const ticket = await createTicket(platformUrl, bearerToken, localSecret, organizationId, {
257
257
  objective,
258
258
  title: String(flags.title ?? ''),
259
259
  acceptanceCriteria: String(flags['acceptance-criteria'] ?? ''),
@@ -91,7 +91,15 @@ function resolveProtocolMetadata(flags = {}, base = {}) {
91
91
  */
92
92
  const DEFAULT_TIMEOUT_MS = 30000;
93
93
 
94
- async function apiPost(platformUrl, token, localSecret, path, body, timeoutMs = DEFAULT_TIMEOUT_MS) {
94
+ async function apiPost(
95
+ platformUrl,
96
+ token,
97
+ localSecret,
98
+ organizationId,
99
+ path,
100
+ body,
101
+ timeoutMs = DEFAULT_TIMEOUT_MS
102
+ ) {
95
103
  const requestUrl = `${platformUrl}${path}`;
96
104
  const requestStart = Date.now();
97
105
  let res;
@@ -99,7 +107,7 @@ async function apiPost(platformUrl, token, localSecret, path, body, timeoutMs =
99
107
  res = await fetch(requestUrl, {
100
108
  method: 'POST',
101
109
  headers: {
102
- ...buildAuthHeaders(token, localSecret),
110
+ ...buildAuthHeaders(token, localSecret, organizationId),
103
111
  'Content-Type': 'application/json'
104
112
  },
105
113
  body: JSON.stringify(body),
@@ -150,8 +158,8 @@ async function apiPost(platformUrl, token, localSecret, path, body, timeoutMs =
150
158
  if (res.status === 401) {
151
159
  throw new Error(
152
160
  `Authentication failed (401): ${data.error ?? 'Invalid or missing token.'}\n` +
153
- `IMPORTANT: Stop all work immediately. Your agent token is invalid, expired, or revoked.\n` +
154
- `The user should open Overlord Settings Agent Tokens and retrieve an updated token for this project.\n` +
161
+ `IMPORTANT: Stop all work immediately. Your Overlord auth session is invalid, expired, or missing required scope.\n` +
162
+ `The user should sign in again with Overlord Desktop or \`ovld auth login\`.\n` +
155
163
  `Ask the user if they would like to proceed without submitting updates to Overlord.`
156
164
  );
157
165
  }
@@ -474,7 +482,7 @@ function resolveExternalSessionId(flags) {
474
482
  async function protocolAttach(args) {
475
483
  const flags = parseFlags(args);
476
484
  const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
477
- const { platformUrl, agentToken, localSecret } = resolveAuth();
485
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
478
486
  const timeoutMs = resolveTimeout(flags);
479
487
 
480
488
  const externalSessionId = resolveExternalSessionId(flags);
@@ -489,8 +497,9 @@ async function protocolAttach(args) {
489
497
 
490
498
  const data = await apiPost(
491
499
  platformUrl,
492
- agentToken,
500
+ bearerToken,
493
501
  localSecret,
502
+ organizationId,
494
503
  '/api/protocol/attach',
495
504
  body,
496
505
  timeoutMs
@@ -519,7 +528,7 @@ async function protocolUpdate(args) {
519
528
  ? readTextFile(String(flags['summary-file']), '--summary-file')
520
529
  : requireFlag(flags, 'summary', undefined);
521
530
 
522
- const { platformUrl, agentToken, localSecret } = resolveAuth();
531
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
523
532
  const timeoutMs = resolveTimeout(flags);
524
533
  const changeRationales = await resolveChangeRationales(flags);
525
534
  const externalSessionId = resolveExternalSessionId(flags);
@@ -545,8 +554,9 @@ async function protocolUpdate(args) {
545
554
 
546
555
  const data = await apiPost(
547
556
  platformUrl,
548
- agentToken,
557
+ bearerToken,
549
558
  localSecret,
559
+ organizationId,
550
560
  '/api/protocol/update',
551
561
  body,
552
562
  timeoutMs
@@ -571,7 +581,7 @@ async function protocolRecordChangeRationales(args) {
571
581
  );
572
582
  }
573
583
 
574
- const { platformUrl, agentToken, localSecret } = resolveAuth();
584
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
575
585
  const timeoutMs = resolveTimeout(flags);
576
586
 
577
587
  const body = {
@@ -588,8 +598,9 @@ async function protocolRecordChangeRationales(args) {
588
598
 
589
599
  const data = await apiPost(
590
600
  platformUrl,
591
- agentToken,
601
+ bearerToken,
592
602
  localSecret,
603
+ organizationId,
593
604
  '/api/protocol/change-rationales',
594
605
  body,
595
606
  timeoutMs
@@ -610,7 +621,7 @@ async function protocolAsk(args) {
610
621
  ? readTextFile(String(flags['question-file']), '--question-file')
611
622
  : requireFlag(flags, 'question', undefined);
612
623
 
613
- const { platformUrl, agentToken, localSecret } = resolveAuth();
624
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
614
625
  const timeoutMs = resolveTimeout(flags);
615
626
 
616
627
  const body = {
@@ -621,7 +632,15 @@ async function protocolAsk(args) {
621
632
  ...(flags['payload-json'] ? { payload: parseJsonFlag('--payload-json', flags['payload-json']) } : {})
622
633
  };
623
634
 
624
- const data = await apiPost(platformUrl, agentToken, localSecret, '/api/protocol/ask', body, timeoutMs);
635
+ const data = await apiPost(
636
+ platformUrl,
637
+ bearerToken,
638
+ localSecret,
639
+ organizationId,
640
+ '/api/protocol/ask',
641
+ body,
642
+ timeoutMs
643
+ );
625
644
  console.log(JSON.stringify(data, null, 2));
626
645
  }
627
646
 
@@ -636,13 +655,14 @@ async function protocolPermissionRequest(args) {
636
655
  ? await readJsonFileOrStdin(String(flags['payload-file']), '--payload-file')
637
656
  : {};
638
657
 
639
- const { platformUrl, agentToken, localSecret } = resolveAuth();
658
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
640
659
  const timeoutMs = resolveTimeout(flags);
641
660
 
642
661
  const data = await apiPost(
643
662
  platformUrl,
644
- agentToken,
663
+ bearerToken,
645
664
  localSecret,
665
+ organizationId,
646
666
  `/api/protocol/permission-request?ticketId=${encodeURIComponent(ticketId)}`,
647
667
  payload,
648
668
  timeoutMs
@@ -660,7 +680,7 @@ async function protocolReadContext(args) {
660
680
  if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
661
681
  if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
662
682
 
663
- const { platformUrl, agentToken, localSecret } = resolveAuth();
683
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
664
684
  const timeoutMs = resolveTimeout(flags);
665
685
 
666
686
  const body = {
@@ -672,8 +692,9 @@ async function protocolReadContext(args) {
672
692
 
673
693
  const data = await apiPost(
674
694
  platformUrl,
675
- agentToken,
695
+ bearerToken,
676
696
  localSecret,
697
+ organizationId,
677
698
  '/api/protocol/read-context',
678
699
  body,
679
700
  timeoutMs
@@ -703,7 +724,7 @@ async function protocolWriteContext(args) {
703
724
  value = String(flags.value);
704
725
  }
705
726
 
706
- const { platformUrl, agentToken, localSecret } = resolveAuth();
727
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
707
728
  const timeoutMs = resolveTimeout(flags);
708
729
 
709
730
  const body = {
@@ -716,8 +737,9 @@ async function protocolWriteContext(args) {
716
737
 
717
738
  const data = await apiPost(
718
739
  platformUrl,
719
- agentToken,
740
+ bearerToken,
720
741
  localSecret,
742
+ organizationId,
721
743
  '/api/protocol/write-context',
722
744
  body,
723
745
  timeoutMs
@@ -742,7 +764,7 @@ async function protocolDeliver(args) {
742
764
  ? readTextFile(String(flags['summary-file']), '--summary-file')
743
765
  : requireFlag(flags, 'summary', undefined));
744
766
 
745
- const { platformUrl, agentToken, localSecret } = resolveAuth();
767
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
746
768
  const timeoutMs = resolveTimeout(flags);
747
769
 
748
770
  let artifacts = deliverPayload?.artifacts ?? [];
@@ -775,8 +797,9 @@ async function protocolDeliver(args) {
775
797
 
776
798
  const data = await apiPost(
777
799
  platformUrl,
778
- agentToken,
800
+ bearerToken,
779
801
  localSecret,
802
+ organizationId,
780
803
  '/api/protocol/deliver',
781
804
  body,
782
805
  timeoutMs
@@ -795,7 +818,7 @@ async function protocolArtifactPrepareUpload(args) {
795
818
  if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
796
819
  const fileName = requireFlag(flags, 'file-name', undefined);
797
820
 
798
- const { platformUrl, agentToken, localSecret } = resolveAuth();
821
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
799
822
  const timeoutMs = resolveTimeout(flags);
800
823
 
801
824
  const body = {
@@ -811,8 +834,9 @@ async function protocolArtifactPrepareUpload(args) {
811
834
 
812
835
  const data = await apiPost(
813
836
  platformUrl,
814
- agentToken,
837
+ bearerToken,
815
838
  localSecret,
839
+ organizationId,
816
840
  '/api/protocol/artifacts/prepare-upload',
817
841
  body,
818
842
  timeoutMs
@@ -828,7 +852,7 @@ async function protocolArtifactFinalizeUpload(args) {
828
852
  const storagePath = requireFlag(flags, 'storage-path', undefined);
829
853
  const label = requireFlag(flags, 'label', undefined);
830
854
 
831
- const { platformUrl, agentToken, localSecret } = resolveAuth();
855
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
832
856
  const timeoutMs = resolveTimeout(flags);
833
857
 
834
858
  const body = {
@@ -844,8 +868,9 @@ async function protocolArtifactFinalizeUpload(args) {
844
868
 
845
869
  const data = await apiPost(
846
870
  platformUrl,
847
- agentToken,
871
+ bearerToken,
848
872
  localSecret,
873
+ organizationId,
849
874
  '/api/protocol/artifacts/finalize-upload',
850
875
  body,
851
876
  timeoutMs
@@ -862,7 +887,7 @@ async function protocolArtifactGetDownloadUrl(args) {
862
887
  throw new Error('--artifact-id or --storage-path is required');
863
888
  }
864
889
 
865
- const { platformUrl, agentToken, localSecret } = resolveAuth();
890
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
866
891
  const timeoutMs = resolveTimeout(flags);
867
892
 
868
893
  const body = {
@@ -875,8 +900,9 @@ async function protocolArtifactGetDownloadUrl(args) {
875
900
 
876
901
  const data = await apiPost(
877
902
  platformUrl,
878
- agentToken,
903
+ bearerToken,
879
904
  localSecret,
905
+ organizationId,
880
906
  '/api/protocol/artifacts/get-download-url',
881
907
  body,
882
908
  timeoutMs
@@ -900,13 +926,14 @@ async function protocolArtifactUploadFile(args) {
900
926
  const fileStats = await stat(filePath);
901
927
  const fileBytes = await readFile(filePath);
902
928
 
903
- const { platformUrl, agentToken, localSecret } = resolveAuth();
929
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
904
930
  const timeoutMs = resolveTimeout(flags);
905
931
 
906
932
  const prepared = await apiPost(
907
933
  platformUrl,
908
- agentToken,
934
+ bearerToken,
909
935
  localSecret,
936
+ organizationId,
910
937
  '/api/protocol/artifacts/prepare-upload',
911
938
  {
912
939
  sessionKey,
@@ -931,8 +958,9 @@ async function protocolArtifactUploadFile(args) {
931
958
 
932
959
  const finalized = await apiPost(
933
960
  platformUrl,
934
- agentToken,
961
+ bearerToken,
935
962
  localSecret,
963
+ organizationId,
936
964
  '/api/protocol/artifacts/finalize-upload',
937
965
  {
938
966
  sessionKey,
@@ -956,15 +984,16 @@ async function protocolArtifactUploadFile(args) {
956
984
 
957
985
  async function protocolDiscoverProject(args) {
958
986
  const flags = parseFlags(args);
959
- const { platformUrl, agentToken, localSecret } = resolveAuth();
987
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
960
988
  const timeoutMs = resolveTimeout(flags);
961
989
 
962
990
  const workingDirectory = String(flags['working-directory'] ?? process.cwd());
963
991
 
964
992
  const data = await apiPost(
965
993
  platformUrl,
966
- agentToken,
994
+ bearerToken,
967
995
  localSecret,
996
+ organizationId,
968
997
  '/api/protocol/discover-project',
969
998
  { workingDirectory },
970
999
  timeoutMs
@@ -983,7 +1012,7 @@ async function protocolDiscoverProject(args) {
983
1012
  async function protocolConnect(args) {
984
1013
  const flags = parseFlags(args);
985
1014
  const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
986
- const { platformUrl, agentToken, localSecret } = resolveAuth();
1015
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
987
1016
  const timeoutMs = resolveTimeout(flags);
988
1017
 
989
1018
  const body = {
@@ -995,8 +1024,9 @@ async function protocolConnect(args) {
995
1024
 
996
1025
  const data = await apiPost(
997
1026
  platformUrl,
998
- agentToken,
1027
+ bearerToken,
999
1028
  localSecret,
1029
+ organizationId,
1000
1030
  '/api/protocol/connect',
1001
1031
  body,
1002
1032
  timeoutMs
@@ -1017,15 +1047,16 @@ async function protocolConnect(args) {
1017
1047
  async function protocolLoadContext(args) {
1018
1048
  const flags = parseFlags(args);
1019
1049
  const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
1020
- const { platformUrl, agentToken, localSecret } = resolveAuth();
1050
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
1021
1051
  const timeoutMs = resolveTimeout(flags);
1022
1052
 
1023
1053
  const body = { ticketId };
1024
1054
 
1025
1055
  const data = await apiPost(
1026
1056
  platformUrl,
1027
- agentToken,
1057
+ bearerToken,
1028
1058
  localSecret,
1059
+ organizationId,
1029
1060
  '/api/protocol/load-context',
1030
1061
  body,
1031
1062
  timeoutMs
@@ -1040,7 +1071,7 @@ async function protocolLoadContext(args) {
1040
1071
  async function protocolSpawn(args) {
1041
1072
  const flags = parseFlags(args);
1042
1073
  const objective = requireFlag(flags, 'objective', undefined);
1043
- const { platformUrl, agentToken, localSecret } = resolveAuth();
1074
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
1044
1075
  const timeoutMs = resolveTimeout(flags);
1045
1076
  const agentIdentifier = resolveProtocolAgentIdentifier(flags);
1046
1077
  const modelIdentifier = resolveProtocolModelIdentifier(flags);
@@ -1072,8 +1103,9 @@ async function protocolSpawn(args) {
1072
1103
 
1073
1104
  const data = await apiPost(
1074
1105
  platformUrl,
1075
- agentToken,
1106
+ bearerToken,
1076
1107
  localSecret,
1108
+ organizationId,
1077
1109
  '/api/protocol/spawn',
1078
1110
  body,
1079
1111
  timeoutMs
@@ -1099,7 +1131,7 @@ async function protocolCreateTicket(args) {
1099
1131
  const flags = parseFlags(args);
1100
1132
  const { sessionKey, ticketId } = resolveSessionFlags(flags);
1101
1133
  const objective = requireFlag(flags, 'objective', undefined);
1102
- const { platformUrl, agentToken, localSecret } = resolveAuth();
1134
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
1103
1135
  const timeoutMs = resolveTimeout(flags);
1104
1136
  const agentIdentifier = resolveProtocolAgentIdentifier(flags);
1105
1137
  const modelIdentifier = resolveProtocolModelIdentifier(flags);
@@ -1122,8 +1154,9 @@ async function protocolCreateTicket(args) {
1122
1154
 
1123
1155
  const data = await apiPost(
1124
1156
  platformUrl,
1125
- agentToken,
1157
+ bearerToken,
1126
1158
  localSecret,
1159
+ organizationId,
1127
1160
  '/api/protocol/create-ticket',
1128
1161
  body,
1129
1162
  timeoutMs
@@ -1159,8 +1192,9 @@ async function protocolCreateTicket(args) {
1159
1192
 
1160
1193
  const data = await apiPost(
1161
1194
  platformUrl,
1162
- agentToken,
1195
+ bearerToken,
1163
1196
  localSecret,
1197
+ organizationId,
1164
1198
  '/api/protocol/tickets',
1165
1199
  standaloneBody,
1166
1200
  timeoutMs
@@ -1173,7 +1207,7 @@ async function protocolCreateTicket(args) {
1173
1207
  // ---------------------------------------------------------------------------
1174
1208
 
1175
1209
  async function protocolAuthStatus() {
1176
- const status = getAuthStatus();
1210
+ const status = await getAuthStatus();
1177
1211
 
1178
1212
  console.log(
1179
1213
  JSON.stringify(
@@ -1185,6 +1219,9 @@ async function protocolAuthStatus() {
1185
1219
  platformUrlSource: status.platformUrlSource,
1186
1220
  tokenSource: status.tokenSource,
1187
1221
  tokenPresent: status.tokenPresent,
1222
+ organizationId: status.organizationId,
1223
+ authMode: status.authMode,
1224
+ error: status.error,
1188
1225
  hasLocalSecret: status.hasLocalSecret,
1189
1226
  credentialsFileExists: status.credentialsFileExists,
1190
1227
  electronCredentialsFileExists: status.electronCredentialsFileExists
@@ -1243,7 +1280,7 @@ Subcommands:
1243
1280
  Environment fallback:
1244
1281
  --session-key <- SESSION_KEY
1245
1282
  --ticket-id <- TICKET_ID
1246
- auth/host <- OVERLORD_URL, AGENT_TOKEN, or shared credentials from ovld auth/Desktop login
1283
+ auth/host <- OVERLORD_URL, optional legacy AGENT_TOKEN, or shared OAuth credentials from ovld auth/Desktop login
1247
1284
  --timeout <- OVERLORD_TIMEOUT
1248
1285
 
1249
1286
  Common flags:
@@ -15,11 +15,11 @@ export async function ticketContext(ticketId) {
15
15
  process.exit(1);
16
16
  }
17
17
 
18
- const { platformUrl, agentToken, localSecret } = resolveAuth();
18
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
19
19
 
20
20
  const url = `${platformUrl}/api/protocol/context/${ticketId}`;
21
21
  const res = await fetch(url, {
22
- headers: buildAuthHeaders(agentToken, localSecret)
22
+ headers: buildAuthHeaders(bearerToken, localSecret, organizationId)
23
23
  });
24
24
 
25
25
  if (!res.ok) {
@@ -33,11 +33,11 @@ function parseFlags(args, knownFlags) {
33
33
  return result;
34
34
  }
35
35
 
36
- async function apiPost(url, token, localSecret, body) {
36
+ async function apiPost(url, token, localSecret, organizationId, body) {
37
37
  const res = await fetch(url, {
38
38
  method: 'POST',
39
39
  headers: {
40
- ...buildAuthHeaders(token, localSecret),
40
+ ...buildAuthHeaders(token, localSecret, organizationId),
41
41
  'Content-Type': 'application/json'
42
42
  },
43
43
  body: JSON.stringify(body)
@@ -59,7 +59,7 @@ export async function ticketsCreate(args) {
59
59
  export async function ticketsList(args) {
60
60
  const flags = parseFlags(args, ['status', 'include-completed']);
61
61
 
62
- const { platformUrl, agentToken, localSecret } = resolveAuth();
62
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
63
63
 
64
64
  const body = {
65
65
  includeCompleted: flags['include-completed'] !== false,
@@ -68,8 +68,9 @@ export async function ticketsList(args) {
68
68
 
69
69
  const data = await apiPost(
70
70
  `${platformUrl}/api/protocol/list-tickets`,
71
- agentToken,
71
+ bearerToken,
72
72
  localSecret,
73
+ organizationId,
73
74
  body
74
75
  );
75
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "4.17.0",
3
+ "version": "4.19.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {