@cargo-ai/cli 1.0.45 → 1.0.46

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.
Files changed (56) hide show
  1. package/README.md +50 -10
  2. package/build/api.d.ts +2 -1
  3. package/build/api.d.ts.map +1 -1
  4. package/build/api.js +5 -1
  5. package/build/commands/auth/auth0.d.ts +33 -0
  6. package/build/commands/auth/auth0.d.ts.map +1 -0
  7. package/build/commands/auth/auth0.js +90 -0
  8. package/build/commands/auth/email.d.ts +16 -0
  9. package/build/commands/auth/email.d.ts.map +1 -0
  10. package/build/commands/auth/email.js +93 -0
  11. package/build/commands/auth/index.d.ts +19 -1
  12. package/build/commands/auth/index.d.ts.map +1 -1
  13. package/build/commands/auth/index.js +121 -59
  14. package/build/commands/auth/loginChannel.d.ts +21 -0
  15. package/build/commands/auth/loginChannel.d.ts.map +1 -0
  16. package/build/commands/auth/loginChannel.js +42 -0
  17. package/build/commands/auth/oauth.d.ts +2 -6
  18. package/build/commands/auth/oauth.d.ts.map +1 -1
  19. package/build/commands/auth/oauth.js +54 -91
  20. package/build/commands/auth/passwordless.d.ts +22 -0
  21. package/build/commands/auth/passwordless.d.ts.map +1 -0
  22. package/build/commands/auth/passwordless.js +70 -0
  23. package/build/commands/auth/session.d.ts +21 -0
  24. package/build/commands/auth/session.d.ts.map +1 -0
  25. package/build/commands/auth/session.js +55 -0
  26. package/build/commands/auth/workspace.d.ts +9 -1
  27. package/build/commands/auth/workspace.d.ts.map +1 -1
  28. package/build/commands/auth/workspace.js +42 -69
  29. package/build/commands/billing/addPaymentMethod.d.ts +26 -0
  30. package/build/commands/billing/addPaymentMethod.d.ts.map +1 -0
  31. package/build/commands/billing/addPaymentMethod.js +57 -0
  32. package/build/commands/billing/subscription.d.ts.map +1 -1
  33. package/build/commands/billing/subscription.js +28 -1
  34. package/build/commands/doctor.js +3 -3
  35. package/build/commands/mcp.d.ts.map +1 -1
  36. package/build/commands/mcp.js +35 -8
  37. package/build/commands/runHandler.d.ts +6 -0
  38. package/build/commands/runHandler.d.ts.map +1 -1
  39. package/build/commands/runHandler.js +34 -4
  40. package/build/config.d.ts +22 -1
  41. package/build/config.d.ts.map +1 -1
  42. package/build/config.js +44 -3
  43. package/build/credentials.d.ts +1 -10
  44. package/build/credentials.d.ts.map +1 -1
  45. package/build/credentials.js +4 -34
  46. package/build/index.js +14 -7
  47. package/build/oauthConfig.d.ts +1 -6
  48. package/build/oauthConfig.d.ts.map +1 -1
  49. package/build/oauthConfig.js +9 -14
  50. package/build/utils/openBrowser.d.ts +7 -0
  51. package/build/utils/openBrowser.d.ts.map +1 -0
  52. package/build/utils/openBrowser.js +29 -0
  53. package/build/utils/prompt.d.ts +12 -0
  54. package/build/utils/prompt.d.ts.map +1 -0
  55. package/build/utils/prompt.js +38 -0
  56. package/package.json +4 -2
package/README.md CHANGED
@@ -23,17 +23,26 @@ npx @cargo-ai/cli orchestration workflow list
23
23
 
24
24
  ## Authentication
25
25
 
26
- A single `cargo-ai login` command supports two modes — pass exactly one of `--token` or `--oauth`:
26
+ A single `cargo-ai login` command supports three channels — pass exactly one of `--email`, `--oauth` or `--token`:
27
27
 
28
28
  ```bash
29
- # 1. Sign in with an existing workspace-scoped API token
30
- cargo-ai login --token <your-api-token>
29
+ # 1. Sign in with a code emailed to you, no browser needed
30
+ cargo-ai login --email you@company.com
31
31
 
32
32
  # 2. Sign in via your browser using the OAuth 2.0 Device Authorization Flow
33
33
  cargo-ai login --oauth
34
+
35
+ # 3. Sign in with an API token you already have
36
+ cargo-ai login --token <your-api-token>
34
37
  ```
35
38
 
36
- Credentials are stored in `~/.config/cargo-ai/credentials.json` (file mode `0600`).
39
+ `--email` and `--oauth` both create the account if it does not exist yet, so there is no separate sign-up command.
40
+
41
+ `--email` and `--oauth` save the signed-in session — a short-lived access token plus the refresh token that renews it — to `~/.config/cargo-ai/credentials.json` (file mode `0600`). The CLI renews it in the background as it expires, so you stay signed in and no API token is created per machine. `--token` saves the API token you supplied instead. The same file backs `cargo-cdk`, so one login serves both.
42
+
43
+ `cargo-ai logout` revokes the session at the identity provider, so signing out is final. A token you supplied yourself with `--token` is only forgotten locally, since it may be in use elsewhere; pass `--keep-token` to force that behaviour for a session too.
44
+
45
+ Treat the file as you would a password. A saved session acts as you across **every** workspace you belong to — unlike an API token, which is limited to one workspace and to the permissions it was created with — and it stays valid until you run `cargo-ai logout` or the identity provider expires it. On a shared or long-lived machine (CI, a build box), prefer an API token created under **Settings → API** and passed via `CARGO_API_TOKEN`, which you can scope down and revoke from the dashboard.
37
46
 
38
47
  ```bash
39
48
  # Check current auth status
@@ -43,22 +52,53 @@ cargo-ai whoami
43
52
  cargo-ai logout
44
53
  ```
45
54
 
55
+ ### Sign-in without a browser (`--email`)
56
+
57
+ `cargo-ai login --email you@company.com` emails you a one-time code, then creates your account (on first use) and a workspace — no browser and no dashboard visit at any point. On a terminal the code is read back from a prompt. Where there is no terminal (sandboxes, agent shells) the first call sends the code and exits, so you can finish with:
58
+
59
+ ```bash
60
+ cargo-ai login --email you@company.com --code 123456 --workspace-name "Acme GTM"
61
+ ```
62
+
63
+ Re-running with an address that already has an account resolves to its existing workspace rather than creating one, and `--workspace-name` reuses a workspace of that name when one already exists, so this is safe to repeat.
64
+
65
+ To keep the code out of your shell history, pass it on stdin with `--code -`:
66
+
67
+ ```bash
68
+ echo 123456 | cargo-ai login --email you@company.com --code -
69
+ ```
70
+
46
71
  ### Browser sign-in (`--oauth`)
47
72
 
48
- `cargo-ai login --oauth` runs the standard [OAuth 2.0 Device Authorization Flow](https://datatracker.ietf.org/doc/html/rfc8628) against the Cargo OAuth provider, prints a verification URL and user code, opens your default browser, and polls until you complete sign-in. On success the access token is written to `~/.config/cargo-ai/credentials.json`.
73
+ `cargo-ai login --oauth` runs the standard [OAuth 2.0 Device Authorization Flow](https://datatracker.ietf.org/doc/html/rfc8628) against the Cargo OAuth provider, prints a verification URL and user code, opens your default browser, and polls until you complete sign-in. On success the CLI writes the resulting session to `~/.config/cargo-ai/credentials.json`.
49
74
 
50
- The CLI ships with a built-in OAuth client, so no setup is required. Advanced users can override the provider via `--client-id`, `--domain`, and `--audience`.
75
+ The CLI ships with a built-in OAuth client, so no setup is required.
51
76
 
52
77
  ### Workspace selection
53
78
 
54
- API tokens (`--token`) are workspace-scoped, so the workspace is implicit and nothing is persisted alongside the token.
55
-
56
- OAuth sessions (`--oauth`) are user-scoped, so after authentication the CLI picks a default workspace and saves it next to the token:
79
+ `--email` and `--oauth` authenticate you as a user, so the CLI picks a workspace to save alongside the session:
57
80
 
81
+ - **No workspaces**: one is created (you are prompted for a name in a TTY, or pass `--workspace-name <name>`).
58
82
  - **One workspace**: it is selected automatically.
59
83
  - **Multiple workspaces**: you are prompted to pick one (in a TTY); in non-interactive shells the CLI prints the list and asks you to re-run with `--workspace-uuid <uuid>`.
60
84
 
61
- Pass `cargo-ai login --oauth --workspace-uuid <uuid>` to skip the prompt. `CARGO_WORKSPACE_UUID` (or per-command `--workspace-uuid` flags where supported) overrides the saved default at runtime.
85
+ Pass `--workspace-uuid <uuid>` or `--workspace-name <name>` to skip the prompt.
86
+
87
+ ### Switching workspaces
88
+
89
+ A signed-in session is not tied to one workspace, so `CARGO_WORKSPACE_UUID` switches workspace for a single command or a whole shell:
90
+
91
+ ```bash
92
+ CARGO_WORKSPACE_UUID=<other-workspace-uuid> cargo-ai storage model list
93
+ ```
94
+
95
+ To change the default your session uses, sign in again for that workspace:
96
+
97
+ ```bash
98
+ cargo-ai login --email you@company.com --workspace-uuid <other-workspace-uuid>
99
+ ```
100
+
101
+ An API token is different: it is scoped to a single workspace and the server takes the workspace from the token, not from anything sent alongside it. So if you signed in with `--token` and `CARGO_WORKSPACE_UUID` names a different workspace, the CLI refuses the command rather than silently running against the token's workspace. The override still applies when you supply the token yourself via `CARGO_API_TOKEN`, since only you know which workspace that token is for.
62
102
 
63
103
  ### Environment variables (override)
64
104
 
package/build/api.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { type Api } from "@cargo-ai/api";
2
+ import type { AccessTokenProvider } from "@cargo-ai/cdk/cli";
2
3
  export type { Api };
3
4
  export declare function createApi(opts: {
4
5
  baseUrl: string;
5
- accessToken: string;
6
+ getAccessToken: AccessTokenProvider;
6
7
  workspaceUuid: string | undefined;
7
8
  }): Api;
8
9
  //# sourceMappingURL=api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,eAAe,CAAC;AAInD,YAAY,EAAE,GAAG,EAAE,CAAC;AAQpB,wBAAgB,SAAS,CAAC,IAAI,EAAE;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,GAAG,GAAG,CAWN"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAI7D,YAAY,EAAE,GAAG,EAAE,CAAC;AAQpB,wBAAgB,SAAS,CAAC,IAAI,EAAE;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,mBAAmB,CAAC;IACpC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,GAAG,GAAG,CAeN"}
package/build/api.js CHANGED
@@ -8,8 +8,12 @@ export function createApi(opts) {
8
8
  return buildApi({
9
9
  baseUrl: opts.baseUrl,
10
10
  workspaceUuid: opts.workspaceUuid,
11
- accessToken: opts.accessToken,
11
+ getAccessToken: opts.getAccessToken,
12
12
  origin: ORIGIN,
13
+ // A saved session outlives the workspace list baked into its token, so a
14
+ // workspace granted since signing in reads as a 401 until the token is
15
+ // reissued. Renew and retry rather than telling the user to sign in again.
16
+ retryOnUnauthorized: true,
13
17
  // The client is `fetch`-based; supply a proxy-tunneling `fetch` (undici
14
18
  // `ProxyAgent`) when proxy env vars are set, since Node's global `fetch`
15
19
  // does not honor them. `undefined` when no proxy applies.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What both sign-in channels hand to `establishSession`. The refresh token is
3
+ * optional: the identity provider only issues one when the application has the
4
+ * refresh grant enabled.
5
+ */
6
+ export type IdentityTokens = {
7
+ accessToken: string;
8
+ refreshToken: string | undefined;
9
+ /** Epoch milliseconds at which `accessToken` stops being accepted. */
10
+ expiresAt: number;
11
+ };
12
+ export type Auth0ErrorBody = {
13
+ error?: string;
14
+ error_description?: string;
15
+ };
16
+ /**
17
+ * Throws rather than calling `failWith` so the device-flow poll can treat a
18
+ * blip as a retryable iteration instead of losing an in-progress sign-in.
19
+ */
20
+ export declare function postToAuth0(opts: {
21
+ path: string;
22
+ body: URLSearchParams | Record<string, unknown>;
23
+ timeoutMs?: number;
24
+ }): Promise<Response>;
25
+ export declare function parseTokenResponse(json: unknown): IdentityTokens;
26
+ /**
27
+ * Falls back to the raw text: a gateway or WAF in front of the provider answers
28
+ * with HTML, and "no response body" would throw away the only clue about why.
29
+ */
30
+ export declare function readErrorBody(response: Response): Promise<Auth0ErrorBody | string | undefined>;
31
+ export declare function describeError(body: Auth0ErrorBody | string | undefined): string;
32
+ export declare function errorCodeOf(body: Auth0ErrorBody | string | undefined): string | undefined;
33
+ //# sourceMappingURL=auth0.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth0.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/auth0.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAWF;;;GAGG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,QAAQ,CAAC,CA0BpB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAoBhE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,cAAc,GAAG,MAAM,GAAG,SAAS,CAAC,CAkB9C;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,GACxC,MAAM,CAkBR;AAED,wBAAgB,WAAW,CACzB,IAAI,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,GACxC,MAAM,GAAG,SAAS,CAMpB"}
@@ -0,0 +1,90 @@
1
+ import { getOAuthConfig } from "../../oauthConfig.js";
2
+ import { failWith } from "../runHandler.js";
3
+ const REQUEST_TIMEOUT_MS = 15_000;
4
+ // Only used when the provider omits `expires_in`, which Auth0 does not do.
5
+ const FALLBACK_LIFETIME_SECONDS = 3600;
6
+ /**
7
+ * Throws rather than calling `failWith` so the device-flow poll can treat a
8
+ * blip as a retryable iteration instead of losing an in-progress sign-in.
9
+ */
10
+ export async function postToAuth0(opts) {
11
+ const config = getOAuthConfig();
12
+ const timeoutMs = opts.timeoutMs !== undefined ? opts.timeoutMs : REQUEST_TIMEOUT_MS;
13
+ const isForm = opts.body instanceof URLSearchParams;
14
+ try {
15
+ return await fetch(`https://${config.domain}${opts.path}`, {
16
+ method: "POST",
17
+ headers: {
18
+ "content-type": isForm
19
+ ? "application/x-www-form-urlencoded"
20
+ : "application/json",
21
+ },
22
+ body: isForm ? opts.body.toString() : JSON.stringify(opts.body),
23
+ signal: AbortSignal.timeout(timeoutMs),
24
+ });
25
+ }
26
+ catch (err) {
27
+ const seconds = String(Math.round(timeoutMs / 1000));
28
+ const reason = err instanceof Error && err.name === "TimeoutError"
29
+ ? `did not respond within ${seconds}s`
30
+ : `could not be reached (${err instanceof Error ? err.message : String(err)})`;
31
+ throw new Error(`The identity provider (${config.domain}) ${reason}.`);
32
+ }
33
+ }
34
+ export function parseTokenResponse(json) {
35
+ const token = json;
36
+ if (typeof token.access_token !== "string") {
37
+ failWith(`Unexpected token response from the identity provider: ${JSON.stringify(json)}`);
38
+ }
39
+ const lifetime = typeof token.expires_in === "number"
40
+ ? token.expires_in
41
+ : FALLBACK_LIFETIME_SECONDS;
42
+ return {
43
+ accessToken: token.access_token,
44
+ refreshToken: typeof token.refresh_token === "string" ? token.refresh_token : undefined,
45
+ expiresAt: Date.now() + lifetime * 1000,
46
+ };
47
+ }
48
+ /**
49
+ * Falls back to the raw text: a gateway or WAF in front of the provider answers
50
+ * with HTML, and "no response body" would throw away the only clue about why.
51
+ */
52
+ export async function readErrorBody(response) {
53
+ let text;
54
+ try {
55
+ text = await response.text();
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ if (text.length === 0) {
61
+ return undefined;
62
+ }
63
+ try {
64
+ return JSON.parse(text);
65
+ }
66
+ catch {
67
+ return text;
68
+ }
69
+ }
70
+ export function describeError(body) {
71
+ if (body === undefined) {
72
+ return "no response body";
73
+ }
74
+ if (typeof body === "string") {
75
+ return body;
76
+ }
77
+ if (body.error_description !== undefined) {
78
+ return body.error_description;
79
+ }
80
+ if (body.error !== undefined) {
81
+ return body.error;
82
+ }
83
+ return JSON.stringify(body);
84
+ }
85
+ export function errorCodeOf(body) {
86
+ if (body === undefined || typeof body === "string") {
87
+ return undefined;
88
+ }
89
+ return body.error;
90
+ }
@@ -0,0 +1,16 @@
1
+ /** `--code -` reads the code from stdin, keeping it out of shell history. */
2
+ export declare const STDIN_CODE = "-";
3
+ export type EmailLoginOptions = {
4
+ email?: string;
5
+ code?: string;
6
+ workspaceName?: string;
7
+ workspaceUuid?: string;
8
+ baseUrl?: string;
9
+ };
10
+ /**
11
+ * Signs in with a code emailed by the identity provider, creating the account
12
+ * on first use. This is the only channel that needs neither a browser nor an
13
+ * existing credential.
14
+ */
15
+ export declare function runEmailLogin(opts: EmailLoginOptions): Promise<void>;
16
+ //# sourceMappingURL=email.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/email.ts"],"names":[],"mappings":"AAaA,6EAA6E;AAC7E,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6C1E"}
@@ -0,0 +1,93 @@
1
+ import { getCredentialsPath } from "../../credentials.js";
2
+ import { askQuestion, openPromptInput } from "../../utils/prompt.js";
3
+ import { ExitCodes, failWith, info, outputJson, startSpinner, success, } from "../runHandler.js";
4
+ import { exchangeEmailOtp, startEmailPasswordless } from "./passwordless.js";
5
+ import { establishSession, resolveBaseUrl } from "./session.js";
6
+ /** `--code -` reads the code from stdin, keeping it out of shell history. */
7
+ export const STDIN_CODE = "-";
8
+ /**
9
+ * Signs in with a code emailed by the identity provider, creating the account
10
+ * on first use. This is the only channel that needs neither a browser nor an
11
+ * existing credential.
12
+ */
13
+ export async function runEmailLogin(opts) {
14
+ const email = opts.email;
15
+ if (email === undefined || email.length === 0) {
16
+ failWith("Must pass --email <email>.", { code: ExitCodes.InvalidUsage });
17
+ }
18
+ const resumeCommand = `cargo-ai login --email ${email} --code <code>`;
19
+ const code = await resolveCode({ email, code: opts.code, resumeCommand });
20
+ if (code === undefined) {
21
+ // The code was sent but there is no terminal to type it into. Exit
22
+ // successfully so an agent can relay the code and re-run non-interactively.
23
+ outputJson({
24
+ ok: true,
25
+ status: "codeSent",
26
+ email,
27
+ next: resumeCommand,
28
+ });
29
+ return;
30
+ }
31
+ const spinner = startSpinner("Verifying code...");
32
+ const tokens = await exchangeEmailOtp({ email, otp: code });
33
+ spinner.stop();
34
+ const workspace = await establishSession({
35
+ tokens,
36
+ baseUrlOverride: opts.baseUrl,
37
+ workspaceUuid: opts.workspaceUuid,
38
+ workspaceName: opts.workspaceName,
39
+ });
40
+ success(`Signed in as ${email}. Workspace: ${workspace.name}.`);
41
+ outputJson({
42
+ ok: true,
43
+ status: "signedIn",
44
+ credentialsFile: getCredentialsPath(),
45
+ baseUrl: resolveBaseUrl(opts.baseUrl),
46
+ email,
47
+ workspace,
48
+ tokenType: "session",
49
+ });
50
+ }
51
+ /**
52
+ * Returns the one-time code, sending a fresh one first unless the caller
53
+ * already has it. Resolves to `undefined` when a code was sent but no terminal
54
+ * is available to read it back.
55
+ */
56
+ async function resolveCode(opts) {
57
+ if (opts.code === STDIN_CODE) {
58
+ return await readCodeFromStdin();
59
+ }
60
+ if (opts.code !== undefined && opts.code.length > 0) {
61
+ return opts.code;
62
+ }
63
+ const spinner = startSpinner(`Sending a verification code to ${opts.email}...`);
64
+ await startEmailPasswordless({ email: opts.email });
65
+ spinner.stop();
66
+ success(`Verification code sent to ${opts.email}.`);
67
+ const promptInput = openPromptInput();
68
+ if (promptInput === undefined) {
69
+ info(`No interactive terminal available. Re-run with the code: ${opts.resumeCommand}`);
70
+ return undefined;
71
+ }
72
+ try {
73
+ const answer = await askQuestion(promptInput.stream, "Enter the code from your inbox: ");
74
+ if (answer === undefined) {
75
+ failWith("No code entered.", { code: ExitCodes.InvalidUsage });
76
+ }
77
+ return answer;
78
+ }
79
+ finally {
80
+ promptInput.close();
81
+ }
82
+ }
83
+ async function readCodeFromStdin() {
84
+ const chunks = [];
85
+ for await (const chunk of process.stdin) {
86
+ chunks.push(Buffer.from(chunk));
87
+ }
88
+ const code = Buffer.concat(chunks).toString("utf-8").trim();
89
+ if (code.length === 0) {
90
+ failWith("No code received on stdin.", { code: ExitCodes.InvalidUsage });
91
+ }
92
+ return code;
93
+ }
@@ -1,4 +1,22 @@
1
- import type { Command } from "commander";
1
+ import { type Command } from "commander";
2
2
  import type { Api } from "../../api.js";
3
+ import type { Credentials } from "../../credentials.js";
4
+ export type LogoutOptions = {
5
+ keepToken?: boolean;
6
+ keepSession?: boolean;
7
+ };
3
8
  export declare function registerAuthCommands(program: Command, getApi: () => Api): void;
9
+ export type RevocationPlan = {
10
+ kind: "session";
11
+ } | {
12
+ kind: "none";
13
+ };
14
+ /**
15
+ * Decides what signing out should invalidate remotely. A token the user
16
+ * supplied with --token is left alone: it carries no session, and may well be
17
+ * in use elsewhere.
18
+ *
19
+ * Exported for tests; `runLogout` is the only production caller.
20
+ */
21
+ export declare function planRevocation(credentials: Credentials | undefined, opts: LogoutOptions): RevocationPlan;
4
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AA8BxC,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAiFN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,WAAW,CAAC;AAEjD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAwBxD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAkGN;AAiFD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,IAAI,EAAE,aAAa,GAClB,cAAc,CAchB"}
@@ -1,48 +1,66 @@
1
+ import { revokeSession } from "@cargo-ai/cdk/cli";
2
+ import { Option } from "commander";
1
3
  import { getConfig } from "../../config.js";
2
- import { clearCredentials, getCredentialsPath, saveCredentials, } from "../../credentials.js";
4
+ import { clearCredentials, getCredentialsPath, loadCredentials, saveCredentials, } from "../../credentials.js";
3
5
  import { ExitCodes, failWith, handleApiCall, info, outputJson, success, } from "../runHandler.js";
6
+ import { runEmailLogin } from "./email.js";
7
+ import { selectLoginChannel } from "./loginChannel.js";
4
8
  import { runOAuthDeviceFlow } from "./oauth.js";
5
- import { resolveWorkspace } from "./workspace.js";
6
- const DEFAULT_BASE_URL = "https://api.getcargo.io";
9
+ import { DEFAULT_BASE_URL, establishSession, resolveBaseUrl, } from "./session.js";
7
10
  export function registerAuthCommands(program, getApi) {
8
11
  program
9
12
  .command("login")
10
- .description("Sign in to Cargo with an API token (--token) or via browser (--oauth)")
11
- .option("--token <token>", "Cargo API token; mutually exclusive with --oauth")
13
+ .description("Sign in to Cargo with an emailed code (--email), via browser (--oauth), or with an existing API token (--token)")
14
+ .option("--email <email>", "Sign in with a code emailed to this address; creates the account on first use")
15
+ .option("--code <code>", 'Verification code from the email, or "-" to read it from stdin; omit to have one sent (--email only)')
12
16
  .option("--oauth", "Sign in via browser using the OAuth 2.0 Device Authorization Flow")
17
+ .option("--token <token>", "Existing Cargo API token")
18
+ .option("--workspace-name <name>", "Workspace to use, created if no workspace of that name exists (skips the workspace prompt)")
19
+ .option("--workspace-uuid <uuid>", "Workspace UUID to sign in for (skips the workspace prompt)")
13
20
  .option("--base-url <url>", `Cargo API base URL (string, default: ${DEFAULT_BASE_URL})`)
14
- .option("--workspace-uuid <uuid>", "Default workspace UUID to save (--oauth only; skips the workspace prompt)")
15
- .option("--client-id <id>", "OAuth native application client ID (--oauth only; defaults to the built-in Cargo CLI client)")
16
- .option("--domain <domain>", "OAuth provider domain (--oauth only; default: auth.getcargo.io)")
17
- .option("--audience <audience>", "OAuth API audience (--oauth only; default: https://api.getcargo.io)")
18
21
  .addHelpText("after", `
19
22
  Examples:
20
- $ cargo-ai login --token sk_live_abc123
21
- $ cargo-ai login --token sk_live_abc123 --base-url https://api.custom.io
23
+ $ cargo-ai login --email you@company.com
24
+ $ cargo-ai login --email you@company.com --code 123456 --workspace-name "Acme GTM"
22
25
  $ cargo-ai login --oauth
23
26
  $ cargo-ai login --oauth --workspace-uuid 550e8400-e29b-41d4-a716-446655440000
27
+ $ cargo-ai login --token sk_live_abc123 --base-url https://api.custom.io
28
+
29
+ --email and --oauth both create the account if it does not exist yet, so there
30
+ is no separate sign-up step. --email needs no browser at any point: a code is
31
+ emailed and read back from the prompt. Where there is no terminal (sandboxes,
32
+ agent shells) the first call sends the code and exits, so re-run with --code to
33
+ finish. To keep the code out of shell history, pass it on stdin instead:
34
+ echo 123456 | cargo-ai login --email you@company.com --code -
35
+
36
+ Signing in with an address that already has an account resolves to its existing
37
+ workspace rather than creating one, and --workspace-name reuses a workspace of
38
+ that name when one exists, so this is safe to re-run.
39
+
40
+ --email and --oauth save the signed-in session to
41
+ ~/.config/cargo-ai/credentials.json and renew it automatically, so you stay
42
+ signed in without a token being created for each machine. Switch workspace at
43
+ any time with CARGO_WORKSPACE_UUID or by signing in again with
44
+ --workspace-uuid <uuid>. An API token passed with --token stays pinned to its
45
+ own workspace, so the override does not apply to it.
24
46
 
25
- Credentials are stored in ~/.config/cargo-ai/credentials.json.
26
47
  Environment variables (CARGO_API_TOKEN, CARGO_WORKSPACE_UUID, CARGO_BASE_URL) take precedence over saved credentials, and the nearest project .env (walking up from the current directory) fills them in when they are not already exported.`)
27
48
  .action(async (opts) => {
28
49
  await runLogin(opts);
29
50
  });
30
51
  program
31
52
  .command("logout")
32
- .description("Remove saved API credentials")
33
- .action(() => {
34
- const removed = clearCredentials();
35
- if (removed === true) {
36
- success("Signed out.");
37
- }
38
- else {
39
- info("No saved credentials to remove.");
40
- }
41
- outputJson({
42
- ok: true,
43
- removed,
44
- credentialsFile: getCredentialsPath(),
45
- });
53
+ .description("Sign out: revoke the saved credential and remove it locally")
54
+ .option("--keep-token", "Only forget the credentials locally, leaving the session or token valid")
55
+ // The credential is usually a session now, so accept the name people reach
56
+ // for; --keep-token stays the documented spelling for existing scripts.
57
+ .addOption(new Option("--keep-session").hideHelp())
58
+ .addHelpText("after", `
59
+ A session from "cargo-ai login --email" or "--oauth" is revoked at the identity
60
+ provider, so signing out is final. Tokens you supplied yourself with --token are
61
+ only forgotten locally, since they may be in use elsewhere.`)
62
+ .action(async (opts) => {
63
+ await runLogout(opts);
46
64
  });
47
65
  program
48
66
  .command("whoami")
@@ -55,63 +73,107 @@ authentication source ("environment" or "credentials-file"), and base URL.`)
55
73
  });
56
74
  }
57
75
  async function runLogin(opts) {
58
- const hasToken = opts.token !== undefined;
59
- const wantsOAuth = opts.oauth === true;
60
- if (hasToken === true && wantsOAuth === true) {
61
- failWith("Cannot combine --token and --oauth; pass only one.", {
62
- code: ExitCodes.InvalidUsage,
63
- });
76
+ const selection = selectLoginChannel(opts);
77
+ if ("error" in selection) {
78
+ failWith(selection.error, { code: ExitCodes.InvalidUsage });
64
79
  }
65
- if (hasToken === false && wantsOAuth === false) {
66
- failWith("Must pass --token <token> (API token) or --oauth (browser sign-in).", { code: ExitCodes.InvalidUsage });
80
+ if (selection.channel === "email") {
81
+ await runEmailLogin(opts);
82
+ return;
67
83
  }
68
- const baseUrl = opts.baseUrl !== undefined && opts.baseUrl.length > 0
69
- ? opts.baseUrl
70
- : DEFAULT_BASE_URL;
71
- if (hasToken === true) {
72
- const token = opts.token;
84
+ const baseUrl = resolveBaseUrl(opts.baseUrl);
85
+ if (selection.channel === "token") {
73
86
  saveCredentials({
74
- accessToken: token,
87
+ accessToken: selection.token,
88
+ workspaceUuid: opts.workspaceUuid,
75
89
  baseUrl: opts.baseUrl,
76
90
  });
77
91
  success("Signed in with API token.");
78
92
  outputJson({
79
93
  ok: true,
94
+ status: "signedIn",
80
95
  credentialsFile: getCredentialsPath(),
81
96
  baseUrl,
97
+ workspaceUuid: opts.workspaceUuid,
82
98
  tokenType: "api-token",
83
99
  });
84
100
  return;
85
101
  }
86
- const result = await runOAuthDeviceFlow({
87
- clientId: opts.clientId,
88
- domain: opts.domain,
89
- audience: opts.audience,
90
- });
91
- const workspace = await resolveWorkspace({
92
- baseUrl,
93
- accessToken: result.accessToken,
94
- explicitUuid: opts.workspaceUuid,
95
- });
96
- saveCredentials({
97
- accessToken: result.accessToken,
98
- workspaceUuid: workspace.uuid,
99
- baseUrl: opts.baseUrl,
102
+ const tokens = await runOAuthDeviceFlow();
103
+ const workspace = await establishSession({
104
+ tokens,
105
+ baseUrlOverride: opts.baseUrl,
106
+ workspaceUuid: opts.workspaceUuid,
107
+ workspaceName: opts.workspaceName,
100
108
  });
101
109
  success(`Signed in. Default workspace: ${workspace.name}.`);
102
110
  outputJson({
103
111
  ok: true,
112
+ status: "signedIn",
104
113
  credentialsFile: getCredentialsPath(),
105
114
  baseUrl,
106
115
  workspace,
107
- tokenType: "oauth-jwt",
108
- expiresIn: result.expiresIn,
116
+ tokenType: "session",
109
117
  });
110
118
  }
119
+ async function runLogout(opts) {
120
+ const credentials = loadCredentials();
121
+ const plan = planRevocation(credentials, opts);
122
+ const revoked = await applyRevocation(plan, credentials);
123
+ const removed = clearCredentials();
124
+ if (removed === true) {
125
+ success(revoked === true ? "Signed out and revoked access." : "Signed out.");
126
+ }
127
+ else {
128
+ info("No saved credentials to remove.");
129
+ }
130
+ outputJson({
131
+ ok: true,
132
+ removed,
133
+ revoked,
134
+ credentialsFile: getCredentialsPath(),
135
+ });
136
+ }
137
+ /**
138
+ * Decides what signing out should invalidate remotely. A token the user
139
+ * supplied with --token is left alone: it carries no session, and may well be
140
+ * in use elsewhere.
141
+ *
142
+ * Exported for tests; `runLogout` is the only production caller.
143
+ */
144
+ export function planRevocation(credentials, opts) {
145
+ if (opts.keepToken === true || opts.keepSession === true) {
146
+ return { kind: "none" };
147
+ }
148
+ if (credentials === undefined) {
149
+ return { kind: "none" };
150
+ }
151
+ if (credentials.session !== undefined) {
152
+ return { kind: "session" };
153
+ }
154
+ return { kind: "none" };
155
+ }
156
+ async function applyRevocation(plan, credentials) {
157
+ if (plan.kind === "none" || credentials === undefined) {
158
+ return false;
159
+ }
160
+ const session = credentials.session;
161
+ if (session === undefined) {
162
+ return false;
163
+ }
164
+ try {
165
+ await revokeSession(session);
166
+ return true;
167
+ }
168
+ catch {
169
+ info("Could not revoke the session remotely; the local credentials were still removed.");
170
+ return false;
171
+ }
172
+ }
111
173
  async function runWhoami(getApi) {
112
- const { accessToken, baseUrl, source } = getConfig();
113
- if (accessToken === undefined) {
114
- failWith('Not authenticated. Run "cargo-ai login --token <token>" or "cargo-ai login --oauth".', { code: ExitCodes.NotAuthenticated });
174
+ const { getAccessToken, baseUrl, source } = getConfig();
175
+ if (getAccessToken === undefined) {
176
+ failWith('Not authenticated. Run "cargo-ai login --email you@company.com" (no browser needed) or "cargo-ai login --oauth".', { code: ExitCodes.NotAuthenticated });
115
177
  }
116
178
  const api = getApi();
117
179
  const { user } = await handleApiCall(() => api.userManagement.user.getCurrent());
@@ -0,0 +1,21 @@
1
+ import type { EmailLoginOptions } from "./email.js";
2
+ export type LoginOptions = EmailLoginOptions & {
3
+ token?: string;
4
+ oauth?: boolean;
5
+ };
6
+ export type LoginChannelSelection = {
7
+ channel: "email";
8
+ } | {
9
+ channel: "oauth";
10
+ } | {
11
+ channel: "token";
12
+ token: string;
13
+ } | {
14
+ error: string;
15
+ };
16
+ /**
17
+ * Picks the sign-in channel and rejects flag combinations whose extra options
18
+ * would otherwise be accepted and quietly ignored.
19
+ */
20
+ export declare function selectLoginChannel(opts: LoginOptions): LoginChannelSelection;
21
+ //# sourceMappingURL=loginChannel.d.ts.map