@cargo-ai/cli 1.0.113 → 1.0.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,7 @@ Using a coding agent? Skip the manual setup — see [Agent Skills](#agent-skills
51
51
 
52
52
  ## Authentication
53
53
 
54
- A single `cargo-ai login` command supports three channels — pass exactly one of `--email`, `--oauth` or `--token`:
54
+ A single `cargo-ai login` command supports four channels — pass exactly one of `--email`, `--oauth`, `--agentid` or `--token`:
55
55
 
56
56
  ```bash
57
57
  # 1. Sign in with a code emailed to you, no browser needed
@@ -60,13 +60,16 @@ cargo-ai login --email you@company.com
60
60
  # 2. Sign in via your browser using the OAuth 2.0 Device Authorization Flow
61
61
  cargo-ai login --oauth
62
62
 
63
- # 3. Sign in with an API token you already have
63
+ # 3. Sign an agent in through AgentID
64
+ cargo-ai login --agentid
65
+
66
+ # 4. Sign in with an API token you already have
64
67
  cargo-ai login --token <your-api-token>
65
68
  ```
66
69
 
67
- `--email` and `--oauth` both create the account if it does not exist yet, so there is no separate sign-up command.
70
+ `--email`, `--oauth` and `--agentid` all create the account if it does not exist yet, so there is no separate sign-up command.
68
71
 
69
- `--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.
72
+ `--email`, `--oauth` and `--agentid` 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.
70
73
 
71
74
  `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. If a credentials file cannot be deleted — a read-only config directory, or one owned by another user — `logout` fails and names the file rather than reporting a sign-out the next command would contradict.
72
75
 
@@ -114,9 +117,23 @@ echo 123456 | cargo-ai login --email you@company.com --code -
114
117
 
115
118
  The CLI ships with a built-in OAuth client, so no setup is required.
116
119
 
120
+ ### AgentID sign-in (`--agentid`)
121
+
122
+ `cargo-ai login --agentid` signs an agent in as itself, through the same AgentID connection the web app offers. It does not open the Cargo app.
123
+
124
+ The command prints an Auth0 authorize URL (`connection=agentid`) and listens on `http://127.0.0.1` for the redirect. Open that URL once. A second visit starts a different sign-in. Auth0 sends the browser to `https://auth.agentid.com`. When that page shows an auth token, approve it within 5 minutes:
125
+
126
+ ```bash
127
+ agentmail inboxes authorize --inbox-id <inbox> --auth-token <token> --accept-disclosure true
128
+ ```
129
+
130
+ AgentID then redirects back to the CLI, which writes the session the same way `--email` and `--oauth` do. A browser that already has an AgentID session for that inbox continues without the token step.
131
+
132
+ The cargo-ai application in Auth0 has to list `http://127.0.0.1:52701/callback` under Allowed Callback URLs, and have the AgentID connection enabled. Auth0 does not accept a wildcard port, so the CLI always uses this one. Until that URL is allowed, the command stops immediately and says so.
133
+
117
134
  ### Workspace selection
118
135
 
119
- `--email` and `--oauth` authenticate you as a user, so the CLI picks a workspace to save alongside the session:
136
+ `--email`, `--oauth` and `--agentid` authenticate you as a user, so the CLI picks a workspace to save alongside the session:
120
137
 
121
138
  - **No workspaces**: one is created (you are prompted for a name in a TTY, or pass `--workspace-name <name>`).
122
139
  - **One workspace**: it is selected automatically.
@@ -1,4 +1,4 @@
1
- import { cdkProjectRoot } from "@cargo-ai/cdk/cli";
1
+ import { findProjectDir } from "@cargo-ai/cdk/cli";
2
2
  import { buildReleaseInstructionAppendix } from "./release.js";
3
3
  const LOCAL_WORKING_COPY_INSTRUCTIONS = `You are working in the user's local working copy, not a sandbox. Prefer plan over deploy.
4
4
 
@@ -21,7 +21,7 @@ const LOCAL_CDK_RAILS_SKILL = {
21
21
  * Exported for tests.
22
22
  */
23
23
  export function localHarnessInstructions(dir, release) {
24
- const rails = cdkProjectRoot(dir) !== undefined
24
+ const rails = findProjectDir(dir) !== undefined
25
25
  ? `${LOCAL_WORKING_COPY_INSTRUCTIONS}\n\n${CDK_RAILS_INSTRUCTIONS}`
26
26
  : LOCAL_WORKING_COPY_INSTRUCTIONS;
27
27
  if (release === undefined) {
@@ -36,7 +36,7 @@ export function localHarnessInstructions(dir, release) {
36
36
  }
37
37
  /** Exported for tests. */
38
38
  export function localHarnessSkills(dir) {
39
- if (cdkProjectRoot(dir) !== undefined) {
39
+ if (findProjectDir(dir) !== undefined) {
40
40
  return [LOCAL_CDK_RAILS_SKILL];
41
41
  }
42
42
  return [];
@@ -0,0 +1,67 @@
1
+ import { type IdentityTokens } from "./auth0.js";
2
+ /** Auth0 connection name. Matches the web sign-in button. */
3
+ export declare const AGENTID_CONNECTION = "agentid";
4
+ /**
5
+ * Auth0 refuses a wildcard port in Allowed Callback URLs, so the CLI always
6
+ * listens here. The cargo-ai application must allow this exact URL.
7
+ */
8
+ export declare const AGENTID_CALLBACK_PORT = 52701;
9
+ export declare function loopbackRedirectUri(port: number): string;
10
+ export declare const AGENTID_CALLBACK_URL: string;
11
+ export declare function buildAgentIdAuthorizeUrl(opts: {
12
+ domain: string;
13
+ clientId: string;
14
+ audience: string;
15
+ scope: string;
16
+ redirectUri: string;
17
+ state: string;
18
+ codeChallenge: string;
19
+ }): string;
20
+ export type AuthorizeCheck = {
21
+ ok: true;
22
+ } | {
23
+ ok: false;
24
+ message: string;
25
+ };
26
+ /**
27
+ * The device-code flow cannot name a connection, so AgentID sign-in starts
28
+ * with a normal authorize request and a loopback redirect. Auth0 answers that
29
+ * request before any browser work; a callback that is not allowed, or a
30
+ * connection that is off, fails here instead of after a five-minute wait.
31
+ *
32
+ * A started sign-in looks like exactly one thing: a redirect away from the
33
+ * Auth0 domain, to AgentID. A page served from Auth0 itself — universal login,
34
+ * an error — means the sign-in never left for AgentID.
35
+ */
36
+ export declare function classifyAuthorizeResponse(opts: {
37
+ domain: string;
38
+ status: number;
39
+ location: string | null;
40
+ body: string;
41
+ }): AuthorizeCheck;
42
+ export type CallbackResult = {
43
+ code: string;
44
+ } | {
45
+ error: string;
46
+ };
47
+ export declare function readCallbackResult(search: string): CallbackResult;
48
+ /**
49
+ * Only a browser navigation that carries this login's `state` and the
50
+ * authorization result may end the wait. The callback port is fixed and
51
+ * documented, so anything else that reaches it — a probe, a preflight, a
52
+ * cross-site request, a redirect left over from an earlier run — has to leave
53
+ * the listener up rather than settle the wait and fail the sign-in. Auth0
54
+ * returns `state` on error redirects too, so a denial still gets through.
55
+ */
56
+ export declare function isSignInCallback(method: string, search: string, expectedState: string): boolean;
57
+ /**
58
+ * Signs in through the AgentID Auth0 connection and returns the same session
59
+ * tokens the other channels hand to `establishSession`.
60
+ *
61
+ * The command prints an authorize URL on Auth0. Opening it lands on
62
+ * auth.agentid.com, never on the Cargo app. The agent approves the auth token
63
+ * shown on that page with `agentmail inboxes authorize`. AgentID then redirects
64
+ * back to a localhost port this process is listening on.
65
+ */
66
+ export declare function runAgentIdLogin(): Promise<IdentityTokens>;
67
+ //# sourceMappingURL=agentId.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentId.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/agentId.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,YAAY,CAAC;AAEpB,6DAA6D;AAC7D,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAO5C;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAQ,CAAC;AAE3C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,eAAO,MAAM,oBAAoB,QAA6C,CAAC;AAE/E,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,MAAM,CAcT;AAED,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAS3E;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,cAAc,CA6BjB;AAgCD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAmBjE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAcT;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,cAAc,CAAC,CAkE/D"}
@@ -0,0 +1,332 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createServer, } from "node:http";
3
+ import { getOAuthConfig } from "../../oauthConfig.js";
4
+ import { failWith, info } from "../runHandler.js";
5
+ import { describeError, parseTokenResponse, postToAuth0, readErrorBody, USER_AGENT, } from "./auth0.js";
6
+ /** Auth0 connection name. Matches the web sign-in button. */
7
+ export const AGENTID_CONNECTION = "agentid";
8
+ /** AgentID drops the waiting page after five minutes. */
9
+ const SIGN_IN_TIMEOUT_MS = 5 * 60 * 1000;
10
+ const CALLBACK_PATH = "/callback";
11
+ /**
12
+ * Auth0 refuses a wildcard port in Allowed Callback URLs, so the CLI always
13
+ * listens here. The cargo-ai application must allow this exact URL.
14
+ */
15
+ export const AGENTID_CALLBACK_PORT = 52701;
16
+ export function loopbackRedirectUri(port) {
17
+ return `http://127.0.0.1:${String(port)}${CALLBACK_PATH}`;
18
+ }
19
+ export const AGENTID_CALLBACK_URL = loopbackRedirectUri(AGENTID_CALLBACK_PORT);
20
+ export function buildAgentIdAuthorizeUrl(opts) {
21
+ const params = new URLSearchParams({
22
+ response_type: "code",
23
+ client_id: opts.clientId,
24
+ redirect_uri: opts.redirectUri,
25
+ scope: opts.scope,
26
+ audience: opts.audience,
27
+ connection: AGENTID_CONNECTION,
28
+ state: opts.state,
29
+ code_challenge: opts.codeChallenge,
30
+ code_challenge_method: "S256",
31
+ });
32
+ return `https://${opts.domain}/authorize?${params.toString()}`;
33
+ }
34
+ /**
35
+ * Auth0 sent the request to its own universal login page. That page is the
36
+ * Cargo sign-in form, which an agent cannot complete, so the connection never
37
+ * took effect.
38
+ */
39
+ const CONNECTION_NOT_APPLIED = `Auth0 answered with its own sign-in page instead of handing the request to AgentID, so the "${AGENTID_CONNECTION}" connection did not take effect. Enable it on the cargo-ai Auth0 application, then try again.`;
40
+ /**
41
+ * The device-code flow cannot name a connection, so AgentID sign-in starts
42
+ * with a normal authorize request and a loopback redirect. Auth0 answers that
43
+ * request before any browser work; a callback that is not allowed, or a
44
+ * connection that is off, fails here instead of after a five-minute wait.
45
+ *
46
+ * A started sign-in looks like exactly one thing: a redirect away from the
47
+ * Auth0 domain, to AgentID. A page served from Auth0 itself — universal login,
48
+ * an error — means the sign-in never left for AgentID.
49
+ */
50
+ export function classifyAuthorizeResponse(opts) {
51
+ if (opts.body.includes("Callback URL mismatch") === true) {
52
+ return {
53
+ ok: false,
54
+ message: `Auth0 rejected the CLI callback (${AGENTID_CALLBACK_URL}). On the cargo-ai Auth0 application, add that exact URL to Allowed Callback URLs and enable the AgentID connection, then try again.`,
55
+ };
56
+ }
57
+ if (opts.status >= 300 && opts.status < 400 && opts.location !== null) {
58
+ return classifyRedirect(opts.domain, opts.location);
59
+ }
60
+ const detail = htmlError(opts.body);
61
+ if (detail !== undefined) {
62
+ return {
63
+ ok: false,
64
+ message: `Auth0 refused to start AgentID sign-in: ${detail}`,
65
+ };
66
+ }
67
+ if (opts.status >= 200 && opts.status < 300) {
68
+ return { ok: false, message: CONNECTION_NOT_APPLIED };
69
+ }
70
+ return {
71
+ ok: false,
72
+ message: `Auth0 refused to start AgentID sign-in: HTTP ${String(opts.status)}`,
73
+ };
74
+ }
75
+ function classifyRedirect(domain, location) {
76
+ let url;
77
+ try {
78
+ url = new URL(location, `https://${domain}`);
79
+ }
80
+ catch {
81
+ return {
82
+ ok: false,
83
+ message: `Auth0 answered the AgentID sign-in request with a redirect the CLI could not read (${location}).`,
84
+ };
85
+ }
86
+ const error = url.searchParams.get("error");
87
+ if (error !== null) {
88
+ const description = url.searchParams.get("error_description");
89
+ return {
90
+ ok: false,
91
+ message: `Auth0 refused to start AgentID sign-in: ${description !== null ? description : error}`,
92
+ };
93
+ }
94
+ if (url.host === domain) {
95
+ return { ok: false, message: CONNECTION_NOT_APPLIED };
96
+ }
97
+ return { ok: true };
98
+ }
99
+ export function readCallbackResult(search) {
100
+ const params = new URLSearchParams(search.startsWith("?") === true ? search.slice(1) : search);
101
+ const error = params.get("error");
102
+ if (error !== null) {
103
+ const description = params.get("error_description");
104
+ return { error: description !== null ? description : error };
105
+ }
106
+ const code = params.get("code");
107
+ if (code === null) {
108
+ return { error: "The sign-in redirect did not include a code." };
109
+ }
110
+ return { code };
111
+ }
112
+ /**
113
+ * Only a browser navigation that carries this login's `state` and the
114
+ * authorization result may end the wait. The callback port is fixed and
115
+ * documented, so anything else that reaches it — a probe, a preflight, a
116
+ * cross-site request, a redirect left over from an earlier run — has to leave
117
+ * the listener up rather than settle the wait and fail the sign-in. Auth0
118
+ * returns `state` on error redirects too, so a denial still gets through.
119
+ */
120
+ export function isSignInCallback(method, search, expectedState) {
121
+ if (method !== "GET") {
122
+ return false;
123
+ }
124
+ const params = new URLSearchParams(search.startsWith("?") === true ? search.slice(1) : search);
125
+ if (params.get("state") !== expectedState) {
126
+ return false;
127
+ }
128
+ return params.get("error") !== null || params.get("code") !== null;
129
+ }
130
+ /**
131
+ * Signs in through the AgentID Auth0 connection and returns the same session
132
+ * tokens the other channels hand to `establishSession`.
133
+ *
134
+ * The command prints an authorize URL on Auth0. Opening it lands on
135
+ * auth.agentid.com, never on the Cargo app. The agent approves the auth token
136
+ * shown on that page with `agentmail inboxes authorize`. AgentID then redirects
137
+ * back to a localhost port this process is listening on.
138
+ */
139
+ export async function runAgentIdLogin() {
140
+ const config = getOAuthConfig();
141
+ const state = randomBytes(16).toString("base64url");
142
+ const verifier = randomBytes(32).toString("base64url");
143
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
144
+ let listener;
145
+ try {
146
+ listener = await listenForCallback(state);
147
+ }
148
+ catch (error) {
149
+ failWith(error instanceof Error ? error.message : String(error));
150
+ }
151
+ try {
152
+ const redirectUri = loopbackRedirectUri(listener.port);
153
+ const authorizeUrl = buildAgentIdAuthorizeUrl({
154
+ domain: config.domain,
155
+ clientId: config.clientId,
156
+ audience: config.audience,
157
+ scope: config.scope,
158
+ redirectUri,
159
+ state,
160
+ codeChallenge: challenge,
161
+ });
162
+ await assertAuthorizeStarts({ authorizeUrl, domain: config.domain });
163
+ info([
164
+ "",
165
+ "To sign in with AgentID, open this URL once. It goes to Auth0 and then AgentID, not the Cargo app:",
166
+ ` ${authorizeUrl}`,
167
+ "",
168
+ "Open it only once. A second visit starts a different AgentID sign-in, and approving that one does not finish this command.",
169
+ "",
170
+ "If that page shows an auth token, approve it within 5 minutes:",
171
+ " agentmail inboxes authorize --inbox-id <inbox> --auth-token <token> --accept-disclosure true",
172
+ "",
173
+ "Waiting for that page to redirect back here...",
174
+ "",
175
+ ].join("\n"));
176
+ let callback;
177
+ try {
178
+ callback = await listener.done;
179
+ }
180
+ catch (error) {
181
+ failWith(error instanceof Error ? error.message : String(error));
182
+ }
183
+ const result = readCallbackResult(callback);
184
+ if ("error" in result) {
185
+ failWith(`AgentID sign-in failed: ${result.error}`);
186
+ }
187
+ return await exchangeAuthorizationCode({
188
+ code: result.code,
189
+ redirectUri,
190
+ codeVerifier: verifier,
191
+ });
192
+ }
193
+ finally {
194
+ listener.close();
195
+ }
196
+ }
197
+ async function assertAuthorizeStarts(opts) {
198
+ let response;
199
+ try {
200
+ response = await fetch(opts.authorizeUrl, {
201
+ method: "GET",
202
+ redirect: "manual",
203
+ headers: { "user-agent": USER_AGENT },
204
+ signal: AbortSignal.timeout(15_000),
205
+ });
206
+ }
207
+ catch (error) {
208
+ const reason = error instanceof Error ? error.message : String(error);
209
+ failWith(`Could not reach Auth0 to start AgentID sign-in: ${reason}`);
210
+ }
211
+ const check = classifyAuthorizeResponse({
212
+ domain: opts.domain,
213
+ status: response.status,
214
+ location: response.headers.get("location"),
215
+ body: await response.text(),
216
+ });
217
+ if (check.ok === false) {
218
+ failWith(check.message);
219
+ }
220
+ }
221
+ async function exchangeAuthorizationCode(opts) {
222
+ const config = getOAuthConfig();
223
+ const response = await postToAuth0({
224
+ path: "/oauth/token",
225
+ body: new URLSearchParams({
226
+ grant_type: "authorization_code",
227
+ client_id: config.clientId,
228
+ code: opts.code,
229
+ redirect_uri: opts.redirectUri,
230
+ code_verifier: opts.codeVerifier,
231
+ }),
232
+ });
233
+ if (response.ok === false) {
234
+ const errorBody = await readErrorBody(response);
235
+ failWith(`Failed to exchange the AgentID sign-in code (HTTP ${String(response.status)}): ${describeError(errorBody)}`, { extra: { oauthError: errorBody } });
236
+ }
237
+ return parseTokenResponse(await response.json());
238
+ }
239
+ function listenForCallback(expectedState) {
240
+ return new Promise((resolve, reject) => {
241
+ let settle;
242
+ let fail;
243
+ let settled = false;
244
+ const done = new Promise((resolveDone, rejectDone) => {
245
+ settle = resolveDone;
246
+ fail = rejectDone;
247
+ });
248
+ // The wait can reject while Auth0 is still being checked, before the
249
+ // caller awaits `done`. A handler here keeps that from surfacing as an
250
+ // unhandled rejection; awaiting `done` still throws.
251
+ void done.catch(() => undefined);
252
+ const finish = (search) => {
253
+ if (settled === true || settle === undefined) {
254
+ return;
255
+ }
256
+ settled = true;
257
+ clearTimeout(timeout);
258
+ settle(search);
259
+ };
260
+ const giveUp = (error) => {
261
+ if (settled === true || fail === undefined) {
262
+ return;
263
+ }
264
+ settled = true;
265
+ clearTimeout(timeout);
266
+ fail(error);
267
+ };
268
+ const server = createServer((req, res) => {
269
+ handleCallbackRequest(req, res, expectedState, finish);
270
+ });
271
+ const timeout = setTimeout(() => {
272
+ giveUp(new Error("AgentID sign-in timed out before the browser returned. The AgentID page expires after 5 minutes."));
273
+ server.close();
274
+ }, SIGN_IN_TIMEOUT_MS);
275
+ server.on("error", (error) => {
276
+ clearTimeout(timeout);
277
+ if (error.code === "EADDRINUSE") {
278
+ reject(new Error(`${AGENTID_CALLBACK_URL} is already in use. Free that port and run the command again.`));
279
+ return;
280
+ }
281
+ reject(error);
282
+ });
283
+ server.listen(AGENTID_CALLBACK_PORT, "127.0.0.1", () => {
284
+ const address = server.address();
285
+ if (address === null || typeof address === "string") {
286
+ reject(new Error("Could not bind a localhost port for AgentID sign-in."));
287
+ return;
288
+ }
289
+ resolve({
290
+ port: address.port,
291
+ done,
292
+ close: () => {
293
+ clearTimeout(timeout);
294
+ server.close();
295
+ },
296
+ });
297
+ });
298
+ });
299
+ }
300
+ function handleCallbackRequest(req, res, expectedState, finish) {
301
+ const requestUrl = req.url;
302
+ if (requestUrl === undefined) {
303
+ res.writeHead(400);
304
+ res.end();
305
+ return;
306
+ }
307
+ const url = new URL(requestUrl, "http://127.0.0.1");
308
+ if (url.pathname !== CALLBACK_PATH) {
309
+ res.writeHead(404);
310
+ res.end();
311
+ return;
312
+ }
313
+ const method = req.method === undefined ? "" : req.method;
314
+ if (isSignInCallback(method, url.search, expectedState) === false) {
315
+ res.writeHead(method === "GET" ? 404 : 405);
316
+ res.end();
317
+ return;
318
+ }
319
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
320
+ res.end("<!doctype html><title>Cargo</title><p>Sign-in finished. You can close this window and return to the terminal.</p>");
321
+ finish(url.search);
322
+ }
323
+ function htmlError(body) {
324
+ const match = body.match(/class="error-message">([\s\S]*?)<\/p>/);
325
+ if (match === null || match[1] === undefined) {
326
+ return undefined;
327
+ }
328
+ return match[1]
329
+ .replace(/<[^>]+>/g, " ")
330
+ .replace(/\s+/g, " ")
331
+ .trim();
332
+ }
@@ -1 +1 @@
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;AAGjD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AA2BxD,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,CAsIN;AA2GD,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
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,WAAW,CAAC;AAGjD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AA6BxD,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,CAwJN;AAqHD,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"}
@@ -6,6 +6,7 @@ import { clearCredentials, CredentialsNotClearedError, getCredentialsPath, loadC
6
6
  import { parseBooleanOption } from "../../utils/booleanOption.js";
7
7
  import { ExitCodes, failWith, handleApiCall, info, outputJson, success, } from "../runHandler.js";
8
8
  import { clearStartState } from "../start/state.js";
9
+ import { runAgentIdLogin } from "./agentId.js";
9
10
  import { runEmailLogin } from "./email.js";
10
11
  import { selectLoginChannel } from "./loginChannel.js";
11
12
  import { runOAuthDeviceFlow } from "./oauth.js";
@@ -13,10 +14,11 @@ import { DEFAULT_BASE_URL, establishSession, resolveBaseUrl, } from "./session.j
13
14
  export function registerAuthCommands(program, getApi) {
14
15
  const login = program
15
16
  .command("login")
16
- .description("Sign in to Cargo with an emailed code (--email), via browser (--oauth), or with an existing API token (--token)")
17
+ .description("Sign in to Cargo with an emailed code (--email), via browser (--oauth), with AgentID (--agentid), or with an existing API token (--token)")
17
18
  .option("--email <email>", "Sign in with a code emailed to this address; creates the account on first use")
18
19
  .option("--code <code>", 'Verification code from the email, or "-" to read it from stdin; omit to have one sent (--email only)')
19
20
  .option("--oauth [true|false]", "Sign in via browser using the OAuth 2.0 Device Authorization Flow", parseBooleanOption)
21
+ .option("--agentid [true|false]", "Sign in as an agent through the AgentID connection; prints an Auth0 URL and waits for it to redirect back to this machine", parseBooleanOption)
20
22
  .option("--token <token>", "Existing Cargo API token")
21
23
  .option("--workspace-name <name>", "Workspace to use, created if no workspace of that name exists (skips the workspace prompt)")
22
24
  .option("--workspace-uuid <uuid>", "Workspace UUID to sign in for (skips the workspace prompt)")
@@ -27,6 +29,7 @@ Examples:
27
29
  $ cargo-ai login --email you@company.com --code 123456 --workspace-name "Acme GTM"
28
30
  $ cargo-ai login --oauth
29
31
  $ cargo-ai login --oauth --workspace-uuid 550e8400-e29b-41d4-a716-446655440000
32
+ $ cargo-ai login --agentid
30
33
  $ cargo-ai login --token sk_live_abc123 --base-url https://api.custom.io
31
34
 
32
35
  --email and --oauth both create the account if it does not exist yet, so there
@@ -40,7 +43,14 @@ Signing in with an address that already has an account resolves to its existing
40
43
  workspace rather than creating one, and --workspace-name reuses a workspace of
41
44
  that name when one exists, so this is safe to re-run.
42
45
 
43
- --email and --oauth save the signed-in session to
46
+ --agentid signs an agent in as itself. The command prints an Auth0 URL with
47
+ the AgentID connection and waits on http://127.0.0.1 for the redirect. Open
48
+ that URL once; a second visit starts a different sign-in. It does not open the Cargo app. When
49
+ AgentID shows an auth token, approve it with "agentmail inboxes authorize".
50
+ The cargo-ai Auth0 application must allow http://127.0.0.1:52701/callback and have
51
+ the AgentID connection enabled.
52
+
53
+ --email, --oauth and --agentid save the signed-in session to
44
54
  ~/.config/cargo-ai/credentials.json and renew it automatically, so you stay
45
55
  signed in without a token being created for each machine. CARGO_CONFIG_DIR
46
56
  (or XDG_CONFIG_HOME) moves that file; where the home directory is not writable
@@ -66,6 +76,11 @@ Environment variables (CARGO_API_TOKEN, CARGO_WORKSPACE_UUID, CARGO_BASE_URL) ta
66
76
  hint: "opens a browser; no code to copy",
67
77
  flag: "--oauth",
68
78
  },
79
+ {
80
+ label: "AgentID",
81
+ hint: "an agent signs in as itself; approve the token AgentID shows",
82
+ flag: "--agentid",
83
+ },
69
84
  {
70
85
  label: "Emailed code",
71
86
  hint: "no browser needed; signs you up on first use",
@@ -88,7 +103,7 @@ Environment variables (CARGO_API_TOKEN, CARGO_WORKSPACE_UUID, CARGO_BASE_URL) ta
88
103
  // for; --keep-token stays the documented spelling for existing scripts.
89
104
  .addOption(new Option("--keep-session").hideHelp())
90
105
  .addHelpText("after", `
91
- A session from "cargo-ai login --email" or "--oauth" is revoked at the identity
106
+ A session from "cargo-ai login --email", "--oauth" or "--agentid" is revoked at the identity
92
107
  provider, so signing out is final. Tokens you supplied yourself with --token are
93
108
  only forgotten locally, since they may be in use elsewhere.
94
109
 
@@ -136,7 +151,7 @@ async function runLogin(opts) {
136
151
  });
137
152
  return;
138
153
  }
139
- const tokens = await runOAuthDeviceFlow();
154
+ const tokens = await tokensForChannel(selection.channel);
140
155
  const workspace = await establishSession({
141
156
  tokens,
142
157
  baseUrlOverride: opts.baseUrl,
@@ -154,6 +169,12 @@ async function runLogin(opts) {
154
169
  tokenType: "session",
155
170
  });
156
171
  }
172
+ async function tokensForChannel(channel) {
173
+ if (channel === "agentid") {
174
+ return await runAgentIdLogin();
175
+ }
176
+ return await runOAuthDeviceFlow();
177
+ }
157
178
  async function runLogout(opts) {
158
179
  const credentials = loadCredentials();
159
180
  const plan = planRevocation(credentials, opts);
@@ -2,11 +2,14 @@ import type { EmailLoginOptions } from "./email.js";
2
2
  export type LoginOptions = EmailLoginOptions & {
3
3
  token?: string;
4
4
  oauth?: boolean;
5
+ agentid?: boolean;
5
6
  };
6
7
  export type LoginChannelSelection = {
7
8
  channel: "email";
8
9
  } | {
9
10
  channel: "oauth";
11
+ } | {
12
+ channel: "agentid";
10
13
  } | {
11
14
  channel: "token";
12
15
  token: string;
@@ -1 +1 @@
1
- {"version":3,"file":"loginChannel.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/loginChannel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACpB;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACpB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtB;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,qBAAqB,CAmD5E"}
1
+ {"version":3,"file":"loginChannel.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/loginChannel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACpB;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACpB;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,GACtB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtB;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,qBAAqB,CA6D5E"}
@@ -5,16 +5,24 @@
5
5
  export function selectLoginChannel(opts) {
6
6
  const wantsEmail = opts.email !== undefined;
7
7
  const wantsOAuth = opts.oauth === true;
8
+ const wantsAgentId = opts.agentid === true;
8
9
  const wantsToken = opts.token !== undefined;
9
- const selectedCount = [wantsEmail, wantsOAuth, wantsToken].filter((selected) => {
10
+ const selectedCount = [
11
+ wantsEmail,
12
+ wantsOAuth,
13
+ wantsAgentId,
14
+ wantsToken,
15
+ ].filter((selected) => {
10
16
  return selected === true;
11
17
  }).length;
12
18
  if (selectedCount > 1) {
13
- return { error: "Pass only one of --email, --oauth or --token." };
19
+ return {
20
+ error: "Pass only one of --email, --oauth, --agentid or --token.",
21
+ };
14
22
  }
15
23
  if (selectedCount === 0) {
16
24
  return {
17
- error: "Must pass --email <email> (emailed code), --oauth (browser sign-in) or --token <token> (existing API token).",
25
+ error: "Must pass --email <email> (emailed code), --oauth (browser sign-in), --agentid (AgentID) or --token <token> (existing API token).",
18
26
  };
19
27
  }
20
28
  if (opts.code !== undefined && wantsEmail === false) {
@@ -35,6 +43,9 @@ export function selectLoginChannel(opts) {
35
43
  if (opts.email !== undefined) {
36
44
  return { channel: "email" };
37
45
  }
46
+ if (opts.agentid === true) {
47
+ return { channel: "agentid" };
48
+ }
38
49
  if (opts.token !== undefined) {
39
50
  return { channel: "token", token: opts.token };
40
51
  }
@@ -15,7 +15,7 @@
15
15
  // or a docs snippet, and every step leaves a valid machine behind: cancel after
16
16
  // signing in and you are signed in.
17
17
  import { spawnSync } from "node:child_process";
18
- import { askText, availableCodingAgents, cdkProjectRoot, COMPANY_CONTEXT_PATH, hasCompanyContext, installedCookbooks, isInteractive, launchBriefed, requireAnswer, selectOne, skillsInstalledFor, } from "@cargo-ai/cdk/cli";
18
+ import { askText, availableCodingAgents, COMPANY_CONTEXT_PATH, findProjectDir, hasCompanyContext, installedCookbooks, isInteractive, launchBriefed, requireAnswer, selectOne, skillsInstalledFor, } from "@cargo-ai/cdk/cli";
19
19
  import { AGENT_SKILLS_NEXT_STEP } from "../../agentSkills.js";
20
20
  import { isSignedIn } from "../../setupIssues.js";
21
21
  import { parseBooleanOption } from "../../utils/booleanOption.js";
@@ -35,7 +35,7 @@ const EMAIL = "email";
35
35
  * Exported for tests; `go` is the only production caller.
36
36
  */
37
37
  export function resolveProjectContext(cwd) {
38
- const root = cdkProjectRoot(cwd);
38
+ const root = findProjectDir(cwd);
39
39
  if (root === undefined) {
40
40
  return { kind: "none" };
41
41
  }
package/build/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { availableCodingAgents, BRAND, canRunPalette, cdkProjectRoot, creditsLeftOf, declareSources, fetchSubscription, fetchWorkspaceName, formatWorkspaceSuffix, isInteractive, isUnpaidPlan, printSetupIssues, registerCommands as registerCdkCommands, runPalette, SessionExpiredError, } from "@cargo-ai/cdk/cli";
3
+ import { availableCodingAgents, BRAND, canRunPalette, creditsLeftOf, declareSources, fetchSubscription, fetchWorkspaceName, findProjectDir, formatWorkspaceSuffix, isInteractive, isUnpaidPlan, printSetupIssues, registerCommands as registerCdkCommands, runPalette, SessionExpiredError, } from "@cargo-ai/cdk/cli";
4
4
  import { Command } from "commander";
5
5
  import { AGENT_SKILLS_HINT } from "./agentSkills.js";
6
6
  import { createApi } from "./api.js";
@@ -62,6 +62,7 @@ Agent skills:
62
62
  Authentication:
63
63
  Emailed code: cargo-ai login --email you@company.com (no browser; signs you up on first use)
64
64
  Browser sign-in: cargo-ai login --oauth
65
+ AgentID: cargo-ai login --agentid
65
66
  With API token: cargo-ai login --token <your-token>
66
67
  Use env variables: CARGO_API_TOKEN, CARGO_WORKSPACE_UUID, CARGO_BASE_URL
67
68
  Check status: cargo-ai whoami
@@ -289,11 +290,11 @@ async function openPalette(startPath) {
289
290
  // Read once: a layout describing two different directories could offer to
290
291
  // deploy one and name the other.
291
292
  const cwd = process.cwd();
292
- const root = cdkProjectRoot(cwd);
293
+ const root = findProjectDir(cwd);
293
294
  return buildPaletteLayout({
294
295
  signedIn: isSignedIn(),
295
296
  agents,
296
- cdkProjectRoot: root,
297
+ projectDir: root,
297
298
  cwd,
298
299
  unpaidPlan,
299
300
  });
@@ -2,8 +2,8 @@ import { type CodingAgent, type PaletteLayout, type PinnedRow } from "@cargo-ai/
2
2
  export type PaletteLayoutInput = {
3
3
  signedIn: boolean;
4
4
  agents: readonly CodingAgent[];
5
- /** The CDK repo root the palette is standing in, or undefined when none. */
6
- cdkProjectRoot: string | undefined;
5
+ /** The project (`cargo.json`) folder the palette is in, or undefined when none. */
6
+ projectDir: string | undefined;
7
7
  /**
8
8
  * Where the palette is standing. The project rows are resolved by walking
9
9
  * *up* from here, so this is what says whether they point at the directory
@@ -1 +1 @@
1
- {"version":3,"file":"paletteLayout.d.ts","sourceRoot":"","sources":["../src/paletteLayout.ts"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,mBAAmB,CAAC;AA+E3B,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IAC/B,4EAA4E;IAC5E,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAS3E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,kBAAkB,GAAG,SAAS,EAAE,CA+DjE"}
1
+ {"version":3,"file":"paletteLayout.d.ts","sourceRoot":"","sources":["../src/paletteLayout.ts"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,mBAAmB,CAAC;AA+E3B,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IAC/B,mFAAmF;IACnF,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAS3E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,kBAAkB,GAAG,SAAS,EAAE,CA+DjE"}
@@ -113,8 +113,8 @@ export function pinnedRows(input) {
113
113
  // for a workspace you have not built yet, and this one is open on screen.
114
114
  // Outside, there is no project to add to or deploy, so offer the tour and the
115
115
  // scaffold instead.
116
- const projectRows = input.cdkProjectRoot !== undefined
117
- ? cdkRows(input.cdkProjectRoot, input.cwd)
116
+ const projectRows = input.projectDir !== undefined
117
+ ? cdkRows(input.projectDir, input.cwd)
118
118
  : [
119
119
  {
120
120
  value: DEMO,
@@ -163,9 +163,8 @@ function cdkRows(root, cwd) {
163
163
  value: ADD,
164
164
  label: "Add",
165
165
  hint: "a connector or a cookbook into this project",
166
- // `--dir` because `project add` refuses a directory with no package.json,
167
- // and the palette is as often open in `infra/` — where the scaffold's
168
- // README sends people — as at the root.
166
+ // `--dir` pins the project the palette found, so the command adds to
167
+ // the one on screen whichever folder of it the palette is open in.
169
168
  argv: ["project", "add", "--dir", root],
170
169
  },
171
170
  {
@@ -17,21 +17,18 @@ export declare function importModuleDefault(file: string, subject: string): Prom
17
17
  /**
18
18
  * The CDK project directory a `--file` module belongs to — the directory `cdk
19
19
  * plan` and `cdk deploy` run in, which is what a harness agent's inferred
20
- * `rootDirectory` is measured from. Walk up to the repo, then back down to the
21
- * project (`infra/` in a scaffolded repo); a file in no recognisable project
22
- * answers its own directory.
23
- *
24
- * Deliberately NOT the nearest cargo.state.json. That file's position is
25
- * incidental — a repo deployed before `cdk deploy` ran from the project has one
26
- * at the repo root — so measuring from it would report no `rootDirectory` and
27
- * clone the repo root, while the deployed agent clones `infra/`.
20
+ * `rootDirectory` is measured from: the resources of the nearest `cargo.json`
21
+ * above the file, else above the cwd (a scratch file run from inside a
22
+ * project). With neither, the command stops, as `cdk plan` does: building from
23
+ * the file's own folder instead would load part of the project and bind the
24
+ * agent to a folder the deployed one does not clone, with nothing said.
28
25
  */
29
26
  export declare function cdkProjectDirFor(file: string): string;
30
27
  /**
31
28
  * Deep-resolve every `@cargo-ai/cdk` handle token in `value` to its real uuid
32
- * using the nearest cargo.state.json (searched from `file`'s directory upward,
33
- * then the cwd). `subject` names the thing under test in error messages (e.g. a
34
- * workflow or agent slug). A value with no tokens (literal-uuid refs, or a pure
29
+ * using the deploy state of the project `file` belongs to (`cdkProjectDirFor`).
30
+ * `subject` names the thing under test in error messages (e.g. a workflow or
31
+ * agent slug). A value with no tokens (literal-uuid refs, or a pure
35
32
  * body) is returned unchanged — no state needed. Fails with an actionable error
36
33
  * when state is missing or a referenced resource isn't deployed yet.
37
34
  */
@@ -1 +1 @@
1
- {"version":3,"file":"cdkState.d.ts","sourceRoot":"","sources":["../../src/utils/cdkState.ts"],"names":[],"mappings":"AAoBA,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CASxE;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,CAAC,CAyBtD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOrD;AAaD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CA2CT"}
1
+ {"version":3,"file":"cdkState.d.ts","sourceRoot":"","sources":["../../src/utils/cdkState.ts"],"names":[],"mappings":"AAoBA,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CASxE;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,CAAC,CAyBtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBrD;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAkCT"}
@@ -5,10 +5,10 @@
5
5
  // `@cargo-ai/cdk` handle tokens the module references to their real uuids from
6
6
  // this project's deploy state — without deploying the module itself.
7
7
  import { existsSync } from "node:fs";
8
- import { dirname, isAbsolute, join, resolve } from "node:path";
8
+ import { dirname, isAbsolute, resolve } from "node:path";
9
9
  import { pathToFileURL } from "node:url";
10
10
  import { dependenciesOf, readStateOffline, resolveTokens } from "@cargo-ai/cdk";
11
- import { cdkProjectDir, cdkProjectRoot } from "@cargo-ai/cdk/cli";
11
+ import { cdkResourceDir, missingProjectFile } from "@cargo-ai/cdk/cli";
12
12
  import { ExitCodes, failWith } from "../commands/runHandler.js";
13
13
  // Peel the extra interop layer tsx adds when a TS source is transpiled to CJS:
14
14
  // the ESM namespace's `default` is then the CJS `module.exports`
@@ -53,41 +53,30 @@ export async function importModuleDefault(file, subject) {
53
53
  /**
54
54
  * The CDK project directory a `--file` module belongs to — the directory `cdk
55
55
  * plan` and `cdk deploy` run in, which is what a harness agent's inferred
56
- * `rootDirectory` is measured from. Walk up to the repo, then back down to the
57
- * project (`infra/` in a scaffolded repo); a file in no recognisable project
58
- * answers its own directory.
59
- *
60
- * Deliberately NOT the nearest cargo.state.json. That file's position is
61
- * incidental — a repo deployed before `cdk deploy` ran from the project has one
62
- * at the repo root — so measuring from it would report no `rootDirectory` and
63
- * clone the repo root, while the deployed agent clones `infra/`.
56
+ * `rootDirectory` is measured from: the resources of the nearest `cargo.json`
57
+ * above the file, else above the cwd (a scratch file run from inside a
58
+ * project). With neither, the command stops, as `cdk plan` does: building from
59
+ * the file's own folder instead would load part of the project and bind the
60
+ * agent to a folder the deployed one does not clone, with nothing said.
64
61
  */
65
62
  export function cdkProjectDirFor(file) {
66
63
  const absPath = isAbsolute(file) ? file : resolve(process.cwd(), file);
67
- const fileDir = dirname(absPath);
68
- const repoRoot = cdkProjectRoot(fileDir);
69
- if (repoRoot === undefined)
70
- return fileDir;
71
- const project = cdkProjectDir(repoRoot);
72
- return project === undefined ? repoRoot : project;
73
- }
74
- // Walk up from `startDir` looking for a directory that holds cargo.state.json.
75
- function findStateRoot(startDir) {
76
- let dir = startDir;
77
- for (;;) {
78
- if (existsSync(join(dir, "cargo.state.json")))
79
- return dir;
80
- const parent = dirname(dir);
81
- if (parent === dir)
82
- return undefined;
83
- dir = parent;
64
+ const fileProject = cdkResourceDir(dirname(absPath));
65
+ const project = fileProject !== undefined ? fileProject : cdkResourceDir(process.cwd());
66
+ if (project === undefined) {
67
+ const missing = missingProjectFile(dirname(absPath));
68
+ failWith(missing.message, {
69
+ code: ExitCodes.InvalidUsage,
70
+ extra: { run: missing.run },
71
+ });
84
72
  }
73
+ return project;
85
74
  }
86
75
  /**
87
76
  * Deep-resolve every `@cargo-ai/cdk` handle token in `value` to its real uuid
88
- * using the nearest cargo.state.json (searched from `file`'s directory upward,
89
- * then the cwd). `subject` names the thing under test in error messages (e.g. a
90
- * workflow or agent slug). A value with no tokens (literal-uuid refs, or a pure
77
+ * using the deploy state of the project `file` belongs to (`cdkProjectDirFor`).
78
+ * `subject` names the thing under test in error messages (e.g. a workflow or
79
+ * agent slug). A value with no tokens (literal-uuid refs, or a pure
91
80
  * body) is returned unchanged — no state needed. Fails with an actionable error
92
81
  * when state is missing or a referenced resource isn't deployed yet.
93
82
  */
@@ -95,20 +84,15 @@ export function resolveTokensFromState(value, file, subject) {
95
84
  const deps = dependenciesOf(value);
96
85
  if (deps.size === 0)
97
86
  return value;
98
- const absPath = isAbsolute(file) ? file : resolve(process.cwd(), file);
99
- const stateRoot = findStateRoot(dirname(absPath)) ?? findStateRoot(process.cwd());
100
- if (stateRoot === undefined) {
101
- failWith(`${subject} references ${String(deps.size)} deployed resource(s) ` +
102
- `(${[...deps].join(", ")}), but no cargo.state.json was found near ` +
103
- `${absPath} or the current directory. Deploy the resources first with ` +
104
- "`cargo-ai project deploy`, or reference them by literal uuid " +
105
- "(toolRef / agentRef / connectorRef).", { code: ExitCodes.NotFound });
106
- }
87
+ // The project's own state file, beside its cargo.json, rather than the
88
+ // nearest cargo.state.json: in a repository of several projects that could
89
+ // be another project's.
90
+ const project = cdkProjectDirFor(file);
107
91
  // A project whose state lives in the workspace has a pointer here, not a
108
92
  // resource map, so the cache the last plan or deploy left behind is what
109
93
  // answers this offline. Resolving a handle is a read of what is already
110
94
  // deployed — worth a stale answer, not worth a round trip on every run.
111
- const state = readStateOffline(stateRoot);
95
+ const state = readStateOffline(project);
112
96
  const outputs = new Map();
113
97
  if (state !== undefined) {
114
98
  for (const [id, entry] of Object.entries(state.resources)) {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@cargo-ai/cli",
3
- "version": "1.0.113",
3
+ "version": "1.0.116",
4
+ "mcpName": "io.github.getcargohq/cargo",
4
5
  "private": false,
5
6
  "license": "UNLICENSED",
6
7
  "homepage": "https://getcargo.ai",
@@ -73,9 +74,9 @@
73
74
  "dependencies": {
74
75
  "@ai-sdk/harness": "^1.0.144",
75
76
  "@ai-sdk/harness-claude-code": "^1.0.148",
76
- "@cargo-ai/api": "^1.0.94",
77
+ "@cargo-ai/api": "^1.0.96",
77
78
  "@cargo-ai/app-sdk": "^1.0.11",
78
- "@cargo-ai/cdk": "^1.0.111",
79
+ "@cargo-ai/cdk": "^1.0.112",
79
80
  "@cargo-ai/types": "^1.0.96",
80
81
  "@cargo-ai/utils": "^1.0.32",
81
82
  "@cargo-ai/worker-sdk": "^1.0.20",