@cargo-ai/cli 1.0.45 → 1.0.47

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 +6 -4
@@ -0,0 +1,57 @@
1
+ import { openBrowser } from "../../utils/openBrowser.js";
2
+ import { failWith, handleApiCall, info, outputJson, sleep, success, } from "../runHandler.js";
3
+ export const DEFAULT_TIMEOUT_SECONDS = 300;
4
+ export const DEFAULT_POLL_INTERVAL_SECONDS = 3;
5
+ /**
6
+ * Hands card entry off to Stripe's hosted billing portal and waits for the card
7
+ * on file to change.
8
+ *
9
+ * The card never touches this process: the CLI only ever sees the last four
10
+ * digits that the API already exposes. Polling is what makes the handoff usable
11
+ * from a script — the caller gets a definite answer instead of having to guess
12
+ * whether the human finished.
13
+ */
14
+ export async function runAddPaymentMethod(api, opts) {
15
+ const before = await handleApiCall(() => api.billing.subscription.getCreditCard());
16
+ const { portalSession } = await handleApiCall(() => api.billing.subscription.createPortalSession());
17
+ info("");
18
+ info("To add or replace your card, open this URL in a browser:");
19
+ info(` ${portalSession.url}`);
20
+ info("");
21
+ info("Waiting for the card on file to change...");
22
+ info("");
23
+ if (opts.open === true) {
24
+ openBrowser(portalSession.url);
25
+ }
26
+ const deadline = Date.now() + opts.timeout * 1000;
27
+ const beforeFingerprint = fingerprintCard(before.creditCard);
28
+ while (Date.now() < deadline) {
29
+ await sleep(opts.pollInterval * 1000);
30
+ const current = await handleApiCall(() => api.billing.subscription.getCreditCard(), { spinner: false });
31
+ if (fingerprintCard(current.creditCard) !== beforeFingerprint) {
32
+ const { creditCard } = current;
33
+ success(creditCard !== undefined
34
+ ? `Payment method updated: ${creditCard.brand} ending ${creditCard.last4}.`
35
+ : "Payment method removed.");
36
+ outputJson({ ok: true, status: "updated", creditCard });
37
+ return;
38
+ }
39
+ }
40
+ failWith(`Timed out after ${String(opts.timeout)}s waiting for the card to change. If you finished in the browser, check with: cargo-ai billing subscription get-credit-card`);
41
+ }
42
+ /**
43
+ * Stripe never exposes a raw card number here, so a change is detected from the
44
+ * fields the API does return. Replacing a card with the very same one is
45
+ * therefore indistinguishable from doing nothing, and the command times out.
46
+ */
47
+ export function fingerprintCard(card) {
48
+ if (card === undefined) {
49
+ return "none";
50
+ }
51
+ return [
52
+ card.brand,
53
+ card.last4,
54
+ String(card.expMonth),
55
+ String(card.expYear),
56
+ ].join("|");
57
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../../../src/commands/billing/subscription.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAGxC,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAsPN"}
1
+ {"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../../../src/commands/billing/subscription.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAQxC,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAqSN"}
@@ -1,4 +1,5 @@
1
- import { handleApiCall, outputJson } from "../runHandler.js";
1
+ import { handleApiCall, outputJson, parsePositiveInt } from "../runHandler.js";
2
+ import { DEFAULT_POLL_INTERVAL_SECONDS, DEFAULT_TIMEOUT_SECONDS, runAddPaymentMethod, } from "./addPaymentMethod.js";
2
3
  export function registerSubscriptionCommands(parent, getApi) {
3
4
  const subscription = parent
4
5
  .command("subscription")
@@ -49,6 +50,32 @@ Example:
49
50
  const result = await handleApiCall(() => api.billing.subscription.createSetupIntent());
50
51
  outputJson(result);
51
52
  });
53
+ subscription
54
+ .command("add-payment-method")
55
+ .description("Add or replace the card on file via Stripe's hosted portal, then wait for it to land")
56
+ .option("--timeout <seconds>", `How long to wait for the card to change (default: ${String(DEFAULT_TIMEOUT_SECONDS)})`)
57
+ .option("--poll-interval <seconds>", `How often to check for the new card (default: ${String(DEFAULT_POLL_INTERVAL_SECONDS)})`)
58
+ .option("--no-open", "Print the URL without launching a browser")
59
+ .addHelpText("after", `
60
+ Examples:
61
+ $ cargo-ai billing subscription add-payment-method
62
+ $ cargo-ai billing subscription add-payment-method --no-open --timeout 600
63
+
64
+ Card details are entered on Stripe's hosted portal and never pass through this
65
+ CLI. The URL is printed as well as opened, so this works over SSH and in
66
+ sandboxes — relay the link and the command will detect the new card itself.`)
67
+ .action(async (opts) => {
68
+ const api = getApi();
69
+ await runAddPaymentMethod(api, {
70
+ timeout: opts.timeout !== undefined
71
+ ? parsePositiveInt(opts.timeout, "--timeout")
72
+ : DEFAULT_TIMEOUT_SECONDS,
73
+ pollInterval: opts.pollInterval !== undefined
74
+ ? parsePositiveInt(opts.pollInterval, "--poll-interval")
75
+ : DEFAULT_POLL_INTERVAL_SECONDS,
76
+ open: opts.open,
77
+ });
78
+ });
52
79
  subscription
53
80
  .command("update-payment-method")
54
81
  .description("Replace the workspace's payment method")
@@ -55,13 +55,13 @@ function buildSkillsPinCheck(currentVersion) {
55
55
  };
56
56
  }
57
57
  async function checkApi(config) {
58
- if (config.accessToken === undefined) {
58
+ if (config.getAccessToken === undefined) {
59
59
  return {
60
60
  check: {
61
61
  ok: false,
62
62
  error: {
63
63
  status: "no-credentials",
64
- message: 'Not authenticated. Run "cargo-ai login --token <token>" or "cargo-ai login --oauth".',
64
+ message: 'Not authenticated. Run "cargo-ai login --email <email>" (no browser needed) or "cargo-ai login --oauth".',
65
65
  },
66
66
  },
67
67
  exitCode: ExitCodes.NotAuthenticated,
@@ -69,7 +69,7 @@ async function checkApi(config) {
69
69
  }
70
70
  const client = createApi({
71
71
  baseUrl: config.baseUrl,
72
- accessToken: config.accessToken,
72
+ getAccessToken: config.getAccessToken,
73
73
  workspaceUuid: config.workspaceUuid,
74
74
  });
75
75
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAerC,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,IAAI,CAqD3E"}
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAerC,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,IAAI,CAoD3E"}
@@ -1,4 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
+ import { SessionExpiredError } from "@cargo-ai/cdk/cli";
2
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
5
  import { getConfig } from "../config.js";
@@ -32,14 +33,13 @@ Environment:
32
33
  Reads auth from the same place as every other command (CARGO_API_TOKEN or the
33
34
  credentials file). stdout carries the MCP protocol; all logs go to stderr.`)
34
35
  .action(async (opts) => {
35
- const { accessToken, baseUrl, workspaceUuid } = getConfig();
36
- if (accessToken === undefined) {
37
- failWith('Not authenticated. Run "cargo-ai login --token <token>" or "cargo-ai login --oauth".', { code: ExitCodes.NotAuthenticated });
36
+ const { getAccessToken, baseUrl, workspaceUuid } = getConfig();
37
+ if (getAccessToken === undefined) {
38
+ failWith('Not authenticated. Run "cargo-ai login --email <email>" (no browser needed) or "cargo-ai login --oauth".', { code: ExitCodes.NotAuthenticated });
38
39
  }
39
40
  const serverUuid = await resolveServerUuid(opts.server, getApi);
40
41
  const endpoint = resolveMcpEndpoint(baseUrl, serverUuid);
41
42
  const headers = {
42
- authorization: `Bearer ${accessToken}`,
43
43
  "x-cargo-origin": "cli",
44
44
  "x-cargo-origin-version": version,
45
45
  };
@@ -47,7 +47,7 @@ credentials file). stdout carries the MCP protocol; all logs go to stderr.`)
47
47
  headers["selected-workspace-uuid"] = workspaceUuid;
48
48
  }
49
49
  info(`Cargo MCP bridge -> ${endpoint}`);
50
- await runBridge(endpoint, headers);
50
+ await runBridge(endpoint, headers, getAccessToken);
51
51
  });
52
52
  }
53
53
  const resolveServerUuid = async (explicit, getApi) => {
@@ -101,13 +101,40 @@ const mapApiHostToMcp = (baseUrl) => {
101
101
  // MCP client) and the remote streamable-HTTP client (facing Cargo). Forwarding raw
102
102
  // JSON-RPC keeps the bridge transparent — tools, resources, and long-running-tool
103
103
  // progress notifications all pass through without enumerating capabilities here.
104
- const runBridge = async (endpoint, headers) => {
104
+ const runBridge = async (endpoint, headers, getAccessToken) => {
105
105
  // Prefer a proxied fetch when HTTPS_PROXY/HTTP_PROXY applies — Node's
106
106
  // built-in fetch ignores those env vars, so without this the bridge would
107
107
  // fail in proxied environments even though server discovery (Axios) works.
108
+ const proxyFetch = getProxyFetch(endpoint);
109
+ // The transport reports every failure as an error of its own making, so the
110
+ // reason a request could not be sent is lost by the time the bridge tears
111
+ // down. Hold on to an expired session here: it is the one failure the user
112
+ // can act on, and it has to keep its exit code.
113
+ let sessionExpired;
114
+ // A bridge stays open far longer than an access token lives, so the
115
+ // Authorization header is rebuilt per request instead of being captured once
116
+ // in `requestInit` — that is what lets the session renew mid-session.
117
+ const authenticatedFetch = async (url, init) => {
118
+ let accessToken;
119
+ try {
120
+ accessToken = await getAccessToken();
121
+ }
122
+ catch (error) {
123
+ if (error instanceof SessionExpiredError) {
124
+ sessionExpired = error;
125
+ }
126
+ throw error;
127
+ }
128
+ const merged = new Headers(init !== undefined ? init.headers : undefined);
129
+ merged.set("authorization", `Bearer ${accessToken}`);
130
+ const request = { ...init, headers: merged };
131
+ return proxyFetch !== undefined
132
+ ? proxyFetch(url, request)
133
+ : fetch(url, request);
134
+ };
108
135
  const remote = new StreamableHTTPClientTransport(new URL(endpoint), {
109
136
  requestInit: { headers },
110
- fetch: getProxyFetch(endpoint),
137
+ fetch: authenticatedFetch,
111
138
  });
112
139
  const local = new StdioServerTransport();
113
140
  await new Promise((resolve, reject) => {
@@ -147,7 +174,7 @@ const runBridge = async (endpoint, headers) => {
147
174
  return;
148
175
  state.closed = true;
149
176
  teardown();
150
- reject(new Error(message));
177
+ reject(sessionExpired !== undefined ? sessionExpired : new Error(message));
151
178
  };
152
179
  // Forward raw JSON-RPC both ways. A failing transport reports the reason via
153
180
  // onerror (which fires before the send rejection), so the reason is logged
@@ -39,8 +39,14 @@ export declare function applyRecordsLimit<T>(records: T[], limit: number): {
39
39
  };
40
40
  type HandleApiCallOpts = {
41
41
  spinner?: string | false;
42
+ /**
43
+ * Replaces the default "run cargo-ai login" advice on a 401 — which reads as
44
+ * a loop when the failing call is itself part of signing in.
45
+ */
46
+ notAuthenticatedHint?: string;
42
47
  };
43
48
  export declare function handleApiCall<T>(fn: () => Promise<T>, opts?: HandleApiCallOpts): Promise<T>;
49
+ export declare function sleep(ms: number): Promise<void>;
44
50
  export declare function pollRunUntilFinished<T>(getResult: () => Promise<T>, getStatus: (result: T) => string, intervalMs: number): Promise<T>;
45
51
  export declare function pollBatchUntilFinished<T>(getResult: () => Promise<T>, getStatus: (result: T) => string, intervalMs: number): Promise<T>;
46
52
  export declare function pollMessageUntilFinished<T>(getResult: () => Promise<T>, getFinishedAt: (result: T) => string | null | undefined, intervalMs: number): Promise<T>;
@@ -1 +1 @@
1
- {"version":3,"file":"runHandler.d.ts","sourceRoot":"","sources":["../../src/commands/runHandler.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;;;;;;;CAOZ,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAclE,eAAO,MAAM,MAAM;aACR,MAAM,KAAG,MAAM;eACb,MAAM,KAAG,MAAM;gBACd,MAAM,KAAG,MAAM;cACjB,MAAM,KAAG,MAAM;aAChB,MAAM,KAAG,MAAM;cACd,MAAM,KAAG,MAAM;CAC1B,CAAC;AAEF,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAM/C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAID,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUhE;AAED,KAAK,YAAY,GAAG;IAClB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,KAAK,CAyBpE;AAKD,MAAM,MAAM,OAAO,GAAG;IAEpB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAGlC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CA6CrD;AAGD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,GAAG,CAQhE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ1E;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,OAAO,EAAE,CAAC,EAAE,EACZ,KAAK,EAAE,MAAM,GACZ;IAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CASxD;AAKD,KAAK,iBAAiB,GAAG;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC1B,CAAC;AAEF,wBAAsB,aAAa,CAAC,CAAC,EACnC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,CAAC,CAAC,CAiDZ;AAmFD,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,SAAS,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,EAChC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC,CASZ;AAED,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,SAAS,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,EAChC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC,CASZ;AAED,wBAAsB,wBAAwB,CAAC,CAAC,EAC9C,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,aAAa,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,EACvD,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC,CAWZ"}
1
+ {"version":3,"file":"runHandler.d.ts","sourceRoot":"","sources":["../../src/commands/runHandler.ts"],"names":[],"mappings":"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"}
@@ -57,6 +57,12 @@ export function failWith(message, opts) {
57
57
  ? opts.code
58
58
  : ExitCodes.GenericError;
59
59
  const extra = opts !== undefined ? opts.extra : undefined;
60
+ // This exits the process, so a `finally { spinner.stop() }` around the failing
61
+ // call never runs. Stop it here or the error prints onto a spinning line and
62
+ // the terminal is left with a hidden cursor.
63
+ if (activeSpinner !== undefined) {
64
+ activeSpinner.stop();
65
+ }
60
66
  if (process.stderr.isTTY === true) {
61
67
  process.stderr.write(`${colors.red("✗")} ${message}\n`);
62
68
  if (extra !== undefined) {
@@ -77,7 +83,28 @@ export function failWith(message, opts) {
77
83
  }
78
84
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
79
85
  const SPINNER_INTERVAL_MS = 80;
86
+ let activeSpinner;
80
87
  export function startSpinner(message) {
88
+ // Only one spinner can own the line, so retire any predecessor: otherwise its
89
+ // interval keeps redrawing underneath this one and never gets cleared.
90
+ if (activeSpinner !== undefined) {
91
+ activeSpinner.stop();
92
+ }
93
+ const spinner = createSpinner(message);
94
+ const tracked = {
95
+ update: spinner.update,
96
+ log: spinner.log,
97
+ stop: () => {
98
+ if (activeSpinner === tracked) {
99
+ activeSpinner = undefined;
100
+ }
101
+ spinner.stop();
102
+ },
103
+ };
104
+ activeSpinner = tracked;
105
+ return tracked;
106
+ }
107
+ function createSpinner(message) {
81
108
  if (process.stderr.isTTY !== true) {
82
109
  // Non-TTY (CI, piped): no animation — just emit each line so progress is
83
110
  // still visible in logs.
@@ -182,7 +209,7 @@ export async function handleApiCall(fn, opts) {
182
209
  const parsedError = error;
183
210
  const status = parsedError.status;
184
211
  const body = parsedError.body !== undefined ? parsedError.body : String(error);
185
- const { message, code } = describeApiError(status, body);
212
+ const { message, code } = describeApiError(status, body, opts !== undefined ? opts.notAuthenticatedHint : undefined);
186
213
  failWith(message, {
187
214
  code,
188
215
  extra: { status: status !== undefined ? status : "unknown", body },
@@ -191,12 +218,15 @@ export async function handleApiCall(fn, opts) {
191
218
  throw error;
192
219
  }
193
220
  }
194
- function describeApiError(status, body) {
221
+ function describeApiError(status, body, notAuthenticatedHint) {
195
222
  const detail = extractApiErrorDetail(body);
196
223
  const detailSuffix = detail !== undefined ? `: ${detail}` : "";
197
224
  if (status === 401) {
225
+ const hint = notAuthenticatedHint !== undefined
226
+ ? notAuthenticatedHint
227
+ : "Run `cargo-ai login` to refresh credentials.";
198
228
  return {
199
- message: `Not authenticated${detailSuffix}. Run \`cargo-ai login\` to refresh credentials.`,
229
+ message: `Not authenticated${detailSuffix}. ${hint}`,
200
230
  code: ExitCodes.NotAuthenticated,
201
231
  };
202
232
  }
@@ -247,7 +277,7 @@ function extractApiErrorDetail(body) {
247
277
  }
248
278
  return undefined;
249
279
  }
250
- function sleep(ms) {
280
+ export function sleep(ms) {
251
281
  return new Promise((resolve) => setTimeout(resolve, ms));
252
282
  }
253
283
  const TERMINAL_RUN_STATUSES = new Set([
package/build/config.d.ts CHANGED
@@ -1,9 +1,30 @@
1
+ import type { AccessTokenProvider } from "@cargo-ai/cdk/cli";
1
2
  export type ConfigSource = "environment" | "credentials-file" | "none";
2
3
  export type Config = {
3
- accessToken: string | undefined;
4
+ /**
5
+ * Resolves the token for each request, renewing the session first when it is
6
+ * about to expire. `undefined` means no credential at all.
7
+ */
8
+ getAccessToken: AccessTokenProvider | undefined;
4
9
  baseUrl: string;
5
10
  workspaceUuid: string | undefined;
6
11
  source: ConfigSource;
7
12
  };
8
13
  export declare function getConfig(): Config;
14
+ /**
15
+ * An API token is bound to one workspace by the server, which derives the
16
+ * workspace from the token and ignores the one we send alongside it. Pointing
17
+ * CARGO_WORKSPACE_UUID somewhere else would therefore run against the token's
18
+ * workspace while looking like it worked, so refuse instead of silently
19
+ * touching the wrong data. A signed-in session carries no such binding: the
20
+ * server honours the workspace header, so the override just works.
21
+ *
22
+ * Exported for tests; `getConfig` is the only production caller.
23
+ */
24
+ export declare function describeWorkspaceOverrideConflict(opts: {
25
+ isWorkspaceBoundToken: boolean;
26
+ tokenSource: ConfigSource;
27
+ credentialsWorkspaceUuid: string | undefined;
28
+ requestedWorkspaceUuid: string | undefined;
29
+ }): string | undefined;
9
30
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAEvE,MAAM,MAAM,MAAM,GAAG;IACnB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,MAAM,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,wBAAgB,SAAS,IAAI,MAAM,CAclC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAO7D,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAEvE,MAAM,MAAM,MAAM,GAAG;IACnB;;;OAGG;IACH,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,MAAM,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,wBAAgB,SAAS,IAAI,MAAM,CA0BlC;AAED;;;;;;;;;GASG;AACH,wBAAgB,iCAAiC,CAAC,IAAI,EAAE;IACtD,qBAAqB,EAAE,OAAO,CAAC;IAC/B,WAAW,EAAE,YAAY,CAAC;IAC1B,wBAAwB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,sBAAsB,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5C,GAAG,MAAM,GAAG,SAAS,CA0BrB"}
package/build/config.js CHANGED
@@ -1,15 +1,56 @@
1
- import { loadProjectEnv, resolveAuthConfig } from "@cargo-ai/cdk/cli";
1
+ import { loadProjectEnv, resolveAuth } from "@cargo-ai/cdk/cli";
2
+ import { ExitCodes, failWith } from "./commands/runHandler.js";
2
3
  import { loadCredentials } from "./credentials.js";
3
4
  export function getConfig() {
4
5
  // Fold project `.env` into the environment first (never overriding explicit
5
6
  // exports): a repo pinned to one workspace must win over a personal login.
6
7
  loadProjectEnv();
7
8
  const credentials = loadCredentials();
8
- const resolved = resolveAuthConfig(process.env, credentials);
9
+ const resolved = resolveAuth(process.env, credentials);
10
+ const conflict = describeWorkspaceOverrideConflict({
11
+ isWorkspaceBoundToken: resolved.isWorkspaceBoundToken,
12
+ tokenSource: resolved.tokenSource,
13
+ credentialsWorkspaceUuid: credentials !== undefined ? credentials.workspaceUuid : undefined,
14
+ requestedWorkspaceUuid: resolved.workspaceUuid,
15
+ });
16
+ if (conflict !== undefined) {
17
+ failWith(conflict, { code: ExitCodes.InvalidUsage });
18
+ }
9
19
  return {
10
- accessToken: resolved.accessToken,
20
+ getAccessToken: resolved.getAccessToken,
11
21
  baseUrl: resolved.baseUrl,
12
22
  workspaceUuid: resolved.workspaceUuid,
13
23
  source: resolved.tokenSource,
14
24
  };
15
25
  }
26
+ /**
27
+ * An API token is bound to one workspace by the server, which derives the
28
+ * workspace from the token and ignores the one we send alongside it. Pointing
29
+ * CARGO_WORKSPACE_UUID somewhere else would therefore run against the token's
30
+ * workspace while looking like it worked, so refuse instead of silently
31
+ * touching the wrong data. A signed-in session carries no such binding: the
32
+ * server honours the workspace header, so the override just works.
33
+ *
34
+ * Exported for tests; `getConfig` is the only production caller.
35
+ */
36
+ export function describeWorkspaceOverrideConflict(opts) {
37
+ if (opts.isWorkspaceBoundToken === false) {
38
+ return undefined;
39
+ }
40
+ if (opts.tokenSource !== "credentials-file") {
41
+ return undefined;
42
+ }
43
+ if (opts.credentialsWorkspaceUuid === undefined ||
44
+ opts.requestedWorkspaceUuid === undefined) {
45
+ return undefined;
46
+ }
47
+ if (opts.requestedWorkspaceUuid === opts.credentialsWorkspaceUuid) {
48
+ return undefined;
49
+ }
50
+ return [
51
+ `Workspace ${opts.requestedWorkspaceUuid} was requested, but the saved credential is an API token scoped to workspace ${opts.credentialsWorkspaceUuid}.`,
52
+ "The server takes the workspace from the token, so the request would silently run against the token's workspace.",
53
+ `Sign in for that workspace instead: cargo-ai login --email <email> --workspace-uuid ${opts.requestedWorkspaceUuid}`,
54
+ "Or unset CARGO_WORKSPACE_UUID to use the workspace the saved token belongs to.",
55
+ ].join(" ");
56
+ }
@@ -1,11 +1,2 @@
1
- export type Credentials = {
2
- accessToken: string;
3
- workspaceUuid?: string;
4
- baseUrl?: string;
5
- };
6
- export declare function loadCredentials(): Credentials | undefined;
7
- export declare function saveCredentials(creds: Credentials): void;
8
- export declare function clearCredentials(): boolean;
9
- export declare function getCredentialsPath(): string;
10
- export declare function getConfigDir(): string;
1
+ export { clearCredentials, type StoredCredentials as Credentials, getConfigDir, getCredentialsPath, loadCredentials, type RefreshableSession, saveCredentials, } from "@cargo-ai/cdk/cli";
11
2
  //# sourceMappingURL=credentials.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,WAAW,GAAG;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAKF,wBAAgB,eAAe,IAAI,WAAW,GAAG,SAAS,CAQzD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAKxD;AAED,wBAAgB,gBAAgB,IAAI,OAAO,CAI1C;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC"}
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,gBAAgB,EAChB,KAAK,iBAAiB,IAAI,WAAW,EACrC,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,KAAK,kBAAkB,EACvB,eAAe,GAChB,MAAM,mBAAmB,CAAC"}
@@ -1,34 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- const CONFIG_DIR = join(homedir(), ".config", "cargo-ai");
5
- const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
6
- export function loadCredentials() {
7
- if (existsSync(CREDENTIALS_FILE) === false)
8
- return undefined;
9
- try {
10
- const raw = readFileSync(CREDENTIALS_FILE, "utf-8");
11
- return JSON.parse(raw);
12
- }
13
- catch {
14
- return undefined;
15
- }
16
- }
17
- export function saveCredentials(creds) {
18
- mkdirSync(CONFIG_DIR, { recursive: true });
19
- writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2) + "\n", {
20
- mode: 0o600,
21
- });
22
- }
23
- export function clearCredentials() {
24
- if (existsSync(CREDENTIALS_FILE) === false)
25
- return false;
26
- unlinkSync(CREDENTIALS_FILE);
27
- return true;
28
- }
29
- export function getCredentialsPath() {
30
- return CREDENTIALS_FILE;
31
- }
32
- export function getConfigDir() {
33
- return CONFIG_DIR;
34
- }
1
+ // The credentials file is shared with `cargo-cdk`, so its format and access
2
+ // live in @cargo-ai/cdk/cli one login serves both binaries and neither can
3
+ // drift from the other's idea of what is on disk.
4
+ export { clearCredentials, getConfigDir, getCredentialsPath, loadCredentials, saveCredentials, } from "@cargo-ai/cdk/cli";
package/build/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { registerCommands as registerCdkCommands, registerManifestCommands, } from "@cargo-ai/cdk/cli";
3
+ import { registerCommands as registerCdkCommands, registerManifestCommands, SessionExpiredError, } from "@cargo-ai/cdk/cli";
4
4
  import { Command } from "commander";
5
5
  import { createApi } from "./api.js";
6
6
  import { registerAiCommands } from "./commands/ai/index.js";
@@ -35,8 +35,9 @@ program
35
35
  .version(version)
36
36
  .addHelpText("after", `
37
37
  Authentication:
38
- With API token: cargo-ai login --token <your-token>
38
+ Emailed code: cargo-ai login --email you@company.com (no browser; signs you up on first use)
39
39
  Browser sign-in: cargo-ai login --oauth
40
+ With API token: cargo-ai login --token <your-token>
40
41
  Use env variables: CARGO_API_TOKEN, CARGO_WORKSPACE_UUID, CARGO_BASE_URL
41
42
  Check status: cargo-ai whoami
42
43
 
@@ -53,11 +54,11 @@ Exit codes:
53
54
  Run "cargo-ai <command> --help" for details on a specific command group.
54
55
  Run "cargo-ai <command> <subcommand> --help" for details on a subcommand.`);
55
56
  const getApi = () => {
56
- const { baseUrl, accessToken, workspaceUuid } = getConfig();
57
- if (accessToken === undefined) {
58
- failWith("Not authenticated. Run 'cargo-ai login --token <token>' or 'cargo-ai login --oauth'.", { code: ExitCodes.NotAuthenticated });
57
+ const { baseUrl, getAccessToken, workspaceUuid } = getConfig();
58
+ if (getAccessToken === undefined) {
59
+ failWith("Not authenticated. Run 'cargo-ai login --email <email>' (no browser needed) or 'cargo-ai login --oauth'.", { code: ExitCodes.NotAuthenticated });
59
60
  }
60
- return createApi({ baseUrl, accessToken, workspaceUuid });
61
+ return createApi({ baseUrl, getAccessToken, workspaceUuid });
61
62
  };
62
63
  registerAuthCommands(program, getApi);
63
64
  registerVersionCommand(program, version);
@@ -89,5 +90,11 @@ program
89
90
  .parseAsync()
90
91
  .then(() => maybeNotifyUpdate(version))
91
92
  .catch((err) => {
92
- failWith(err instanceof Error ? err.message : String(err));
93
+ // An expired session is the ordinary way to lose authentication, so it has
94
+ // to carry the documented exit code rather than the generic one.
95
+ failWith(err instanceof Error ? err.message : String(err), {
96
+ code: err instanceof SessionExpiredError
97
+ ? ExitCodes.NotAuthenticated
98
+ : undefined,
99
+ });
93
100
  });
@@ -4,10 +4,5 @@ export type OAuthConfig = {
4
4
  audience: string;
5
5
  scope: string;
6
6
  };
7
- export type OAuthConfigOverrides = {
8
- domain?: string;
9
- clientId?: string;
10
- audience?: string;
11
- };
12
- export declare function getOAuthConfig(overrides: OAuthConfigOverrides): OAuthConfig;
7
+ export declare function getOAuthConfig(): OAuthConfig;
13
8
  //# sourceMappingURL=oauthConfig.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"oauthConfig.d.ts","sourceRoot":"","sources":["../src/oauthConfig.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAOF,wBAAgB,cAAc,CAAC,SAAS,EAAE,oBAAoB,GAAG,WAAW,CAO3E"}
1
+ {"version":3,"file":"oauthConfig.d.ts","sourceRoot":"","sources":["../src/oauthConfig.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAOF,wBAAgB,cAAc,IAAI,WAAW,CAO5C"}
@@ -1,17 +1,12 @@
1
- const DEFAULT_DOMAIN = "auth.getcargo.io";
2
- const DEFAULT_AUDIENCE = "https://api.getcargo.io";
3
- const DEFAULT_SCOPE = "openid profile email offline_access";
4
- const DEFAULT_CLIENT_ID = "oUc99KlLSMAYUAPgxTUyqazef0sxR93b";
5
- export function getOAuthConfig(overrides) {
1
+ const DOMAIN = "auth.getcargo.io";
2
+ const AUDIENCE = "https://api.getcargo.io";
3
+ const SCOPE = "openid profile email offline_access";
4
+ const CLIENT_ID = "oUc99KlLSMAYUAPgxTUyqazef0sxR93b";
5
+ export function getOAuthConfig() {
6
6
  return {
7
- domain: pickString(overrides.domain, DEFAULT_DOMAIN),
8
- audience: pickString(overrides.audience, DEFAULT_AUDIENCE),
9
- clientId: pickString(overrides.clientId, DEFAULT_CLIENT_ID),
10
- scope: DEFAULT_SCOPE,
7
+ domain: DOMAIN,
8
+ audience: AUDIENCE,
9
+ clientId: CLIENT_ID,
10
+ scope: SCOPE,
11
11
  };
12
12
  }
13
- const pickString = (override, fallback) => {
14
- if (override !== undefined && override.length > 0)
15
- return override;
16
- return fallback;
17
- };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Best-effort browser launch. Every caller also prints the URL, so a failure
3
+ * here is never fatal: headless boxes and sandboxes simply fall back to the
4
+ * user opening the link themselves.
5
+ */
6
+ export declare function openBrowser(url: string): void;
7
+ //# sourceMappingURL=openBrowser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openBrowser.d.ts","sourceRoot":"","sources":["../../src/utils/openBrowser.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAc7C"}
@@ -0,0 +1,29 @@
1
+ import { spawn } from "node:child_process";
2
+ import { platform } from "node:process";
3
+ /**
4
+ * Best-effort browser launch. Every caller also prints the URL, so a failure
5
+ * here is never fatal: headless boxes and sandboxes simply fall back to the
6
+ * user opening the link themselves.
7
+ */
8
+ export function openBrowser(url) {
9
+ const command = pickOpenCommand();
10
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
11
+ try {
12
+ const child = spawn(command, args, {
13
+ detached: true,
14
+ stdio: "ignore",
15
+ });
16
+ child.on("error", () => undefined);
17
+ child.unref();
18
+ }
19
+ catch {
20
+ // Intentionally ignored — see above.
21
+ }
22
+ }
23
+ function pickOpenCommand() {
24
+ if (platform === "darwin")
25
+ return "open";
26
+ if (platform === "win32")
27
+ return "cmd";
28
+ return "xdg-open";
29
+ }
@@ -0,0 +1,12 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ export type PromptInput = {
3
+ stream: NodeJS.ReadableStream;
4
+ close: () => void;
5
+ };
6
+ export declare function openPromptInput(): PromptInput | undefined;
7
+ /**
8
+ * Asks a single question on the controlling terminal. Prompts are written to
9
+ * stderr so that piping stdout to `jq` keeps working.
10
+ */
11
+ export declare function askQuestion(input: NodeJS.ReadableStream, question: string): Promise<string | undefined>;
12
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/utils/prompt.ts"],"names":[],"mappings":";AAIA,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAMF,wBAAgB,eAAe,IAAI,WAAW,GAAG,SAAS,CAYzD;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,CAAC,cAAc,EAC5B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAe7B"}