overlord-cli 0.2606070914.0 → 0.2606081356.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({