overlord-cli 0.2606081020.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 +21 -0
- package/bin/_cli/auth.mjs +29 -0
- package/bin/_cli/credentials.mjs +51 -0
- package/bin/_cli/index.mjs +2 -1
- package/bin/_cli/onboard.mjs +107 -36
- package/bin/_cli/protocol.mjs +24 -0
- package/bin/_cli/signup.mjs +392 -0
- package/package.json +1 -1
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
|
}
|
package/bin/_cli/credentials.mjs
CHANGED
|
@@ -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;
|
package/bin/_cli/index.mjs
CHANGED
|
@@ -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
|
|
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
|
package/bin/_cli/onboard.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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({
|
package/bin/_cli/protocol.mjs
CHANGED
|
@@ -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
|
|
@@ -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
|
+
}
|