overlord-cli 0.2606081020.0 → 0.2606081442.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
@@ -38,15 +38,36 @@ overlord help
38
38
  ### Auth
39
39
 
40
40
  ```bash
41
+ # No account yet? Create one entirely from the terminal with just an email.
42
+ ovld auth signup --email agent@example.com --name "Build Agent" # emails an 8-digit code, then mints a durable oat_ agent token by default
43
+ ovld onboard --email agent@example.com --project-name Repo # same signup, plus org/project/cwd setup
44
+
41
45
  # Repair shared credentials first when a session already exists
42
46
  ovld auth repair # mirror and chmod shared Desktop/CLI credentials when possible
43
47
 
44
48
  # Login to Overlord if repair does not restore access
45
49
  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.
50
+ ovld auth login --email agent@example.com # log back in after logout with an emailed code (no browser)
46
51
  ovld auth status # show current login status (use --verbose for redacted diagnostics)
47
52
  ovld auth logout # remove stored credentials
48
53
  ```
49
54
 
55
+ #### Terminal-native account creation
56
+
57
+ `ovld auth signup` creates an Overlord account without a browser: it emails an
58
+ 8-digit confirmation code, you enter it in the terminal, and the CLI stores valid
59
+ credentials. Pass `--password` to set a password-manager-generated password;
60
+ without one, signup is passwordless and you log back in later with
61
+ `ovld auth login --email`. By default a durable `oat_…` agent token is minted and
62
+ saved for headless use — pass `--no-agent-token` to skip it. For agents that read
63
+ email out of band, the split flow is available:
64
+
65
+ ```bash
66
+ ovld auth signup request --email agent@example.com --name "Build Agent" --json
67
+ ovld auth signup verify --email agent@example.com --code 12345678 --json
68
+ ovld onboard --use-current-auth --project-name Repo --directory "$PWD" --yes
69
+ ```
70
+
50
71
  Desktop-installed wrappers default `OVERLORD_URL` to `https://www.ovld.ai` unless you override it explicitly.
51
72
  For local dev against the web app on port 3000, export the override before running auth or protocol commands:
52
73
 
package/bin/_cli/auth.mjs CHANGED
@@ -726,7 +726,14 @@ export async function runAuthCommand(subcommand, args = []) {
726
726
  console.log(`ovld auth <subcommand>
727
727
 
728
728
  Subcommands:
729
+ signup Create a new Overlord account from the terminal (email + confirmation code)
730
+ --email <email> Email to create the account with.
731
+ --name <name> Display name.
732
+ --password <password> Optional password-manager password.
733
+ --no-agent-token Skip minting a durable oat_ token.
734
+ Run \`ovld auth signup help\` for request/verify split flow.
729
735
  login Authorize the CLI via browser (works locally or over SSH)
736
+ --email <email> Log in with an emailed code instead of a browser.
730
737
  --token <oat_…> Persist an agent token from Settings → Agents & MCP.
731
738
  Skips the browser flow; token never expires.
732
739
  --organization-id <id> Optional. Validated against your membership but
@@ -740,7 +747,29 @@ Subcommands:
740
747
  return;
741
748
  }
742
749
 
750
+ if (subcommand === 'signup') {
751
+ const { runAuthSignupCommand } = await import('./signup.mjs');
752
+ await runAuthSignupCommand(args);
753
+ return;
754
+ }
755
+
743
756
  if (subcommand === 'login') {
757
+ const [maybeSub, ...rest] = args;
758
+ if (maybeSub === 'request') {
759
+ const { runEmailLoginRequest } = await import('./signup.mjs');
760
+ await runEmailLoginRequest(rest);
761
+ return;
762
+ }
763
+ if (maybeSub === 'verify') {
764
+ const { runEmailLoginVerify } = await import('./signup.mjs');
765
+ await runEmailLoginVerify(rest);
766
+ return;
767
+ }
768
+ if (args.some(arg => arg === '--email' || arg.startsWith('--email='))) {
769
+ const { runEmailLogin } = await import('./signup.mjs');
770
+ await runEmailLogin(args);
771
+ return;
772
+ }
744
773
  await authLogin(args);
745
774
  return;
746
775
  }
@@ -785,6 +785,57 @@ export async function searchTicketsAcrossOrganizations(auth, body, options = {})
785
785
  return { tickets, count: tickets.length };
786
786
  }
787
787
 
788
+ /**
789
+ * Resolve a fresh Supabase access token (JWT) from stored credentials, refreshing
790
+ * via the stored refresh token when the cached access token is stale.
791
+ *
792
+ * Unlike {@link resolveAuth}, this never returns an `oat_…` agent token: it is for
793
+ * callers that must present a real Supabase user JWT (e.g. `/api/auth/cli-onboarding`
794
+ * and `/api/auth/agent-token`, which validate the session with `auth.getUser`).
795
+ *
796
+ * @returns {Promise<{ accessToken: string, platformUrl: string, localSecret: string }>}
797
+ */
798
+ export async function resolveStoredSupabaseAccessToken() {
799
+ const selected = selectStoredCredentials();
800
+ const creds = selected?.credentials ?? migrateLegacyCredentials();
801
+ const platformUrl =
802
+ normalizePlatformUrl(process.env.OVERLORD_URL) ??
803
+ normalizeStoredPlatformUrl(creds?.platform_url) ??
804
+ getDefaultOverlordUrl();
805
+ const runtime = isLocalhostUrl(platformUrl) ? loadRuntime(platformUrl) : null;
806
+ const localSecret =
807
+ runtime?.local_secret && runtime.platform_url === platformUrl ? runtime.local_secret : '';
808
+
809
+ if (!creds) {
810
+ throw new Error('No stored Overlord credentials. Run `ovld auth signup` or `ovld auth login` first.');
811
+ }
812
+
813
+ if (creds.access_token && isAccessTokenFresh(creds)) {
814
+ return { accessToken: creds.access_token, platformUrl, localSecret };
815
+ }
816
+
817
+ if (creds.refresh_token) {
818
+ const refreshed = await refreshOAuthAccessToken(platformUrl, creds.refresh_token, localSecret);
819
+ if (!refreshed.access_token) {
820
+ throw new Error('Failed to refresh the stored Supabase session. Run `ovld auth login --email` again.');
821
+ }
822
+ saveCredentialsToSource(
823
+ {
824
+ ...creds,
825
+ access_token: refreshed.access_token,
826
+ access_token_expires_at: refreshed.access_token_expires_at,
827
+ refresh_token: refreshed.refresh_token || creds.refresh_token
828
+ },
829
+ selected?.source
830
+ );
831
+ return { accessToken: refreshed.access_token, platformUrl, localSecret };
832
+ }
833
+
834
+ throw new Error(
835
+ 'Stored credentials have no Supabase session. Run `ovld auth login --email` to obtain one.'
836
+ );
837
+ }
838
+
788
839
  export async function getAuthStatus() {
789
840
  const creds = loadCredentials();
790
841
  let resolved;
@@ -59,7 +59,8 @@ Agents:
59
59
  Key protocol commands: auth-status, discover-project, create, prompt, attach, connect, load-context, heartbeat.
60
60
 
61
61
  Auth:
62
- ${primaryCommand} auth login Authorize CLI via browser or --token <oat_…>
62
+ ${primaryCommand} auth signup Create an account from the terminal (--email <email>)
63
+ ${primaryCommand} auth login Authorize CLI via browser, --email <email>, or --token <oat_…>
63
64
  ${primaryCommand} auth status Show login status
64
65
  ${primaryCommand} auth repair Repair shared Desktop/CLI credentials
65
66
  ${primaryCommand} auth logout Remove stored credentials
@@ -11,6 +11,7 @@ import {
11
11
  buildAuthHeaders,
12
12
  loadCredentials,
13
13
  loadRuntime,
14
+ resolveStoredSupabaseAccessToken,
14
15
  saveCredentials
15
16
  } from './credentials.mjs';
16
17
  import { upsertLocalOverlordConfig } from './local-config.mjs';
@@ -150,18 +151,87 @@ function buildWebProjectUrl(platformUrl, result) {
150
151
  return `${platformUrl}/projects/${projectId}`;
151
152
  }
152
153
 
154
+ /**
155
+ * Resolve the credentials onboarding will use, based on the chosen auth mode:
156
+ * - `--use-current-auth`: reuse the already signed-in Supabase session.
157
+ * - `--email`: create the account entirely in the terminal (email + code).
158
+ * - default: browser device-authorization flow.
159
+ *
160
+ * Returns the Supabase access token to call `cli-onboarding` with, the resolved
161
+ * platform URL and local secret, and (only for the browser flow) the freshly
162
+ * issued session that still needs persisting.
163
+ */
164
+ async function resolveOnboardingAuth({ flags, name, inviteToken }) {
165
+ const useCurrentAuth = flags['use-current-auth'] === true || flags['use-current-auth'] === 'true';
166
+ const email = trimFlag(flags, 'email');
167
+
168
+ if (useCurrentAuth) {
169
+ const { accessToken, platformUrl, localSecret } = await resolveStoredSupabaseAccessToken();
170
+ return { accessToken, platformUrl, localSecret, sessionToPersist: null };
171
+ }
172
+
173
+ if (email) {
174
+ const { runCliSignupFlow } = await import('./signup.mjs');
175
+ const { session, platformUrl, localSecret } = await runCliSignupFlow({
176
+ email,
177
+ name,
178
+ password: trimFlag(flags, 'password') || undefined,
179
+ invite: inviteToken || undefined,
180
+ 'no-agent-token': flags['no-agent-token'] === true
181
+ });
182
+ return {
183
+ accessToken: session.access_token,
184
+ platformUrl: session.platform_url ?? platformUrl,
185
+ localSecret,
186
+ sessionToPersist: null
187
+ };
188
+ }
189
+
190
+ const storedCredentials = loadCredentials();
191
+ const platformUrl = resolveLoginPlatformUrl(null, storedCredentials?.platform_url ?? null);
192
+ const runtime = loadRuntime(platformUrl);
193
+ const localSecret = runtime?.local_secret ?? process.env.OVERLORD_LOCAL_SECRET ?? '';
194
+
195
+ console.log('\nStarting Overlord onboarding in your browser.');
196
+ console.log('Create an account or sign in, then approve the CLI authorization request.\n');
197
+
198
+ const credentials = await authLoginViaDeviceFlow(platformUrl, localSecret, {
199
+ browserOpener: verificationUri => {
200
+ const signupUrl = buildSignupUrl(platformUrl, verificationUri, name, inviteToken);
201
+ console.log(` Signup URL: ${signupUrl}`);
202
+ openBrowser(signupUrl);
203
+ }
204
+ });
205
+
206
+ return {
207
+ accessToken: credentials.access_token,
208
+ platformUrl: credentials.platform_url ?? platformUrl,
209
+ localSecret,
210
+ sessionToPersist: credentials
211
+ };
212
+ }
213
+
153
214
  export async function runOnboardCommand(args) {
154
215
  const flags = parseFlags(args);
155
216
 
156
217
  if (flags.help === true || flags.h === true || args[0] === 'help') {
157
- console.log(`Usage: ovld onboard [--name <name>] [--organization-name <name>] [--project-name <name>] [--directory <path>] [--invite <token|url>] [--yes] [--no-desktop]
218
+ console.log(`Usage: ovld onboard [--name <name>] [--organization-name <name>] [--project-name <name>] [--directory <path>] [--invite <token|url>] [--email <email>] [--password <password>] [--use-current-auth] [--yes] [--no-desktop]
158
219
 
159
220
  Creates an Overlord account setup from the terminal:
160
221
  1. collects your name, organization, project, and repository directory
161
- 2. opens browser signup/login and authorizes the CLI
222
+ 2. authorizes the CLI (browser by default; terminal-only with --email)
162
223
  3. creates the organization/project, links this directory, and creates the first onboarding ticket
163
224
  4. opens the Desktop download page, or the web project when --no-desktop is used
164
225
 
226
+ Authorization modes:
227
+ (default) Browser signup/login + device authorization.
228
+ --email <email> Create the account entirely in the terminal: an 8-digit
229
+ confirmation code is emailed and entered here. Add
230
+ --password <password> to set a password-manager password;
231
+ otherwise future login uses \`ovld auth login --email\`.
232
+ --use-current-auth Skip auth and provision using the already signed-in session
233
+ (e.g. right after \`ovld auth signup\`).
234
+
165
235
  With --invite <token>, joins the inviting organization (from an invite email)
166
236
  with the invited role instead of creating a new organization; the organization
167
237
  prompt is skipped. Accepts a bare token or a full /invite/<token> URL.
@@ -190,42 +260,43 @@ Run this from the repository you want Overlord agents to work in.`);
190
260
  throw new Error('Organization name is required (or pass --invite <token> to join an org).');
191
261
  }
192
262
 
193
- const storedCredentials = loadCredentials();
194
- const platformUrl = resolveLoginPlatformUrl(null, storedCredentials?.platform_url ?? null);
195
- const runtime = loadRuntime(platformUrl);
196
- const localSecret = runtime?.local_secret ?? process.env.OVERLORD_LOCAL_SECRET ?? '';
197
-
198
- console.log('\nStarting Overlord onboarding in your browser.');
199
- console.log('Create an account or sign in, then approve the CLI authorization request.\n');
200
-
201
- const credentials = await authLoginViaDeviceFlow(platformUrl, localSecret, {
202
- browserOpener: verificationUri => {
203
- const signupUrl = buildSignupUrl(platformUrl, verificationUri, name, inviteToken);
204
- console.log(` Signup URL: ${signupUrl}`);
205
- openBrowser(signupUrl);
206
- }
207
- });
263
+ const {
264
+ accessToken,
265
+ platformUrl: resolvedPlatformUrl,
266
+ localSecret,
267
+ sessionToPersist
268
+ } = await resolveOnboardingAuth({ flags, name, inviteToken });
208
269
 
209
- const resolvedPlatformUrl = credentials.platform_url ?? platformUrl;
210
270
  const deviceFingerprint = readOrCreateDeviceFingerprint(flags);
211
- const result = await completeCliOnboarding(resolvedPlatformUrl, localSecret, credentials, {
212
- name,
213
- ...(organizationName ? { organizationName } : {}),
214
- projectName,
215
- directoryPath,
216
- deviceFingerprint,
217
- deviceHostname: os.hostname(),
218
- devicePlatform: process.platform,
219
- ...(inviteToken ? { inviteToken } : {})
220
- });
221
-
222
- saveCredentials({
223
- access_token: credentials.access_token,
224
- access_token_expires_at: credentials.access_token_expires_at ?? undefined,
225
- refresh_token: credentials.refresh_token,
226
- organization_id: result.organization.organizationId,
227
- platform_url: resolvedPlatformUrl
228
- });
271
+ const result = await completeCliOnboarding(
272
+ resolvedPlatformUrl,
273
+ localSecret,
274
+ { access_token: accessToken },
275
+ {
276
+ name,
277
+ ...(organizationName ? { organizationName } : {}),
278
+ projectName,
279
+ directoryPath,
280
+ deviceFingerprint,
281
+ deviceHostname: os.hostname(),
282
+ devicePlatform: process.platform,
283
+ ...(inviteToken ? { inviteToken } : {})
284
+ }
285
+ );
286
+
287
+ // The browser device-auth flow returns a session that hasn't been stored yet;
288
+ // persist it (with the resolved org). The --email and --use-current-auth paths
289
+ // already saved credentials (including any minted agent token), so we leave the
290
+ // stored credential — and its durable agent token — untouched.
291
+ if (sessionToPersist) {
292
+ saveCredentials({
293
+ access_token: sessionToPersist.access_token,
294
+ access_token_expires_at: sessionToPersist.access_token_expires_at ?? undefined,
295
+ refresh_token: sessionToPersist.refresh_token,
296
+ organization_id: result.organization.organizationId,
297
+ platform_url: resolvedPlatformUrl
298
+ });
299
+ }
229
300
 
230
301
  try {
231
302
  const localConfig = await upsertLocalOverlordConfig({
@@ -3203,6 +3203,30 @@ Use this for ticket lifecycle work from an agent runtime: create a standalone
3203
3203
  draft with \`ovld protocol create\`, create-and-attach with \`ovld protocol prompt\`,
3204
3204
  or attach to an existing ticket with \`ovld protocol attach --ticket-id <ticket_id>\`.
3205
3205
 
3206
+ Account creation & onboarding:
3207
+ No Overlord account or credentials yet? If you have an email address you can
3208
+ create an account and set up a workspace entirely from the terminal — no
3209
+ browser needed. Run these (outside \`ovld protocol\`) before attaching:
3210
+
3211
+ ovld auth signup --email <email> --name <name>
3212
+ Create an account from the terminal: an 8-digit
3213
+ code is emailed and entered here. Mints and
3214
+ stores a durable oat_ agent token by default.
3215
+ ovld onboard --email <email> Same terminal-first signup, then also create an
3216
+ organization + project and link this directory.
3217
+ ovld onboard Browser-based account + workspace setup.
3218
+ ovld onboard --invite <token> Join an organization you were invited to
3219
+ instead of creating a new one.
3220
+ ovld auth login --email <email>
3221
+ Log back in after logout with an emailed code.
3222
+ ovld auth login Authorize an existing account (browser flow,
3223
+ or --token <oat_…> to persist a durable,
3224
+ never-expiring agent token for headless use).
3225
+ ovld auth status Show current login status and organizations.
3226
+
3227
+ To check credentials machine-readably from a script, use
3228
+ \`ovld protocol auth-status\` (returns ok=true|false).
3229
+
3206
3230
  Project discovery:
3207
3231
  When prompting or creating tickets, the CLI automatically resolves the correct
3208
3232
  project by matching your current working directory against your configured
@@ -1,13 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execFileSync, spawn } from 'node:child_process';
4
- import crypto from 'node:crypto';
5
- import fs from 'node:fs';
6
4
  import os from 'node:os';
7
- import path from 'node:path';
5
+
6
+ import { readOrCreateCanonicalDeviceFingerprintSync } from '../../../../lib/overlord/device-identity.mjs';
8
7
 
9
8
  const OVLD_ENTRY = process.argv[1];
10
- const DEVICE_FILE = path.join(os.homedir(), '.ovld', 'device.json');
11
9
  const DEFAULT_TMUX_COMMAND = 'tmux new-session bash {script}';
12
10
 
13
11
  /** @internal Test-only overrides for protocol/launch subprocess calls. */
@@ -65,22 +63,7 @@ export function readOrCreateDeviceFingerprint(flags) {
65
63
  typeof flags['device-fingerprint'] === 'string'
66
64
  ? flags['device-fingerprint'].trim()
67
65
  : process.env.OVERLORD_DEVICE_FINGERPRINT?.trim();
68
- if (explicit) return explicit;
69
-
70
- try {
71
- const raw = fs.readFileSync(DEVICE_FILE, 'utf8');
72
- const parsed = JSON.parse(raw);
73
- if (typeof parsed.deviceFingerprint === 'string' && parsed.deviceFingerprint.trim()) {
74
- return parsed.deviceFingerprint.trim();
75
- }
76
- } catch {
77
- // Create below.
78
- }
79
-
80
- const deviceFingerprint = crypto.randomUUID();
81
- fs.mkdirSync(path.dirname(DEVICE_FILE), { recursive: true });
82
- fs.writeFileSync(DEVICE_FILE, `${JSON.stringify({ deviceFingerprint }, null, 2)}\n`, 'utf8');
83
- return deviceFingerprint;
66
+ return readOrCreateCanonicalDeviceFingerprintSync({ explicitFingerprint: explicit });
84
67
  }
85
68
 
86
69
  function runOvld(args, env = {}) {
@@ -0,0 +1,392 @@
1
+ #!/usr/bin/env node
2
+ /* global console, fetch, process, URL */
3
+
4
+ import os from 'node:os';
5
+ import readline from 'node:readline';
6
+ import { stdin as input, stdout as output } from 'node:process';
7
+
8
+ import { resolveLoginPlatformUrl } from './auth.mjs';
9
+ import { buildAuthHeaders, loadCredentials, loadRuntime, saveCredentials } from './credentials.mjs';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Small parsing / prompting helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export function parseSignupFlags(args = []) {
16
+ const flags = {};
17
+ for (let i = 0; i < args.length; i++) {
18
+ const arg = args[i];
19
+ if (!arg.startsWith('--')) continue;
20
+ const eq = arg.indexOf('=');
21
+ if (eq !== -1) {
22
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
23
+ continue;
24
+ }
25
+ const key = arg.slice(2);
26
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
27
+ flags[key] = args[i + 1];
28
+ i++;
29
+ } else {
30
+ flags[key] = true;
31
+ }
32
+ }
33
+ return flags;
34
+ }
35
+
36
+ function flagString(flags, name) {
37
+ const value = flags[name];
38
+ return typeof value === 'string' ? value.trim() : '';
39
+ }
40
+
41
+ async function promptForValue(question, defaultValue = '') {
42
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return defaultValue;
43
+ const rl = readline.createInterface({ input, output });
44
+ try {
45
+ return await new Promise(resolve => {
46
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
47
+ rl.question(`${question}${suffix}: `, answer => resolve(answer.trim() || defaultValue));
48
+ });
49
+ } finally {
50
+ rl.close();
51
+ }
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Network helpers
56
+ // ---------------------------------------------------------------------------
57
+
58
+ function resolveSignupContext() {
59
+ const storedCredentials = loadCredentials();
60
+ const platformUrl = resolveLoginPlatformUrl(null, storedCredentials?.platform_url ?? null);
61
+ const runtime = loadRuntime(platformUrl);
62
+ const localSecret = runtime?.local_secret ?? process.env.OVERLORD_LOCAL_SECRET ?? '';
63
+ return { platformUrl, localSecret };
64
+ }
65
+
66
+ async function postJson(platformUrl, pathName, localSecret, body, accessToken = '') {
67
+ let res;
68
+ try {
69
+ res = await fetch(`${platformUrl}${pathName}`, {
70
+ method: 'POST',
71
+ headers: {
72
+ ...buildAuthHeaders(accessToken, localSecret),
73
+ 'Content-Type': 'application/json'
74
+ },
75
+ body: JSON.stringify(body)
76
+ });
77
+ } catch (error) {
78
+ const cause = error?.cause;
79
+ const detail = [cause?.code, cause?.message].filter(Boolean).join(': ');
80
+ throw new Error(`Request to ${pathName} failed: ${detail || (error?.message ?? error)}`);
81
+ }
82
+
83
+ const data = await res.json().catch(() => ({}));
84
+ if (!res.ok) {
85
+ const message = data.error ?? `${pathName} failed (${res.status})`;
86
+ const err = new Error(message);
87
+ err.status = res.status;
88
+ err.code = data.code;
89
+ throw err;
90
+ }
91
+ return data;
92
+ }
93
+
94
+ export function requestCliSignup(platformUrl, localSecret, { email, name, password, inviteToken }) {
95
+ return postJson(platformUrl, '/api/auth/cli-signup/request', localSecret, {
96
+ email,
97
+ name,
98
+ ...(password ? { password } : {}),
99
+ ...(inviteToken ? { inviteToken } : {})
100
+ });
101
+ }
102
+
103
+ export function verifyCliSignup(platformUrl, localSecret, { email, token, password }) {
104
+ return postJson(platformUrl, '/api/auth/cli-signup/verify', localSecret, {
105
+ email,
106
+ token,
107
+ ...(password ? { password } : {})
108
+ });
109
+ }
110
+
111
+ export function requestCliLogin(platformUrl, localSecret, { email }) {
112
+ return postJson(platformUrl, '/api/auth/cli-login/request', localSecret, { email });
113
+ }
114
+
115
+ export function verifyCliLogin(platformUrl, localSecret, { email, token }) {
116
+ return postJson(platformUrl, '/api/auth/cli-login/verify', localSecret, { email, token });
117
+ }
118
+
119
+ export function mintAgentToken(platformUrl, localSecret, accessToken, label) {
120
+ return postJson(platformUrl, '/api/auth/agent-token', localSecret, { label }, accessToken);
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Credential persistence after a verified session
125
+ // ---------------------------------------------------------------------------
126
+
127
+ function defaultAgentTokenLabel() {
128
+ const host = os.hostname() || 'cli';
129
+ return `CLI: ${host}`.slice(0, 80);
130
+ }
131
+
132
+ /**
133
+ * Persist a verified Supabase session and, unless suppressed, mint and store a
134
+ * durable `oat_…` agent token as the preferred future credential.
135
+ *
136
+ * @returns {Promise<{ agentToken: string | null }>}
137
+ */
138
+ export async function persistVerifiedSession(
139
+ platformUrl,
140
+ localSecret,
141
+ session,
142
+ { mintToken = true, label = defaultAgentTokenLabel(), logger = console } = {}
143
+ ) {
144
+ const resolvedPlatformUrl = session.platform_url ?? platformUrl;
145
+ let agentToken = null;
146
+
147
+ if (mintToken) {
148
+ try {
149
+ const minted = await mintAgentToken(
150
+ resolvedPlatformUrl,
151
+ localSecret,
152
+ session.access_token,
153
+ label
154
+ );
155
+ agentToken = typeof minted.token === 'string' ? minted.token : null;
156
+ } catch (error) {
157
+ logger.log(
158
+ `\nWarning: could not mint a durable agent token (${error instanceof Error ? error.message : error}).`
159
+ );
160
+ logger.log('Saved the Supabase session instead; re-run `ovld auth login --email` to retry.');
161
+ }
162
+ }
163
+
164
+ saveCredentials({
165
+ ...(agentToken ? { agent_token: agentToken } : {}),
166
+ access_token: session.access_token,
167
+ access_token_expires_at: session.access_token_expires_at ?? undefined,
168
+ refresh_token: session.refresh_token,
169
+ platform_url: resolvedPlatformUrl,
170
+ ...(session.email ? { user_email: session.email } : {})
171
+ });
172
+
173
+ return { agentToken };
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Full interactive signup flow (shared with `ovld onboard --email`)
178
+ // ---------------------------------------------------------------------------
179
+
180
+ /**
181
+ * Run CLI account creation end to end: request a confirmation email, collect the
182
+ * code, verify it, then persist the session (and an agent token by default).
183
+ * Returns the verified session so callers (e.g. onboarding) can keep using the
184
+ * Supabase access token for follow-up provisioning.
185
+ */
186
+ export async function runCliSignupFlow(
187
+ flags,
188
+ { logger = console, mintToken = !(flags['no-agent-token'] === true) } = {}
189
+ ) {
190
+ const email = flagString(flags, 'email') || (await promptForValue('Email'));
191
+ if (!email) throw new Error('An email address is required (--email).');
192
+
193
+ const inferredName = os.userInfo().username || 'Agent';
194
+ const name = flagString(flags, 'name') || (await promptForValue('Your name', inferredName));
195
+ if (!name) throw new Error('A name is required (--name).');
196
+
197
+ const password = flagString(flags, 'password') || undefined;
198
+ const inviteToken = flagString(flags, 'invite') || flagString(flags, 'invite-code') || undefined;
199
+
200
+ const { platformUrl, localSecret } = resolveSignupContext();
201
+
202
+ logger.log(`\nCreating your Overlord account for ${email}...`);
203
+ const requested = await requestCliSignup(platformUrl, localSecret, {
204
+ email,
205
+ name,
206
+ password,
207
+ inviteToken
208
+ });
209
+
210
+ logger.log(`We sent an 8-digit confirmation code to ${email}.`);
211
+ const code = flagString(flags, 'code') || (await promptForValue('Enter confirmation code'));
212
+ if (!code) throw new Error('A confirmation code is required to finish signup.');
213
+
214
+ const session = await verifyCliSignup(platformUrl, localSecret, { email, token: code, password });
215
+
216
+ const { agentToken } = await persistVerifiedSession(platformUrl, localSecret, session, {
217
+ mintToken,
218
+ logger
219
+ });
220
+
221
+ return { session, agentToken, platformUrl, localSecret, passwordless: requested.passwordless };
222
+ }
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // Command handlers
226
+ // ---------------------------------------------------------------------------
227
+
228
+ function printSignupHelp() {
229
+ console.log(`ovld auth signup — create an Overlord account from the terminal
230
+
231
+ Usage:
232
+ ovld auth signup --email <email> [--name <name>] [--password <password>]
233
+ ovld auth signup request --email <email> --name <name> [--password <password>] [--json]
234
+ ovld auth signup verify --email <email> --code <code> [--password <password>] [--json]
235
+
236
+ Flags:
237
+ --email <email> Email to create the account with (prompted if omitted).
238
+ --name <name> Display name (prompted if omitted).
239
+ --password <password> Optional. Recommended: a password-manager-generated password.
240
+ Without one, signup uses an email code and future login is
241
+ via \`ovld auth login --email\`.
242
+ --no-agent-token Do not mint/store a durable oat_ agent token after signup.
243
+ --code <code> Provide the emailed code non-interactively (single-shot flow).
244
+ --json Machine-readable output (request/verify subcommands).
245
+
246
+ Creates the account only. To also create an organization, project, and link this
247
+ directory, use \`ovld onboard --email <email>\`.`);
248
+ }
249
+
250
+ async function runSignupRequest(flags) {
251
+ const email = flagString(flags, 'email');
252
+ const name = flagString(flags, 'name');
253
+ if (!email || !name) {
254
+ throw new Error('`ovld auth signup request` requires --email and --name.');
255
+ }
256
+ const password = flagString(flags, 'password') || undefined;
257
+ const inviteToken = flagString(flags, 'invite') || flagString(flags, 'invite-code') || undefined;
258
+ const { platformUrl, localSecret } = resolveSignupContext();
259
+ const result = await requestCliSignup(platformUrl, localSecret, {
260
+ email,
261
+ name,
262
+ password,
263
+ inviteToken
264
+ });
265
+
266
+ if (flags.json === true) {
267
+ console.log(JSON.stringify(result));
268
+ return;
269
+ }
270
+ console.log(`Confirmation code sent to ${email}.`);
271
+ console.log('Finish with: ovld auth signup verify --email ' + email + ' --code <code>');
272
+ }
273
+
274
+ async function runSignupVerify(flags) {
275
+ const email = flagString(flags, 'email');
276
+ const token = flagString(flags, 'code') || flagString(flags, 'token');
277
+ if (!email || !token) {
278
+ throw new Error('`ovld auth signup verify` requires --email and --code.');
279
+ }
280
+ const password = flagString(flags, 'password') || undefined;
281
+ const { platformUrl, localSecret } = resolveSignupContext();
282
+ const session = await verifyCliSignup(platformUrl, localSecret, { email, token, password });
283
+ const mintToken = !(flags['no-agent-token'] === true);
284
+ const { agentToken } = await persistVerifiedSession(platformUrl, localSecret, session, {
285
+ mintToken
286
+ });
287
+
288
+ if (flags.json === true) {
289
+ console.log(JSON.stringify({ ok: true, email, agentTokenMinted: Boolean(agentToken) }));
290
+ return;
291
+ }
292
+ console.log('Account confirmed and credentials saved.');
293
+ if (agentToken) console.log('A durable agent token was minted and stored for headless use.');
294
+ }
295
+
296
+ export async function runAuthSignupCommand(args = []) {
297
+ const [maybeSub, ...rest] = args;
298
+
299
+ if (maybeSub === 'help' || maybeSub === '--help' || maybeSub === '-h') {
300
+ printSignupHelp();
301
+ return;
302
+ }
303
+
304
+ if (maybeSub === 'request') {
305
+ await runSignupRequest(parseSignupFlags(rest));
306
+ return;
307
+ }
308
+ if (maybeSub === 'verify') {
309
+ await runSignupVerify(parseSignupFlags(rest));
310
+ return;
311
+ }
312
+
313
+ // Single-shot interactive flow: `ovld auth signup --email ...`
314
+ const flags = parseSignupFlags(args);
315
+ const { agentToken, passwordless } = await runCliSignupFlow(flags);
316
+
317
+ console.log('\nOverlord account created.');
318
+ if (agentToken) {
319
+ console.log('A durable agent token was minted and stored for headless use.');
320
+ }
321
+ if (passwordless) {
322
+ console.log('No password was set — log back in later with `ovld auth login --email`.');
323
+ }
324
+ console.log('\nNext: run `ovld onboard --use-current-auth` to create a project in this directory.');
325
+ }
326
+
327
+ /**
328
+ * `ovld auth login --email <email>`: send a fresh email OTP for an existing
329
+ * account, verify it, and persist credentials (minting an agent token by default).
330
+ */
331
+ export async function runEmailLogin(args = []) {
332
+ const flags = parseSignupFlags(args);
333
+ const email = flagString(flags, 'email') || (await promptForValue('Email'));
334
+ if (!email) throw new Error('An email address is required (--email).');
335
+
336
+ const { platformUrl, localSecret } = resolveSignupContext();
337
+
338
+ // Split sub-flow support: `login request` / `login verify` are handled by the
339
+ // caller; this entry point runs the interactive request+verify together.
340
+ console.log(`\nSending a login code to ${email}...`);
341
+ await requestCliLogin(platformUrl, localSecret, { email });
342
+
343
+ const code =
344
+ flagString(flags, 'code') ||
345
+ flagString(flags, 'token') ||
346
+ (await promptForValue('Enter login code'));
347
+ if (!code) throw new Error('A login code is required.');
348
+
349
+ const session = await verifyCliLogin(platformUrl, localSecret, { email, token: code });
350
+ const mintToken = !(flags['no-agent-token'] === true);
351
+ const { agentToken } = await persistVerifiedSession(platformUrl, localSecret, session, {
352
+ mintToken
353
+ });
354
+
355
+ console.log('Logged in successfully.');
356
+ if (agentToken) console.log('A durable agent token was minted and stored for headless use.');
357
+ }
358
+
359
+ /** Split login helpers for agents that pause between request and verify. */
360
+ export async function runEmailLoginRequest(args = []) {
361
+ const flags = parseSignupFlags(args);
362
+ const email = flagString(flags, 'email');
363
+ if (!email) throw new Error('`ovld auth login request` requires --email.');
364
+ const { platformUrl, localSecret } = resolveSignupContext();
365
+ const result = await requestCliLogin(platformUrl, localSecret, { email });
366
+ if (flags.json === true) {
367
+ console.log(JSON.stringify(result));
368
+ return;
369
+ }
370
+ console.log(`Login code sent to ${email}.`);
371
+ }
372
+
373
+ export async function runEmailLoginVerify(args = []) {
374
+ const flags = parseSignupFlags(args);
375
+ const email = flagString(flags, 'email');
376
+ const token = flagString(flags, 'code') || flagString(flags, 'token');
377
+ if (!email || !token) {
378
+ throw new Error('`ovld auth login verify` requires --email and --code.');
379
+ }
380
+ const { platformUrl, localSecret } = resolveSignupContext();
381
+ const session = await verifyCliLogin(platformUrl, localSecret, { email, token });
382
+ const mintToken = !(flags['no-agent-token'] === true);
383
+ const { agentToken } = await persistVerifiedSession(platformUrl, localSecret, session, {
384
+ mintToken
385
+ });
386
+ if (flags.json === true) {
387
+ console.log(JSON.stringify({ ok: true, email, agentTokenMinted: Boolean(agentToken) }));
388
+ return;
389
+ }
390
+ console.log('Logged in successfully.');
391
+ if (agentToken) console.log('A durable agent token was minted and stored for headless use.');
392
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2606081020.0",
3
+ "version": "0.2606081442.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",