@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.
- package/README.md +50 -10
- package/build/api.d.ts +2 -1
- package/build/api.d.ts.map +1 -1
- package/build/api.js +5 -1
- package/build/commands/auth/auth0.d.ts +33 -0
- package/build/commands/auth/auth0.d.ts.map +1 -0
- package/build/commands/auth/auth0.js +90 -0
- package/build/commands/auth/email.d.ts +16 -0
- package/build/commands/auth/email.d.ts.map +1 -0
- package/build/commands/auth/email.js +93 -0
- package/build/commands/auth/index.d.ts +19 -1
- package/build/commands/auth/index.d.ts.map +1 -1
- package/build/commands/auth/index.js +121 -59
- package/build/commands/auth/loginChannel.d.ts +21 -0
- package/build/commands/auth/loginChannel.d.ts.map +1 -0
- package/build/commands/auth/loginChannel.js +42 -0
- package/build/commands/auth/oauth.d.ts +2 -6
- package/build/commands/auth/oauth.d.ts.map +1 -1
- package/build/commands/auth/oauth.js +54 -91
- package/build/commands/auth/passwordless.d.ts +22 -0
- package/build/commands/auth/passwordless.d.ts.map +1 -0
- package/build/commands/auth/passwordless.js +70 -0
- package/build/commands/auth/session.d.ts +21 -0
- package/build/commands/auth/session.d.ts.map +1 -0
- package/build/commands/auth/session.js +55 -0
- package/build/commands/auth/workspace.d.ts +9 -1
- package/build/commands/auth/workspace.d.ts.map +1 -1
- package/build/commands/auth/workspace.js +42 -69
- package/build/commands/billing/addPaymentMethod.d.ts +26 -0
- package/build/commands/billing/addPaymentMethod.d.ts.map +1 -0
- package/build/commands/billing/addPaymentMethod.js +57 -0
- package/build/commands/billing/subscription.d.ts.map +1 -1
- package/build/commands/billing/subscription.js +28 -1
- package/build/commands/doctor.js +3 -3
- package/build/commands/mcp.d.ts.map +1 -1
- package/build/commands/mcp.js +35 -8
- package/build/commands/runHandler.d.ts +6 -0
- package/build/commands/runHandler.d.ts.map +1 -1
- package/build/commands/runHandler.js +34 -4
- package/build/config.d.ts +22 -1
- package/build/config.d.ts.map +1 -1
- package/build/config.js +44 -3
- package/build/credentials.d.ts +1 -10
- package/build/credentials.d.ts.map +1 -1
- package/build/credentials.js +4 -34
- package/build/index.js +14 -7
- package/build/oauthConfig.d.ts +1 -6
- package/build/oauthConfig.d.ts.map +1 -1
- package/build/oauthConfig.js +9 -14
- package/build/utils/openBrowser.d.ts +7 -0
- package/build/utils/openBrowser.d.ts.map +1 -0
- package/build/utils/openBrowser.js +29 -0
- package/build/utils/prompt.d.ts +12 -0
- package/build/utils/prompt.d.ts.map +1 -0
- package/build/utils/prompt.js +38 -0
- 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;
|
|
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")
|
package/build/commands/doctor.js
CHANGED
|
@@ -55,13 +55,13 @@ function buildSkillsPinCheck(currentVersion) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
async function checkApi(config) {
|
|
58
|
-
if (config.
|
|
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 --
|
|
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
|
-
|
|
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":"
|
|
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"}
|
package/build/commands/mcp.js
CHANGED
|
@@ -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 {
|
|
36
|
-
if (
|
|
37
|
-
failWith('Not authenticated. Run "cargo-ai login --
|
|
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:
|
|
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,
|
|
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}.
|
|
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
|
-
|
|
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
|
package/build/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
+
}
|
package/build/credentials.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/build/credentials.js
CHANGED
|
@@ -1,34 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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,
|
|
57
|
-
if (
|
|
58
|
-
failWith("Not authenticated. Run 'cargo-ai login --
|
|
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,
|
|
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
|
-
|
|
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
|
});
|
package/build/oauthConfig.d.ts
CHANGED
|
@@ -4,10 +4,5 @@ export type OAuthConfig = {
|
|
|
4
4
|
audience: string;
|
|
5
5
|
scope: string;
|
|
6
6
|
};
|
|
7
|
-
export
|
|
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;
|
|
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"}
|
package/build/oauthConfig.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
export function getOAuthConfig(
|
|
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:
|
|
8
|
-
audience:
|
|
9
|
-
clientId:
|
|
10
|
-
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"}
|