overlord-cli 0.2605291046.0 → 0.2605300746.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/bin/_cli/auth.mjs CHANGED
@@ -500,7 +500,27 @@ function parseOrganizationFlag(args) {
500
500
  return parsed;
501
501
  }
502
502
 
503
+ function parseTokenFlag(args) {
504
+ const index = args.findIndex(arg => arg === '--token' || arg.startsWith('--token='));
505
+ if (index === -1) return null;
506
+ const raw = args[index].includes('=') ? args[index].split('=')[1] : args[index + 1];
507
+ if (!raw || typeof raw !== 'string') {
508
+ throw new Error('--token requires a value (e.g. --token oat_…)');
509
+ }
510
+ const trimmed = raw.trim();
511
+ if (!trimmed.startsWith('oat_')) {
512
+ throw new Error('--token value must be an agent token starting with oat_');
513
+ }
514
+ return trimmed;
515
+ }
516
+
503
517
  export async function authLogin(args = []) {
518
+ const agentToken = parseTokenFlag(args);
519
+
520
+ if (agentToken) {
521
+ return authLoginWithAgentToken(agentToken, args);
522
+ }
523
+
504
524
  const preselectedOrganizationId = parseOrganizationFlag(args);
505
525
  const storedCredentials = loadCredentials();
506
526
  const platformUrl = resolveLoginPlatformUrl(null, storedCredentials?.platform_url ?? null);
@@ -558,6 +578,27 @@ export async function authLogin(args = []) {
558
578
  }
559
579
  }
560
580
 
581
+ async function authLoginWithAgentToken(agentToken, args) {
582
+ const preselectedOrganizationId = parseOrganizationFlag(args);
583
+ const storedCredentials = loadCredentials();
584
+ const platformUrl = resolveLoginPlatformUrl(null, storedCredentials?.platform_url ?? null);
585
+
586
+ saveCredentials({
587
+ agent_token: agentToken,
588
+ platform_url: platformUrl,
589
+ ...(preselectedOrganizationId !== null ? { organization_id: preselectedOrganizationId } : {})
590
+ });
591
+
592
+ console.log(`Agent token saved. The CLI will use this token for all protocol commands.`);
593
+ if (preselectedOrganizationId !== null) {
594
+ console.log(` Default organization: ${preselectedOrganizationId}`);
595
+ } else {
596
+ console.log(` Organization will be derived from your token's membership.`);
597
+ }
598
+ console.log(` Platform URL: ${platformUrl}`);
599
+ console.log(`\nTo remove: ovld auth logout`);
600
+ }
601
+
561
602
  async function printVerboseAuthStatus() {
562
603
  const status = await getAuthStatus();
563
604
  if (!status.isLoggedIn) {
@@ -638,6 +679,8 @@ export async function runAuthCommand(subcommand, args = []) {
638
679
 
639
680
  Subcommands:
640
681
  login Authorize the CLI via browser (works locally or over SSH)
682
+ --token <oat_…> Persist an agent token from Settings → Agents & MCP.
683
+ Skips the browser flow; token never expires.
641
684
  --organization-id <id> Optional default organization override
642
685
  (ticket-scoped commands infer organization
643
686
  from ticket ids such as 1:899)
@@ -79,19 +79,21 @@ function parseStoredCredentialsData(parsed, { requireAuthData = false } = {}) {
79
79
  typeof parsed.access_token_expires_at === 'string'
80
80
  ? parsed.access_token_expires_at.trim()
81
81
  : '';
82
+ const agentToken = normalizeAgentToken(parsed.agent_token);
82
83
  const organizationId =
83
84
  typeof parsed.organization_id === 'number' && Number.isFinite(parsed.organization_id)
84
85
  ? parsed.organization_id
85
86
  : null;
86
87
 
87
88
  if (!platformUrl) return null;
88
- if (requireAuthData && !refreshToken) return null;
89
+ if (requireAuthData && !refreshToken && !agentToken) return null;
89
90
 
90
91
  return {
91
92
  platform_url: platformUrl,
92
93
  ...(refreshToken ? { refresh_token: refreshToken } : {}),
93
94
  ...(accessToken ? { access_token: accessToken } : {}),
94
95
  ...(accessTokenExpiresAt ? { access_token_expires_at: accessTokenExpiresAt } : {}),
96
+ ...(agentToken ? { agent_token: agentToken } : {}),
95
97
  ...(organizationId ? { organization_id: organizationId } : {}),
96
98
  ...(typeof parsed.user_email === 'string' && parsed.user_email.trim()
97
99
  ? { user_email: parsed.user_email.trim() }
@@ -116,6 +118,7 @@ function normalizeCredentialsForSave(data) {
116
118
  ...(parsed.access_token_expires_at
117
119
  ? { access_token_expires_at: parsed.access_token_expires_at }
118
120
  : {}),
121
+ ...(parsed.agent_token ? { agent_token: parsed.agent_token } : {}),
119
122
  ...(parsed.organization_id ? { organization_id: parsed.organization_id } : {}),
120
123
  ...(parsed.user_email ? { user_email: parsed.user_email } : {}),
121
124
  ...(parsed.updated_at ? { updated_at: parsed.updated_at } : {})
@@ -181,10 +184,13 @@ function selectStoredCredentials() {
181
184
  requireAuthData: true
182
185
  })
183
186
  }
184
- ].filter(candidate => candidate.credentials?.refresh_token);
187
+ ].filter(candidate => candidate.credentials?.refresh_token || candidate.credentials?.agent_token);
185
188
 
186
189
  if (candidates.length === 0) return null;
187
190
 
191
+ const agentTokenCandidate = candidates.find(c => c.credentials?.agent_token);
192
+ if (agentTokenCandidate) return agentTokenCandidate;
193
+
188
194
  const fresh = candidates.filter(candidate => isAccessTokenFresh(candidate.credentials));
189
195
  const pool = fresh.length > 0 ? fresh : candidates;
190
196
  return pool.sort(
@@ -551,6 +557,27 @@ export async function resolveAuth(options = {}) {
551
557
  ? runtime.local_secret
552
558
  : '';
553
559
 
560
+ const envAgentToken = normalizeAgentToken(process.env.OVERLORD_AGENT_TOKEN);
561
+ if (envAgentToken) {
562
+ const envOrganizationId =
563
+ typeof process.env.OVERLORD_ORGANIZATION_ID === 'string'
564
+ ? Number.parseInt(process.env.OVERLORD_ORGANIZATION_ID, 10)
565
+ : null;
566
+ const resolvedOrganizationId =
567
+ organizationIdHint ?? (Number.isFinite(envOrganizationId) ? envOrganizationId : null);
568
+
569
+ // Agent tokens are long-lived and never need refreshing. Organization is
570
+ // optional — the platform derives it from the token owner's membership when
571
+ // no hint is supplied, so a headless CLI keeps working without Desktop.
572
+ return {
573
+ platformUrl,
574
+ bearerToken: envAgentToken,
575
+ localSecret,
576
+ organizationId: resolvedOrganizationId,
577
+ authMode: 'agent_token'
578
+ };
579
+ }
580
+
554
581
  const envAccessToken = normalizeAccessToken(process.env.OVERLORD_ACCESS_TOKEN);
555
582
  if (envAccessToken) {
556
583
  const envOrganizationId =
@@ -575,6 +602,16 @@ export async function resolveAuth(options = {}) {
575
602
  }
576
603
  }
577
604
 
605
+ if (creds?.agent_token) {
606
+ return {
607
+ platformUrl,
608
+ bearerToken: creds.agent_token,
609
+ localSecret,
610
+ organizationId: organizationIdHint ?? creds.organization_id ?? null,
611
+ authMode: 'agent_token'
612
+ };
613
+ }
614
+
578
615
  if (!creds) {
579
616
  return {
580
617
  platformUrl,
@@ -649,10 +686,15 @@ export async function getAuthStatus() {
649
686
  };
650
687
  }
651
688
 
689
+ const envAgentToken = normalizeAgentToken(process.env.OVERLORD_AGENT_TOKEN);
652
690
  const envAccessToken = normalizeAccessToken(process.env.OVERLORD_ACCESS_TOKEN);
653
691
  let tokenSource = 'fallback';
654
- if (envAccessToken && isEnvAccessTokenUsable(envAccessToken)) {
692
+ if (envAgentToken) {
693
+ tokenSource = 'OVERLORD_AGENT_TOKEN';
694
+ } else if (envAccessToken && isEnvAccessTokenUsable(envAccessToken)) {
655
695
  tokenSource = 'OVERLORD_ACCESS_TOKEN';
696
+ } else if (creds?.agent_token) {
697
+ tokenSource = getCredentialFileSource() + ' (agent token)';
656
698
  } else if (creds?.refresh_token) {
657
699
  tokenSource = getCredentialFileSource();
658
700
  }
@@ -704,6 +746,14 @@ function normalizeAccessToken(value) {
704
746
  return value.trim();
705
747
  }
706
748
 
749
+ const AGENT_TOKEN_PREFIX = 'oat_';
750
+
751
+ function normalizeAgentToken(value) {
752
+ if (typeof value !== 'string') return '';
753
+ const trimmed = value.trim();
754
+ return trimmed.startsWith(AGENT_TOKEN_PREFIX) ? trimmed : '';
755
+ }
756
+
707
757
  function isEnvAccessTokenUsable(accessToken) {
708
758
  const expiresAt = decodeJwtExpiry(accessToken);
709
759
  return expiresAt === null || expiresAt * 1000 - Date.now() > 60_000;
@@ -216,7 +216,7 @@ Overlord flags:
216
216
  --model <identifier> Preferred model (sets the ticket delegate and is forwarded to the agent)
217
217
  --thinking <level> Agent reasoning/effort level
218
218
  --flag <value> Extra agent flag (repeatable)
219
- --pre-command <command> Command to run before the agent binary (e.g. ollama)
219
+ --pre-command <command> Shell command to run before the agent binary (e.g. ollama, agent-pod)
220
220
  --project-id <uuid> Target project (skips working-directory resolution)
221
221
  --personal Create the ticket in your personal project
222
222
  --for-human Create a human-review ticket
@@ -57,7 +57,7 @@ Agents:
57
57
  Key protocol commands: auth-status, discover-project, create, prompt, attach, connect, load-context.
58
58
 
59
59
  Auth:
60
- ${primaryCommand} auth login Authorize CLI via browser
60
+ ${primaryCommand} auth login Authorize CLI via browser or --token <oat_…>
61
61
  ${primaryCommand} auth status Show login status
62
62
  ${primaryCommand} auth repair Repair shared Desktop/CLI credentials
63
63
  ${primaryCommand} auth logout Remove stored credentials
@@ -137,24 +137,53 @@ const supportedAgents = ['claude', 'codex', 'cursor', 'antigravity', 'opencode',
137
137
  // Re-exported under clearer names for the direct-launch dispatcher in index.mjs.
138
138
  export { supportedAgents as BUILTIN_LAUNCH_AGENTS, agentIdentifierMap };
139
139
 
140
+ /** @internal Test-only overrides for launcher subprocess calls. */
141
+ export const launcherTestHooks = {
142
+ execFileSync: null,
143
+ shell: null,
144
+ platform: null
145
+ };
146
+
140
147
  function shellQuote(value) {
141
148
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
142
149
  }
143
150
 
151
+ function resolveLauncherShell() {
152
+ const platform = launcherTestHooks.platform ?? process.platform;
153
+ if (platform === 'win32') {
154
+ return null;
155
+ }
156
+ return launcherTestHooks.shell ?? process.env.SHELL ?? 'sh';
157
+ }
158
+
144
159
  /**
145
160
  * Run an agent binary, optionally routed through a user-defined pre-command
146
- * (e.g. `ollama` to run claude-code through Ollama). When a pre-command is set,
147
- * its first token becomes the executable and the agent binary + args follow it:
148
- * preCommand="ollama" → ollama claude <args...>
161
+ * (e.g. `ollama` to run claude-code through Ollama). On POSIX shells, execute
162
+ * the wrapper through the user's interactive login shell so aliases, functions,
163
+ * and PATH customizations are available:
164
+ * preCommand="agent-pod" → $SHELL -ilc 'agent-pod codex <args...>'
165
+ *
166
+ * The shell must be interactive (`-i`), not just a login shell (`-l`): zsh only
167
+ * sources ~/.zshrc — and bash only sources ~/.bashrc — for interactive shells.
168
+ * Wrappers like agent-pod are commonly installed as a shell alias in ~/.zshrc
169
+ * (its documented default), so a plain `-lc` login shell never sees them and
170
+ * fails with `command not found`.
149
171
  */
150
172
  function execAgentBinary(binary, args, opts, preCommand) {
173
+ const exec = launcherTestHooks.execFileSync ?? execFileSync;
151
174
  const pre = typeof preCommand === 'string' ? preCommand.trim() : '';
152
175
  if (pre) {
176
+ const shell = resolveLauncherShell();
177
+ if (shell) {
178
+ const command = [pre, shellQuote(binary), ...args.map(arg => shellQuote(arg))].join(' ');
179
+ exec(shell, ['-ilc', command], opts);
180
+ return;
181
+ }
153
182
  const preTokens = pre.split(/\s+/).filter(Boolean);
154
- execFileSync(preTokens[0], [...preTokens.slice(1), binary, ...args], opts);
183
+ exec(preTokens[0], [...preTokens.slice(1), binary, ...args], opts);
155
184
  return;
156
185
  }
157
- execFileSync(binary, args, opts);
186
+ exec(binary, args, opts);
158
187
  }
159
188
 
160
189
  function toTomlString(value) {
@@ -292,7 +321,7 @@ Options:
292
321
  --model <identifier> Preferred model identifier
293
322
  --thinking <level> Agent reasoning/effort level
294
323
  --flag <value> Extra agent flag (repeatable)
295
- --pre-command <command> Command to run before the agent binary (e.g. ollama)
324
+ --pre-command <command> Shell command to run before the agent binary (e.g. ollama, agent-pod)
296
325
  --ssh-command <command> Launch remotely over SSH by running ovld on the target host
297
326
  --remote-working-directory <path> Change to this path on the remote host before launch
298
327
  --server-multiplexer <none|tmux> Wrap remote launches in tmux
@@ -2614,7 +2614,7 @@ Subcommands:
2614
2614
  Environment fallback:
2615
2615
  --session-key <- SESSION_KEY or auto-persisted session from last attach/connect/prompt in this working directory
2616
2616
  --ticket-id <- TICKET_ID or auto-persisted session (human-readable ticket_id, e.g. 1:899)
2617
- auth/host <- OVERLORD_URL, optional OVERLORD_ACCESS_TOKEN + OVERLORD_ORGANIZATION_ID, or shared OAuth credentials from ovld auth/Desktop login
2617
+ auth/host <- OVERLORD_URL, OVERLORD_AGENT_TOKEN (durable; best for headless/CI), optional OVERLORD_ACCESS_TOKEN + OVERLORD_ORGANIZATION_ID, or shared OAuth credentials from ovld auth/Desktop login
2618
2618
  --timeout <- OVERLORD_TIMEOUT
2619
2619
 
2620
2620
  Common flags:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2605291046.0",
3
+ "version": "0.2605300746.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "homepage": "https://www.ovld.ai/docs/",
6
6
  "type": "module",