@cargo-ai/cli 1.0.143 → 1.0.146

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 (37) hide show
  1. package/README.md +1 -1
  2. package/build/agentMode.d.ts +30 -0
  3. package/build/agentMode.d.ts.map +1 -0
  4. package/build/agentMode.js +45 -0
  5. package/build/commands/auth/email.js +1 -1
  6. package/build/commands/auth/workspace.d.ts +17 -0
  7. package/build/commands/auth/workspace.d.ts.map +1 -1
  8. package/build/commands/auth/workspace.js +74 -7
  9. package/build/commands/authHints.d.ts +8 -0
  10. package/build/commands/authHints.d.ts.map +1 -0
  11. package/build/commands/authHints.js +9 -0
  12. package/build/commands/connection/connector.d.ts.map +1 -1
  13. package/build/commands/connection/connector.js +7 -3
  14. package/build/commands/doctor.d.ts +13 -0
  15. package/build/commands/doctor.d.ts.map +1 -1
  16. package/build/commands/doctor.js +32 -5
  17. package/build/commands/insufficientCredits.d.ts +31 -0
  18. package/build/commands/insufficientCredits.d.ts.map +1 -0
  19. package/build/commands/insufficientCredits.js +137 -0
  20. package/build/commands/runHandler.d.ts +16 -0
  21. package/build/commands/runHandler.d.ts.map +1 -1
  22. package/build/commands/runHandler.js +69 -13
  23. package/build/commands/signedOutNotice.d.ts.map +1 -1
  24. package/build/commands/signedOutNotice.js +3 -2
  25. package/build/index.js +24 -4
  26. package/build/updateNotifier.d.ts +23 -3
  27. package/build/updateNotifier.d.ts.map +1 -1
  28. package/build/updateNotifier.js +57 -5
  29. package/build/utils/openBrowser.d.ts +14 -0
  30. package/build/utils/openBrowser.d.ts.map +1 -1
  31. package/build/utils/openBrowser.js +20 -8
  32. package/build/utils/prompt.d.ts.map +1 -1
  33. package/build/utils/prompt.js +9 -0
  34. package/build/version.d.ts +5 -0
  35. package/build/version.d.ts.map +1 -1
  36. package/build/version.js +3 -0
  37. package/package.json +4 -4
package/README.md CHANGED
@@ -246,7 +246,7 @@ Other release verbs (`list`, `get <uuid>`, `get-deployed`, `get-draft`, `update-
246
246
 
247
247
  ## Agent Skills
248
248
 
249
- [Cargo Skills](https://github.com/getcargohq/cargo-skills) teaches AI coding agents (Claude Code, Codex, Cursor, Windsurf, GitHub Copilot, Hermes Agent, etc.) how to use this CLI — seventeen skills covering lead sourcing, enrichment, email verification, lead scoring, CRM sync, buying-signal monitoring, orchestration, storage, and projects (GTM as Code).
249
+ [Cargo Skills](https://github.com/getcargohq/cargo-skills) teaches AI coding agents (Claude Code, Codex, Cursor, Windsurf, GitHub Copilot, Hermes Agent, etc.) how to use this CLI: the Cargo skills bundle covers lead sourcing, enrichment, email verification, lead scoring, CRM sync, buying-signal monitoring, orchestration, storage, and projects (GTM as Code).
250
250
 
251
251
  **Let your agent install everything.** Paste this into Claude Code, Codex, or Cursor and it handles the CLI, sign-in, and skills for its own platform:
252
252
 
@@ -0,0 +1,30 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ export type AgentContext = {
3
+ /** The harness that launched this process, e.g. `claude-code`. */
4
+ codingAgent: string | undefined;
5
+ ci: boolean;
6
+ stdoutIsTty: boolean;
7
+ stderrIsTty: boolean;
8
+ /**
9
+ * True whenever nobody can be expected to answer or watch: a coding agent
10
+ * (whose PTY can make every stream look like a terminal), CI, or a stdout
11
+ * that is not a terminal.
12
+ */
13
+ nonInteractive: boolean;
14
+ };
15
+ export declare function readAgentContext(env?: NodeJS.ProcessEnv, streams?: {
16
+ stdoutIsTty: boolean;
17
+ stderrIsTty: boolean;
18
+ }): AgentContext;
19
+ /**
20
+ * Whether stderr may carry terminal UI (colour, spinners, multi-line human
21
+ * errors). A coding agent's PTY reports a TTY, so the TTY alone cannot decide.
22
+ */
23
+ export declare function canDrawOnStderr(context?: AgentContext): boolean;
24
+ /**
25
+ * Whether this run may block on a person typing into the terminal. Unlike the
26
+ * pipe rule above, a piped stdout alone does not refuse: `cargo-ai login | jq`
27
+ * by hand still has someone at /dev/tty.
28
+ */
29
+ export declare function mayWaitForPerson(context?: AgentContext): boolean;
30
+ //# sourceMappingURL=agentMode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentMode.d.ts","sourceRoot":"","sources":["../src/agentMode.ts"],"names":[],"mappings":";AAgBA,MAAM,MAAM,YAAY,GAAG;IACzB,kEAAkE;IAClE,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,wBAAgB,gBAAgB,CAC9B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE;IAAE,WAAW,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAGpD,GACA,YAAY,CAYd;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,YAAiC,GACzC,OAAO,CAMT;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,YAAiC,GACzC,OAAO,CAET"}
@@ -0,0 +1,45 @@
1
+ // Who is on the other end of this process: a person at a terminal, or a coding
2
+ // agent, CI job or pipe that can read output but never answer a question.
3
+ //
4
+ // Agent detection itself lives in `@cargo-ai/cdk/cli` (`detectCodingAgent`),
5
+ // which already gates every clack prompt and names the harness to the backend;
6
+ // this module only adds what the `cargo-ai` bin needs on top: the TTY facts,
7
+ // and one answer to "may this run behave like a terminal UI" for the places
8
+ // that do their own drawing (spinners, colour, the error format, /dev/tty
9
+ // prompts, opening a browser).
10
+ //
11
+ // The rule, after trigger.dev's CLI: when an agent is detected, never prompt
12
+ // (fail with the flag that answers the question), never animate, never open a
13
+ // browser nobody asked for, and keep stderr to lines a model can parse.
14
+ import { detectCodingAgent, isCiEnvironment } from "@cargo-ai/cdk/cli";
15
+ export function readAgentContext(env = process.env, streams = {
16
+ stdoutIsTty: process.stdout.isTTY === true,
17
+ stderrIsTty: process.stderr.isTTY === true,
18
+ }) {
19
+ const codingAgent = detectCodingAgent(env);
20
+ const ci = isCiEnvironment(env);
21
+ return {
22
+ codingAgent,
23
+ ci,
24
+ stdoutIsTty: streams.stdoutIsTty,
25
+ stderrIsTty: streams.stderrIsTty,
26
+ nonInteractive: codingAgent !== undefined || ci === true || streams.stdoutIsTty === false,
27
+ };
28
+ }
29
+ /**
30
+ * Whether stderr may carry terminal UI (colour, spinners, multi-line human
31
+ * errors). A coding agent's PTY reports a TTY, so the TTY alone cannot decide.
32
+ */
33
+ export function canDrawOnStderr(context = readAgentContext()) {
34
+ return (context.stderrIsTty === true &&
35
+ context.codingAgent === undefined &&
36
+ context.ci === false);
37
+ }
38
+ /**
39
+ * Whether this run may block on a person typing into the terminal. Unlike the
40
+ * pipe rule above, a piped stdout alone does not refuse: `cargo-ai login | jq`
41
+ * by hand still has someone at /dev/tty.
42
+ */
43
+ export function mayWaitForPerson(context = readAgentContext()) {
44
+ return context.codingAgent === undefined && context.ci === false;
45
+ }
@@ -81,7 +81,7 @@ async function resolveCode(opts) {
81
81
  success(`Verification code sent to ${opts.email}.`);
82
82
  const promptInput = openPromptInput();
83
83
  if (promptInput === undefined) {
84
- info(`No interactive terminal available. Re-run with the code: ${opts.resumeCommand}`);
84
+ info(`No interactive terminal to type the code into. Get the code from ${opts.email}'s inbox (ask the user if you are an agent), then run: ${opts.resumeCommand}`);
85
85
  return undefined;
86
86
  }
87
87
  try {
@@ -15,6 +15,23 @@ export declare function resolveWorkspace(opts: {
15
15
  explicitUuid: string | undefined;
16
16
  explicitName: string | undefined;
17
17
  }): Promise<WorkspaceResolution>;
18
+ /**
19
+ * Without a terminal nothing can be picked, so the error itself has to carry
20
+ * the choice: every workspace with its uuid, and the flag that selects one.
21
+ * The sign-in is not saved, and an emailed code works once, so the command is
22
+ * re-run from the start.
23
+ *
24
+ * Exported for tests; `resolveWorkspace` is the only production caller.
25
+ */
26
+ export declare function describeWorkspaceChoice(workspaces: readonly Pick<WorkspaceManagement.Workspace, "uuid" | "name">[]): string;
27
+ /**
28
+ * The free plan caps how many workspaces one person owns, so naming a new one
29
+ * can be refused. The fix is to use a workspace that already exists, which the
30
+ * message lists with the flag that picks it.
31
+ *
32
+ * Exported for tests; `createWorkspace` is the only production caller.
33
+ */
34
+ export declare function describeFreeWorkspaceLimit(name: string, workspaces: readonly Pick<WorkspaceManagement.Workspace, "uuid" | "name">[]): string;
18
35
  export declare function findWorkspaceByName(workspaces: readonly WorkspaceManagement.Workspace[], name: string): ResolvedWorkspace | undefined;
19
36
  /**
20
37
  * The workspace the sign-in Action just created, when it is the account's only
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/workspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGzD,OAAO,EAAE,KAAK,GAAG,EAAa,MAAM,cAAc,CAAC;AAUnD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,iBAAiB,CAAC;IAC7B,mFAAmF;IACnF,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAuBF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAwH/B;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,SAAS,mBAAmB,CAAC,SAAS,EAAE,EACpD,IAAI,EAAE,MAAM,GACX,iBAAiB,GAAG,SAAS,CAY/B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,SAAS,IAAI,CACvB,mBAAmB,CAAC,SAAS,EAC7B,MAAM,GAAG,MAAM,GAAG,WAAW,CAC9B,EAAE,EACH,GAAG,EAAE,IAAI,GACR,iBAAiB,GAAG,SAAS,CAkB/B;AAsHD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ7D"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/workspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGzD,OAAO,EAAE,KAAK,GAAG,EAAa,MAAM,cAAc,CAAC;AAUnD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,iBAAiB,CAAC;IAC7B,mFAAmF;IACnF,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAuBF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAwH/B;AAUD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,SAAS,IAAI,CAAC,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,GAC1E,MAAM,CASR;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,SAAS,IAAI,CAAC,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,GAC1E,MAAM,CAsBR;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,SAAS,mBAAmB,CAAC,SAAS,EAAE,EACpD,IAAI,EAAE,MAAM,GACX,iBAAiB,GAAG,SAAS,CAY/B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,SAAS,IAAI,CACvB,mBAAmB,CAAC,SAAS,EAC7B,MAAM,GAAG,MAAM,GAAG,WAAW,CAC9B,EAAE,EACH,GAAG,EAAE,IAAI,GACR,iBAAiB,GAAG,SAAS,CAkB/B;AAqJD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ7D"}
@@ -55,7 +55,7 @@ export async function resolveWorkspace(opts) {
55
55
  }
56
56
  }
57
57
  return {
58
- workspace: await createWorkspace(api, opts.explicitName),
58
+ workspace: await createWorkspace(api, opts.explicitName, workspaces),
59
59
  wasCreated: true,
60
60
  };
61
61
  }
@@ -92,7 +92,10 @@ export async function resolveWorkspace(opts) {
92
92
  }
93
93
  const promptInput = openPromptInput();
94
94
  if (promptInput === undefined) {
95
- failWith("Multiple workspaces available and no interactive terminal; pass --workspace-uuid <uuid> (or set CARGO_WORKSPACE_UUID) to select one.", { code: ExitCodes.InvalidUsage });
95
+ failWith(describeWorkspaceChoice(workspaces), {
96
+ code: ExitCodes.InvalidUsage,
97
+ extra: { workspaces: listWorkspaceChoices(workspaces) },
98
+ });
96
99
  }
97
100
  let selectedWorkspace;
98
101
  try {
@@ -106,6 +109,49 @@ export async function resolveWorkspace(opts) {
106
109
  }
107
110
  return { workspace: selectedWorkspace, wasCreated: false };
108
111
  }
112
+ function listWorkspaceChoices(workspaces) {
113
+ return workspaces.map((workspace) => {
114
+ return { uuid: workspace.uuid, name: workspace.name };
115
+ });
116
+ }
117
+ /**
118
+ * Without a terminal nothing can be picked, so the error itself has to carry
119
+ * the choice: every workspace with its uuid, and the flag that selects one.
120
+ * The sign-in is not saved, and an emailed code works once, so the command is
121
+ * re-run from the start.
122
+ *
123
+ * Exported for tests; `resolveWorkspace` is the only production caller.
124
+ */
125
+ export function describeWorkspaceChoice(workspaces) {
126
+ return [
127
+ `You have ${String(workspaces.length)} workspaces and there is no interactive terminal to pick one, so this sign-in was not saved.`,
128
+ "Run the same login command again with --workspace-uuid <uuid> (or set CARGO_WORKSPACE_UUID). With --email, request a new code first: a code works only once.",
129
+ "Your workspaces:",
130
+ ...workspaces.map((workspace) => {
131
+ return ` --workspace-uuid ${workspace.uuid} (${workspace.name})`;
132
+ }),
133
+ ].join("\n");
134
+ }
135
+ /**
136
+ * The free plan caps how many workspaces one person owns, so naming a new one
137
+ * can be refused. The fix is to use a workspace that already exists, which the
138
+ * message lists with the flag that picks it.
139
+ *
140
+ * Exported for tests; `createWorkspace` is the only production caller.
141
+ */
142
+ export function describeFreeWorkspaceLimit(name, workspaces) {
143
+ const lines = [
144
+ `Cannot create workspace "${name}": you have reached the free plan's workspace limit.`,
145
+ ];
146
+ if (workspaces.length === 0) {
147
+ lines.push("Upgrade a workspace, or ask a teammate to invite you to theirs.");
148
+ return lines.join("\n");
149
+ }
150
+ lines.push("Use an existing workspace instead: re-run the login command with --workspace-uuid <uuid> in place of --workspace-name. Or upgrade one of them to create more.", "Your workspaces:", ...workspaces.map((workspace) => {
151
+ return ` --workspace-uuid ${workspace.uuid} (${workspace.name})`;
152
+ }));
153
+ return lines.join("\n");
154
+ }
109
155
  export function findWorkspaceByName(workspaces, name) {
110
156
  const wanted = name.trim().toLowerCase();
111
157
  const matched = workspaces.find((workspace) => {
@@ -142,7 +188,7 @@ async function chooseWorkspace(api, workspaces) {
142
188
  if (workspaces.length === 0) {
143
189
  info("You don't have any workspaces yet. Let's create one.");
144
190
  return {
145
- workspace: await createWorkspace(api, await askWorkspaceName()),
191
+ workspace: await createWorkspace(api, await askWorkspaceName(), workspaces),
146
192
  wasCreated: true,
147
193
  };
148
194
  }
@@ -169,7 +215,7 @@ async function chooseWorkspace(api, workspaces) {
169
215
  });
170
216
  if (chosen === CREATE) {
171
217
  return {
172
- workspace: await createWorkspace(api, await askWorkspaceName()),
218
+ workspace: await createWorkspace(api, await askWorkspaceName(), workspaces),
173
219
  wasCreated: true,
174
220
  };
175
221
  }
@@ -212,12 +258,33 @@ async function createWorkspaceByAsking(api) {
212
258
  code: ExitCodes.InvalidUsage,
213
259
  });
214
260
  }
215
- const workspace = await createWorkspace(api, name);
261
+ // Only reached with no workspaces, so the limit message has none to list.
262
+ const workspace = await createWorkspace(api, name, []);
216
263
  success(`Created workspace: ${workspace.name} (${workspace.uuid})`);
217
264
  return { workspace, wasCreated: true };
218
265
  }
219
- async function createWorkspace(api, name) {
220
- const result = await handleApiCall(() => api.workspaceManagement.workspace.create({ name }), { spinner: "Creating workspace..." });
266
+ async function createWorkspace(api, name,
267
+ // Required, not defaulted: the free-plan-limit message lists these as the
268
+ // way out, and a caller that forgot them printed "you have none" to someone
269
+ // who has several.
270
+ existingWorkspaces) {
271
+ const result = await handleApiCall(async () => {
272
+ try {
273
+ return await api.workspaceManagement.workspace.create({ name });
274
+ }
275
+ catch (error) {
276
+ if (error.reason === "freeWorkspaceLimitReached") {
277
+ failWith(describeFreeWorkspaceLimit(name, existingWorkspaces), {
278
+ code: ExitCodes.InvalidUsage,
279
+ extra: {
280
+ reason: "freeWorkspaceLimitReached",
281
+ workspaces: listWorkspaceChoices(existingWorkspaces),
282
+ },
283
+ });
284
+ }
285
+ throw error;
286
+ }
287
+ }, { spinner: "Creating workspace..." });
221
288
  return { uuid: result.workspace.uuid, name: result.workspace.name };
222
289
  }
223
290
  /**
@@ -0,0 +1,8 @@
1
+ /** Sends a code to the address; creates the account on first use. */
2
+ export declare const LOGIN_EMAIL_COMMAND = "cargo-ai login --email you@company.com";
3
+ /** What a new account starts with (#6543): stated once, here. */
4
+ export declare const SIGNUP_OFFER = "100 free credits for your first month, no card";
5
+ /** What works with no account, so a signed-out agent can keep going. */
6
+ export declare const SIGNED_OUT_WORKS = "Browsing the catalog (`cargo-ai orchestration action list <query>`, `cargo-ai connection integration list`) and `--dry-run` work signed out.";
7
+ export declare const NOT_SIGNED_IN_MESSAGE = "Not signed in. Run `cargo-ai login --email you@company.com`: it emails a code (no browser) and creates the account if it is new, with 100 free credits for your first month, no card. Browsing the catalog (`cargo-ai orchestration action list <query>`, `cargo-ai connection integration list`) and `--dry-run` work signed out.";
8
+ //# sourceMappingURL=authHints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authHints.d.ts","sourceRoot":"","sources":["../../src/commands/authHints.ts"],"names":[],"mappings":"AAGA,qEAAqE;AACrE,eAAO,MAAM,mBAAmB,2CAA2C,CAAC;AAE5E,iEAAiE;AACjE,eAAO,MAAM,YAAY,mDAAmD,CAAC;AAE7E,wEAAwE;AACxE,eAAO,MAAM,gBAAgB,iJACmH,CAAC;AAEjJ,eAAO,MAAM,qBAAqB,uUAAiK,CAAC"}
@@ -0,0 +1,9 @@
1
+ // The commands a signed-out caller is pointed at. Kept free of imports so the
2
+ // error path (`runHandler.ts`) and the notices can share them without a cycle.
3
+ /** Sends a code to the address; creates the account on first use. */
4
+ export const LOGIN_EMAIL_COMMAND = "cargo-ai login --email you@company.com";
5
+ /** What a new account starts with (#6543): stated once, here. */
6
+ export const SIGNUP_OFFER = "100 free credits for your first month, no card";
7
+ /** What works with no account, so a signed-out agent can keep going. */
8
+ export const SIGNED_OUT_WORKS = "Browsing the catalog (`cargo-ai orchestration action list <query>`, `cargo-ai connection integration list`) and `--dry-run` work signed out.";
9
+ export const NOT_SIGNED_IN_MESSAGE = `Not signed in. Run \`${LOGIN_EMAIL_COMMAND}\`: it emails a code (no browser) and creates the account if it is new, with ${SIGNUP_OFFER}. ${SIGNED_OUT_WORKS}`;
@@ -1 +1 @@
1
- {"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/commands/connection/connector.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAcxC,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAgUN"}
1
+ {"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/commands/connection/connector.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAcxC,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAgUN"}
@@ -1,4 +1,4 @@
1
- import { awaitBrowserStep, buildCliConnectorCreateUrl, buildCliConnectorEditUrl, isCodingAgent, openBrowser, resolveUserUuid, shouldWaitForPerson, } from "@cargo-ai/cdk/cli";
1
+ import { awaitBrowserStep, buildCliConnectorCreateUrl, buildCliConnectorEditUrl, isCiEnvironment, isCodingAgent, openBrowser, resolveUserUuid, shouldWaitForPerson, } from "@cargo-ai/cdk/cli";
2
2
  import { getConfig } from "../../config.js";
3
3
  import { parseBooleanOption } from "../../utils/booleanOption.js";
4
4
  import { openPromptInput } from "../../utils/prompt.js";
@@ -212,7 +212,7 @@ async function runCreateHandoff(input) {
212
212
  // refuses coding agents, whose stdin often looks like a TTY.
213
213
  if (shouldWaitForPerson({
214
214
  isCodingAgent: isCodingAgent(),
215
- isCi: process.env["CI"] === "true",
215
+ isCi: isCiEnvironment(),
216
216
  hasPromptInput: openPromptInput() !== undefined,
217
217
  }) === false) {
218
218
  outputJson({
@@ -249,7 +249,11 @@ function openConnectorEditForm(connectorUuid) {
249
249
  connectorUuid,
250
250
  });
251
251
  info(["", "Re-authorize the connector here:", ` ${colors.cyan(url)}`, ""].join("\n"));
252
- openBrowser(url);
252
+ // Nobody asked for a browser here: an agent gets the URL above and in the
253
+ // JSON, and opening a tab on the person's screen mid-session is a surprise.
254
+ if (isCodingAgent() === false) {
255
+ openBrowser(url);
256
+ }
253
257
  outputJson({
254
258
  ok: true,
255
259
  status: "actionRequired",
@@ -1,3 +1,16 @@
1
1
  import type { Command } from "commander";
2
+ import { type ExitCode } from "./runHandler.js";
2
3
  export declare function registerDoctorCommand(program: Command, currentVersion: string): void;
4
+ /**
5
+ * The API result decides first; `--strict` only turns a healthy run with an
6
+ * outdated CLI into exit 7. An unknown latest version (offline registry) is
7
+ * not "outdated": the check could not tell.
8
+ *
9
+ * Exported for tests.
10
+ */
11
+ export declare function resolveDoctorExitCode(input: {
12
+ apiExitCode: ExitCode;
13
+ cliUpToDate: boolean | null;
14
+ strict: boolean;
15
+ }): ExitCode;
3
16
  //# sourceMappingURL=doctor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+KzC,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,MAAM,GACrB,IAAI,CAkFN"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAazC,OAAO,EAAU,KAAK,QAAQ,EAAyB,MAAM,iBAAiB,CAAC;AAsK/E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,MAAM,GACrB,IAAI,CA6FN;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,WAAW,EAAE,QAAQ,CAAC;IACtB,WAAW,EAAE,OAAO,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,QAAQ,CAUX"}
@@ -1,12 +1,15 @@
1
1
  import { determineIfIsFetcherError } from "@cargo-ai/api";
2
- import { formatSetupIssues } from "@cargo-ai/cdk/cli";
2
+ import { formatSetupIssues, isCodingAgent, } from "@cargo-ai/cdk/cli";
3
3
  import { createApi } from "../api.js";
4
4
  import { getConfig } from "../config.js";
5
5
  import { getCredentialsPath } from "../credentials.js";
6
6
  import { collectSetupIssues, readSkillsPin } from "../setupIssues.js";
7
7
  import { fetchLatestSkillsPin, fetchLatestVersion, isOutdated, UPDATE_COMMAND, } from "../version.js";
8
+ import { LOGIN_EMAIL_COMMAND } from "./authHints.js";
8
9
  import { colors, ExitCodes, outputJson } from "./runHandler.js";
9
- const LOGIN_COMMAND = "cargo-ai login --oauth";
10
+ // An agent cannot click through a browser sign-in, but can relay an emailed
11
+ // code: point it at the channel it can finish.
12
+ const LOGIN_COMMAND = isCodingAgent() === true ? LOGIN_EMAIL_COMMAND : "cargo-ai login --oauth";
10
13
  const STATUS_PAGE = "https://status.getcargo.io";
11
14
  function buildCliCheck(currentVersion, latestVersion) {
12
15
  if (latestVersion === undefined) {
@@ -130,13 +133,16 @@ most severe failure:
130
133
  1 API unreachable or returned an unexpected error
131
134
  3 not authenticated (no credentials, or the API rejected them)
132
135
  4 permission denied for the configured workspace
136
+ 7 with --strict only: the CLI is behind the latest npm release (checked
137
+ after the failures above, so a more severe one still wins)
133
138
 
134
139
  Scripts and agents should branch on the exit code and read the JSON for details.
135
140
 
136
141
  Examples:
137
142
  $ cargo-ai doctor
138
- $ cargo-ai doctor || echo "exit: $?"`)
139
- .action(async () => {
143
+ $ cargo-ai doctor --strict || echo "exit: $?"`)
144
+ .option("--strict", "Also fail (exit 7) when the CLI is behind the latest release")
145
+ .action(async (opts) => {
140
146
  let latestVersion;
141
147
  try {
142
148
  latestVersion = await fetchLatestVersion();
@@ -160,7 +166,12 @@ Examples:
160
166
  workspaceUuid: config.workspaceUuid !== undefined ? config.workspaceUuid : null,
161
167
  credentialsFile: getCredentialsPath(),
162
168
  };
163
- const { check: api, exitCode } = await checkApi(config);
169
+ const { check: api, exitCode: apiExitCode } = await checkApi(config);
170
+ const exitCode = resolveDoctorExitCode({
171
+ apiExitCode,
172
+ cliUpToDate: cli.upToDate,
173
+ strict: opts.strict === true,
174
+ });
164
175
  // One list, most severe first, each entry carrying its own fix — the same
165
176
  // shape the pre-command header renders, so a reader who has seen the
166
177
  // amber line recognises it here rather than learning a second format.
@@ -188,6 +199,22 @@ Examples:
188
199
  process.exit(exitCode);
189
200
  });
190
201
  }
202
+ /**
203
+ * The API result decides first; `--strict` only turns a healthy run with an
204
+ * outdated CLI into exit 7. An unknown latest version (offline registry) is
205
+ * not "outdated": the check could not tell.
206
+ *
207
+ * Exported for tests.
208
+ */
209
+ export function resolveDoctorExitCode(input) {
210
+ if (input.apiExitCode !== ExitCodes.Success) {
211
+ return input.apiExitCode;
212
+ }
213
+ if (input.strict === true && input.cliUpToDate === false) {
214
+ return ExitCodes.Outdated;
215
+ }
216
+ return ExitCodes.Success;
217
+ }
191
218
  function describeApiIssue(api) {
192
219
  if (api.ok === true) {
193
220
  return undefined;
@@ -0,0 +1,31 @@
1
+ declare const CODES: {
2
+ readonly notEnoughCredits: "insufficient_credits";
3
+ readonly userCreditsLimitReached: "user_credits_limit_reached";
4
+ };
5
+ export type InsufficientCreditsCode = (typeof CODES)[keyof typeof CODES];
6
+ export type InsufficientCreditsFix = {
7
+ kind: string;
8
+ description: string;
9
+ url?: string;
10
+ command?: string;
11
+ };
12
+ export type InsufficientCredits = {
13
+ code: InsufficientCreditsCode;
14
+ errorMessage: string | undefined;
15
+ creditsBalance: number | null;
16
+ estimatedCreditsCost: number | null;
17
+ billingUrl: string | null;
18
+ fixes: InsufficientCreditsFix[];
19
+ };
20
+ /** The error, when `body` is one; undefined for every other API error. */
21
+ export declare function readInsufficientCredits(body: unknown): InsufficientCredits | undefined;
22
+ /**
23
+ * One line for the message, the fixes as fields. The JSON form (non-TTY or a
24
+ * coding agent) carries every field verbatim beside `body`.
25
+ */
26
+ export declare function describeInsufficientCredits(error: InsufficientCredits): {
27
+ message: string;
28
+ extra: Record<string, unknown>;
29
+ };
30
+ export {};
31
+ //# sourceMappingURL=insufficientCredits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insufficientCredits.d.ts","sourceRoot":"","sources":["../../src/commands/insufficientCredits.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,KAAK;;;CAGD,CAAC;AAEX,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO,KAAK,CAAC,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,sBAAsB,EAAE,CAAC;CACjC,CAAC;AA8BF,0EAA0E;AAC1E,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,OAAO,GACZ,mBAAmB,GAAG,SAAS,CA8BjC;AAgED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,mBAAmB,GAAG;IACvE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAwCA"}
@@ -0,0 +1,137 @@
1
+ // The API's out-of-credits error, read back and said in one line a person or
2
+ // an agent can act on.
3
+ //
4
+ // The backend answers `{ reason: "notEnoughCredits" | "userCreditsLimitReached" }`
5
+ // and, since the structured error, also `code`, `errorMessage`,
6
+ // `creditsBalance`, `estimatedCreditsCost`, `billingUrl` and `fixes`. Both
7
+ // shapes are read, so a CLI talking to an older backend still names the fix.
8
+ const CODES = {
9
+ notEnoughCredits: "insufficient_credits",
10
+ userCreditsLimitReached: "user_credits_limit_reached",
11
+ };
12
+ // Named when the backend sent no fixes of its own (an older backend).
13
+ const FALLBACK_FIXES = {
14
+ insufficient_credits: [
15
+ {
16
+ kind: "upgradePlan",
17
+ description: "Buy more credits by raising the plan's allotment",
18
+ command: "cargo-ai billing subscription update-plan --credits-count <n> --cadence monthly",
19
+ },
20
+ {
21
+ kind: "enableTopup",
22
+ description: "Top up automatically when credits run low",
23
+ command: "cargo-ai billing subscription update-topup --enabled --credits-count <n> --threshold <n>",
24
+ },
25
+ ],
26
+ user_credits_limit_reached: [
27
+ {
28
+ kind: "askAdmin",
29
+ description: "Ask a workspace admin to raise or remove your monthly credits limit",
30
+ },
31
+ ],
32
+ };
33
+ /** The error, when `body` is one; undefined for every other API error. */
34
+ export function readInsufficientCredits(body) {
35
+ if (body === null || typeof body !== "object") {
36
+ return undefined;
37
+ }
38
+ const record = body;
39
+ const code = resolveCode(record);
40
+ if (code === undefined) {
41
+ return undefined;
42
+ }
43
+ return {
44
+ code,
45
+ errorMessage: typeof record["errorMessage"] === "string"
46
+ ? record["errorMessage"]
47
+ : undefined,
48
+ creditsBalance: typeof record["creditsBalance"] === "number"
49
+ ? record["creditsBalance"]
50
+ : null,
51
+ estimatedCreditsCost: typeof record["estimatedCreditsCost"] === "number"
52
+ ? record["estimatedCreditsCost"]
53
+ : null,
54
+ billingUrl: typeof record["billingUrl"] === "string" ? record["billingUrl"] : null,
55
+ fixes: readFixes(record["fixes"], code),
56
+ };
57
+ }
58
+ function resolveCode(record) {
59
+ const code = record["code"];
60
+ if (code === CODES.notEnoughCredits ||
61
+ code === CODES.userCreditsLimitReached) {
62
+ return code;
63
+ }
64
+ const reason = record["reason"];
65
+ if (reason === "notEnoughCredits") {
66
+ return CODES.notEnoughCredits;
67
+ }
68
+ if (reason === "userCreditsLimitReached") {
69
+ return CODES.userCreditsLimitReached;
70
+ }
71
+ return undefined;
72
+ }
73
+ function readFixes(value, code) {
74
+ if (Array.isArray(value) === false) {
75
+ return FALLBACK_FIXES[code];
76
+ }
77
+ const fixes = value.flatMap((entry) => {
78
+ if (entry === null || typeof entry !== "object") {
79
+ return [];
80
+ }
81
+ const fix = entry;
82
+ if (typeof fix["kind"] !== "string" ||
83
+ typeof fix["description"] !== "string") {
84
+ return [];
85
+ }
86
+ return [
87
+ {
88
+ kind: fix["kind"],
89
+ description: fix["description"],
90
+ ...(typeof fix["url"] === "string" ? { url: fix["url"] } : {}),
91
+ ...(typeof fix["command"] === "string"
92
+ ? { command: fix["command"] }
93
+ : {}),
94
+ },
95
+ ];
96
+ });
97
+ return fixes.length > 0 ? fixes : FALLBACK_FIXES[code];
98
+ }
99
+ /**
100
+ * One line for the message, the fixes as fields. The JSON form (non-TTY or a
101
+ * coding agent) carries every field verbatim beside `body`.
102
+ */
103
+ export function describeInsufficientCredits(error) {
104
+ const headline = error.code === CODES.userCreditsLimitReached
105
+ ? "You have reached your monthly credits limit in this workspace"
106
+ : "This workspace is out of credits";
107
+ const balance = error.creditsBalance !== null
108
+ ? ` (balance: ${formatCredits(error.creditsBalance)}`
109
+ : "";
110
+ const cost = error.estimatedCreditsCost !== null
111
+ ? `${balance === "" ? " (" : ", "}estimated cost: ${formatCredits(error.estimatedCreditsCost)}`
112
+ : "";
113
+ const closing = balance !== "" || cost !== "" ? ")" : "";
114
+ const firstFix = error.fixes[0];
115
+ const fixHint = firstFix === undefined
116
+ ? ""
117
+ : ` ${firstFix.description}${firstFix.command !== undefined
118
+ ? `: ${firstFix.command}`
119
+ : firstFix.url !== undefined
120
+ ? `: ${firstFix.url}`
121
+ : error.billingUrl !== null
122
+ ? `: ${error.billingUrl}`
123
+ : ""}.`;
124
+ return {
125
+ message: `${headline}${balance}${cost}${closing}.${fixHint}`,
126
+ extra: {
127
+ code: error.code,
128
+ creditsBalance: error.creditsBalance,
129
+ estimatedCreditsCost: error.estimatedCreditsCost,
130
+ billingUrl: error.billingUrl,
131
+ fixes: error.fixes,
132
+ },
133
+ };
134
+ }
135
+ function formatCredits(count) {
136
+ return count.toLocaleString("en-US", { maximumFractionDigits: 2 });
137
+ }
@@ -5,6 +5,10 @@ export declare const ExitCodes: {
5
5
  readonly NotAuthenticated: 3;
6
6
  readonly PermissionDenied: 4;
7
7
  readonly NotFound: 5;
8
+ /** The workspace is out of credits, or the member is over their limit. */
9
+ readonly InsufficientCredits: 6;
10
+ /** Only `doctor --strict`: the CLI is behind the latest npm release. */
11
+ readonly Outdated: 7;
8
12
  };
9
13
  export type ExitCode = (typeof ExitCodes)[keyof typeof ExitCodes];
10
14
  export declare const colorize: (text: string, code: string) => string;
@@ -54,6 +58,18 @@ type HandleApiCallOpts = {
54
58
  notAuthenticatedHint?: string;
55
59
  };
56
60
  export declare function handleApiCall<T>(fn: () => Promise<T>, opts?: HandleApiCallOpts): Promise<T>;
61
+ /**
62
+ * The message, exit code and machine-readable fields for a failed API call.
63
+ * `extra` is lifted to the top of the JSON error so an agent can branch on
64
+ * `code` without digging into `body`.
65
+ *
66
+ * Exported for tests.
67
+ */
68
+ export declare function describeApiError(status: number | undefined, body: unknown, notAuthenticatedHint: string | undefined): {
69
+ message: string;
70
+ code: ExitCode;
71
+ extra?: Record<string, unknown>;
72
+ };
57
73
  export declare function sleep(ms: number): Promise<void>;
58
74
  /**
59
75
  * Poll until the thing reports a `finishedAt`.
@@ -1 +1 @@
1
- {"version":3,"file":"runHandler.d.ts","sourceRoot":"","sources":["../../src/commands/runHandler.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,SAAS;;;;;;;CAOZ,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAOlE,eAAO,MAAM,QAAQ,SAAU,MAAM,QAAQ,MAAM,KAAG,MAKrD,CAAC;AAEF,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,CAgCpE;AAQD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAmBlE;AAED,MAAM,MAAM,OAAO,GAAG;IAEpB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAGlC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAU5B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAIF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAwBrD;AAgED,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;IACzB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,wBAAsB,aAAa,CAAC,CAAC,EACnC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,CAAC,CAAC,CAgEZ;AA0ED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,aAAa,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9D,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC,CAyBZ"}
1
+ {"version":3,"file":"runHandler.d.ts","sourceRoot":"","sources":["../../src/commands/runHandler.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,SAAS;;;;;;;IAOpB,0EAA0E;;IAE1E,wEAAwE;;CAEhE,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAMlE,eAAO,MAAM,QAAQ,SAAU,MAAM,QAAQ,MAAM,KAAG,MAKrD,CAAC;AAEF,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;AAKD,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYhE;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,CA2CpE;AAsBD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAmBlE;AAED,MAAM,MAAM,OAAO,GAAG;IAEpB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAGlC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAU5B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAIF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAwBrD;AAiED,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;IACzB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,wBAAsB,aAAa,CAAC,CAAC,EACnC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,CAAC,CAAC,CAoEZ;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,EAAE,OAAO,EACb,oBAAoB,EAAE,MAAM,GAAG,SAAS,GACvC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CA8DtE;AAwBD,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,aAAa,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9D,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC,CAyBZ"}
@@ -1,5 +1,8 @@
1
1
  import { determineIfIsFetcherError } from "@cargo-ai/api";
2
+ import { canDrawOnStderr, mayWaitForPerson } from "../agentMode.js";
2
3
  import { clipToCells } from "../utils/textWidth.js";
4
+ import { LOGIN_EMAIL_COMMAND } from "./authHints.js";
5
+ import { describeInsufficientCredits, readInsufficientCredits, } from "./insufficientCredits.js";
3
6
  export const ExitCodes = {
4
7
  Success: 0,
5
8
  GenericError: 1,
@@ -7,10 +10,14 @@ export const ExitCodes = {
7
10
  NotAuthenticated: 3,
8
11
  PermissionDenied: 4,
9
12
  NotFound: 5,
13
+ /** The workspace is out of credits, or the member is over their limit. */
14
+ InsufficientCredits: 6,
15
+ /** Only `doctor --strict`: the CLI is behind the latest npm release. */
16
+ Outdated: 7,
10
17
  };
11
- const useColor = process.stderr.isTTY === true &&
12
- process.env["NO_COLOR"] === undefined &&
13
- process.env["CI"] !== "true";
18
+ // A coding agent's PTY reports a TTY, and escape codes in what it reads back
19
+ // are noise it has to strip, so colour follows the same rule as the spinner.
20
+ const useColor = canDrawOnStderr() && process.env["NO_COLOR"] === undefined;
14
21
  export const colorize = (text, code) => {
15
22
  if (useColor !== true) {
16
23
  return text;
@@ -39,10 +46,12 @@ export function success(message) {
39
46
  console.error(`${colors.green("✓")} ${message}`);
40
47
  }
41
48
  // Ask a yes/no question on stderr (stdout stays reserved for JSON). Resolves
42
- // false on a non-TTY stdin (callers should require an explicit --yes there).
49
+ // false on a non-TTY stdin, and for a coding agent or CI whatever stdin looks
50
+ // like (callers should require an explicit --yes there).
43
51
  export async function confirm(question) {
44
- if (process.stdin.isTTY !== true)
52
+ if (process.stdin.isTTY !== true || mayWaitForPerson() === false) {
45
53
  return false;
54
+ }
46
55
  const { createInterface } = await import("node:readline/promises");
47
56
  const rl = createInterface({ input: process.stdin, output: process.stderr });
48
57
  try {
@@ -64,10 +73,21 @@ export function failWith(message, opts) {
64
73
  if (activeSpinner !== undefined) {
65
74
  activeSpinner.stop();
66
75
  }
67
- if (process.stderr.isTTY === true) {
76
+ // A person gets the coloured line; anything else (a pipe, CI, a coding
77
+ // agent even on a PTY) gets one JSON object it can parse.
78
+ if (canDrawOnStderr() === true) {
68
79
  process.stderr.write(`${colors.red("✗")} ${message}\n`);
69
80
  if (extra !== undefined) {
70
81
  for (const [key, value] of Object.entries(extra)) {
82
+ if (value === null || value === undefined) {
83
+ continue;
84
+ }
85
+ if (key === "fixes" && Array.isArray(value)) {
86
+ for (const fix of value) {
87
+ process.stderr.write(` ${colors.dim("fix:")} ${formatFix(fix)}\n`);
88
+ }
89
+ continue;
90
+ }
71
91
  const valueStr = typeof value === "string" ? value : JSON.stringify(value);
72
92
  process.stderr.write(` ${colors.dim(`${key}:`)} ${valueStr}\n`);
73
93
  }
@@ -82,6 +102,16 @@ export function failWith(message, opts) {
82
102
  }
83
103
  process.exit(code);
84
104
  }
105
+ /** `Top up automatically: cargo-ai billing …`, for the human error format. */
106
+ function formatFix(fix) {
107
+ const description = typeof fix["description"] === "string" ? fix["description"] : "";
108
+ const target = typeof fix["command"] === "string"
109
+ ? colors.cyan(fix["command"])
110
+ : typeof fix["url"] === "string"
111
+ ? colors.cyan(fix["url"])
112
+ : undefined;
113
+ return target === undefined ? description : `${description}: ${target}`;
114
+ }
85
115
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
86
116
  const SPINNER_INTERVAL_MS = 80;
87
117
  /** Assumed width when the terminal will not say — the conventional default. */
@@ -130,9 +160,10 @@ export function startSpinner(message) {
130
160
  return tracked;
131
161
  }
132
162
  function createSpinner(message) {
133
- if (process.stderr.isTTY !== true) {
134
- // Non-TTY (CI, piped): no animation — just emit each line so progress is
135
- // still visible in logs.
163
+ if (canDrawOnStderr() === false) {
164
+ // Non-TTY (CI, piped) or a coding agent: no animation, whose redraws an
165
+ // agent reads back as a wall of escape codes. Just emit each line so
166
+ // progress is still visible in logs.
136
167
  process.stderr.write(`${message}\n`);
137
168
  return {
138
169
  update: (m) => process.stderr.write(`${m}\n`),
@@ -254,25 +285,50 @@ export async function handleApiCall(fn, opts) {
254
285
  : parsedError.body !== undefined
255
286
  ? parsedError.body
256
287
  : String(error);
257
- const { message, code } = describeApiError(status, body, opts !== undefined ? opts.notAuthenticatedHint : undefined);
288
+ const { message, code, extra } = describeApiError(status, body, opts !== undefined ? opts.notAuthenticatedHint : undefined);
258
289
  failWith(message, {
259
290
  code,
260
- extra: { status: status !== undefined ? status : "unknown", body },
291
+ extra: {
292
+ ...extra,
293
+ status: status !== undefined ? status : "unknown",
294
+ body,
295
+ },
261
296
  });
262
297
  }
263
298
  throw error;
264
299
  }
265
300
  }
266
- function describeApiError(status, body, notAuthenticatedHint) {
301
+ /**
302
+ * The message, exit code and machine-readable fields for a failed API call.
303
+ * `extra` is lifted to the top of the JSON error so an agent can branch on
304
+ * `code` without digging into `body`.
305
+ *
306
+ * Exported for tests.
307
+ */
308
+ export function describeApiError(status, body, notAuthenticatedHint) {
267
309
  const detail = extractApiErrorDetail(body);
268
310
  const detailSuffix = detail !== undefined ? `: ${detail}` : "";
311
+ // Checked before the status: the orchestration routes answer it as a 400
312
+ // and the newer ones as a 402, and either way the fix is the same.
313
+ const insufficientCredits = readInsufficientCredits(body);
314
+ if (insufficientCredits !== undefined) {
315
+ const described = describeInsufficientCredits(insufficientCredits);
316
+ return {
317
+ message: described.message,
318
+ code: ExitCodes.InsufficientCredits,
319
+ extra: described.extra,
320
+ };
321
+ }
269
322
  if (status === 401) {
270
323
  const hint = notAuthenticatedHint !== undefined
271
324
  ? notAuthenticatedHint
272
- : "Run `cargo-ai login` to refresh credentials.";
325
+ : `Sign in again with \`${LOGIN_EMAIL_COMMAND}\` (emails a code, no browser needed).`;
273
326
  return {
274
327
  message: `Not authenticated${detailSuffix}. ${hint}`,
275
328
  code: ExitCodes.NotAuthenticated,
329
+ extra: notAuthenticatedHint !== undefined
330
+ ? undefined
331
+ : { code: "not_authenticated", fix: LOGIN_EMAIL_COMMAND },
276
332
  };
277
333
  }
278
334
  if (status === 403) {
@@ -1 +1 @@
1
- {"version":3,"file":"signedOutNotice.d.ts","sourceRoot":"","sources":["../../src/commands/signedOutNotice.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,eAAe,2CAA2C,CAAC;AAExE,eAAO,MAAM,iBAAiB,gMAA8K,CAAC;AAE7M,wBAAgB,aAAa,IAAI,IAAI,CAIpC"}
1
+ {"version":3,"file":"signedOutNotice.d.ts","sourceRoot":"","sources":["../../src/commands/signedOutNotice.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,eAAe,2CAAsB,CAAC;AAEnD,eAAO,MAAM,iBAAiB,gMAA+I,CAAC;AAE9K,wBAAgB,aAAa,IAAI,IAAI,CAIpC"}
@@ -1,9 +1,10 @@
1
1
  // What a signed-out caller of a command that reads the public routes is told,
2
2
  // once, on stderr: stdout stays the JSON the command was asked for.
3
3
  import { isSignedIn } from "@cargo-ai/cdk/cli";
4
+ import { LOGIN_EMAIL_COMMAND, SIGNUP_OFFER } from "./authHints.js";
4
5
  import { info } from "./runHandler.js";
5
- export const SIGN_UP_COMMAND = "cargo-ai login --email you@company.com";
6
- export const SIGNED_OUT_NOTICE = `Not signed in: reading the official integrations and their actions only. Sign up with \`${SIGN_UP_COMMAND}\` (100 free credits for your first month, no card) to run one.`;
6
+ export const SIGN_UP_COMMAND = LOGIN_EMAIL_COMMAND;
7
+ export const SIGNED_OUT_NOTICE = `Not signed in: reading the official integrations and their actions only. Sign up with \`${SIGN_UP_COMMAND}\` (${SIGNUP_OFFER}) to run one.`;
7
8
  export function noteSignedOut() {
8
9
  if (isSignedIn() === false) {
9
10
  info(SIGNED_OUT_NOTICE);
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, creditsLeftOf, declareSources, fetchStateConnected, fetchSubscription, fetchWorkspaceName, findProjectDir, formatWorkspaceSuffix, isInteractive, isUnpaidPlan, printSetupIssues, registerCommands as registerCdkCommands, runPalette, SessionExpiredError, skillsInstalledFor, } from "@cargo-ai/cdk/cli";
3
+ import { availableCodingAgents, BRAND, canRunPalette, creditsLeftOf, declareSources, fetchStateConnected, fetchSubscription, fetchWorkspaceName, findProjectDir, formatWorkspaceSuffix, isCodingAgent, isInteractive, isUnpaidPlan, printSetupIssues, registerCommands as registerCdkCommands, runPalette, SessionExpiredError, skillsInstalledFor, } 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";
@@ -9,6 +9,7 @@ import { pinAskBooleanFlags } from "./commands/ask/argvFlags.js";
9
9
  import { registerAskCommand } from "./commands/ask/index.js";
10
10
  import { rewriteChatFlag } from "./commands/ask/rewriteChatFlag.js";
11
11
  import { registerAuthCommands } from "./commands/auth/index.js";
12
+ import { LOGIN_EMAIL_COMMAND, NOT_SIGNED_IN_MESSAGE, } from "./commands/authHints.js";
12
13
  import { registerBillingCommands } from "./commands/billing/index.js";
13
14
  import { registerBookDemoCommand } from "./commands/bookDemo.js";
14
15
  import { registerConnectionCommands } from "./commands/connection/index.js";
@@ -37,7 +38,7 @@ import { getConfig } from "./config.js";
37
38
  import { removeLegacySessionHooks } from "./legacySessionHooks.js";
38
39
  import { buildPaletteLayout } from "./paletteLayout.js";
39
40
  import { collectSetupIssues, isSignedIn } from "./setupIssues.js";
40
- import { cachedLatestSkillsPin, cachedLatestVersion, maybeRefreshUpdateCache, } from "./updateNotifier.js";
41
+ import { cachedLatestSkillsPin, cachedLatestVersion, maybeNoticeOutdatedForAgent, maybeRefreshUpdateCache, } from "./updateNotifier.js";
41
42
  const require = createRequire(import.meta.url);
42
43
  const { version } = require("../package.json");
43
44
  const program = new Command();
@@ -90,9 +91,15 @@ Exit codes:
90
91
  0 Success
91
92
  1 Generic error
92
93
  2 Invalid usage (missing/conflicting flags)
93
- 3 Not authenticated (run \`cargo-ai login\`)
94
+ 3 Not authenticated (run \`cargo-ai login --email you@company.com\`)
94
95
  4 Permission denied
95
96
  5 Not found
97
+ 6 Out of credits, or over your monthly credits limit (the error names the fix)
98
+ 7 CLI behind the latest release (\`cargo-ai doctor --strict\` only)
99
+
100
+ Coding agents (Claude Code, Cursor, Codex, Gemini CLI, Copilot, OpenCode, or
101
+ any harness setting AI_AGENT) and CI get no prompts, spinners or colour, errors
102
+ as one JSON line on stderr, and the exact flag to pass instead of a question.
96
103
 
97
104
  Run "cargo-ai <command> --help" for details on a specific command group.
98
105
  Run "cargo-ai <command> <subcommand> --help" for details on a subcommand.`);
@@ -130,6 +137,13 @@ program.hook("preAction", (_program, action) => {
130
137
  if (SETUP_HEADER_SKIP.includes(rootCommandName(action))) {
131
138
  return;
132
139
  }
140
+ // A coding agent can run on a PTY, so the TTY check behind the header does
141
+ // not keep it out. Agents get the one line they can act on instead, and
142
+ // stderr stays a single parseable JSON error when a command fails.
143
+ if (isCodingAgent() === true) {
144
+ maybeNoticeOutdatedForAgent(version);
145
+ return;
146
+ }
133
147
  printSetupIssues(collectSetupIssues({
134
148
  currentVersion: version,
135
149
  latestVersion: cachedLatestVersion(),
@@ -147,7 +161,13 @@ function rootCommandName(command) {
147
161
  const getApi = () => {
148
162
  const { baseUrl, getAccessToken, workspaceUuid } = getConfig();
149
163
  if (getAccessToken === undefined) {
150
- failWith("Not authenticated. Run 'cargo-ai login --email <email>' (no browser needed) or 'cargo-ai login --oauth'.", { code: ExitCodes.NotAuthenticated });
164
+ failWith(NOT_SIGNED_IN_MESSAGE, {
165
+ code: ExitCodes.NotAuthenticated,
166
+ extra: {
167
+ code: "not_authenticated",
168
+ fix: LOGIN_EMAIL_COMMAND,
169
+ },
170
+ });
151
171
  }
152
172
  return createApi({ baseUrl, getAccessToken, workspaceUuid });
153
173
  };
@@ -16,9 +16,29 @@ export declare function cachedLatestSkillsPin(): string | undefined;
16
16
  * palette) can put an outdated CLI in that list instead of printing a footer
17
17
  * after the fact.
18
18
  *
19
- * Stays silent on non-TTY stderr (scripts/CI), when disabled via
20
- * `CARGO_NO_UPDATE_NOTIFIER`, and on any network/parse failure — it must
21
- * never block, delay, or break the command that triggered it.
19
+ * Skipped on non-TTY stderr (scripts/CI), when disabled via
20
+ * `CARGO_NO_UPDATE_NOTIFIER`, and silent on any network/parse failure — it
21
+ * must never block, delay, or break the command that triggered it. A coding
22
+ * agent is the exception to the TTY rule: it runs without one, and an agent
23
+ * whose CLI never refreshes this cache would never learn it is behind. The
24
+ * cost is the same as a person's: one 1.5s-capped fetch a day, after the
25
+ * command has finished.
22
26
  */
23
27
  export declare function maybeRefreshUpdateCache(): Promise<string | undefined>;
28
+ /**
29
+ * The one line a coding agent is told when its CLI is behind, or undefined.
30
+ * Pure, for tests; `maybeNoticeOutdatedForAgent` is the caller.
31
+ */
32
+ export declare function formatAgentUpdateNotice(input: {
33
+ currentVersion: string;
34
+ latestVersion: string | undefined;
35
+ lastNoticeAt: number | undefined;
36
+ now: number;
37
+ }): string | undefined;
38
+ /**
39
+ * Agents never see the amber setup header (index.ts skips it for them), so the outdated
40
+ * CLI is said to them as one stderr line instead, at most hourly, from the
41
+ * cache alone: no network on the command's path.
42
+ */
43
+ export declare function maybeNoticeOutdatedForAgent(currentVersion: string): void;
24
44
  //# sourceMappingURL=updateNotifier.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"updateNotifier.d.ts","sourceRoot":"","sources":["../src/updateNotifier.ts"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAYxD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,SAAS,CAY1D;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAwC3E"}
1
+ {"version":3,"file":"updateNotifier.d.ts","sourceRoot":"","sources":["../src/updateNotifier.ts"],"names":[],"mappings":"AAqBA;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAYxD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,SAAS,CAY1D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA2C3E;AAID;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,MAAM,GAAG,SAAS,CAiBrB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAyBxE"}
@@ -1,4 +1,5 @@
1
- import { fetchLatestSkillsPin, fetchLatestVersion, readUpdateCache, writeUpdateCache, } from "./version.js";
1
+ import { isCodingAgent } from "@cargo-ai/cdk/cli";
2
+ import { fetchLatestSkillsPin, fetchLatestVersion, isOutdated, readUpdateCache, UPDATE_COMMAND, writeUpdateCache, } from "./version.js";
2
3
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // once per day
3
4
  const NOTIFY_FETCH_TIMEOUT_MS = 1500;
4
5
  const isDisabled = () => {
@@ -41,12 +42,16 @@ export function cachedLatestSkillsPin() {
41
42
  * palette) can put an outdated CLI in that list instead of printing a footer
42
43
  * after the fact.
43
44
  *
44
- * Stays silent on non-TTY stderr (scripts/CI), when disabled via
45
- * `CARGO_NO_UPDATE_NOTIFIER`, and on any network/parse failure — it must
46
- * never block, delay, or break the command that triggered it.
45
+ * Skipped on non-TTY stderr (scripts/CI), when disabled via
46
+ * `CARGO_NO_UPDATE_NOTIFIER`, and silent on any network/parse failure — it
47
+ * must never block, delay, or break the command that triggered it. A coding
48
+ * agent is the exception to the TTY rule: it runs without one, and an agent
49
+ * whose CLI never refreshes this cache would never learn it is behind. The
50
+ * cost is the same as a person's: one 1.5s-capped fetch a day, after the
51
+ * command has finished.
47
52
  */
48
53
  export async function maybeRefreshUpdateCache() {
49
- if (process.stderr.isTTY !== true) {
54
+ if (process.stderr.isTTY !== true && isCodingAgent() === false) {
50
55
  return cachedLatestVersion();
51
56
  }
52
57
  if (isDisabled() === true) {
@@ -72,6 +77,9 @@ export async function maybeRefreshUpdateCache() {
72
77
  lastCheck: now,
73
78
  latestVersion,
74
79
  ...(latestSkillsPin !== undefined ? { latestSkillsPin } : {}),
80
+ ...(cache !== undefined && cache.agentNoticeAt !== undefined
81
+ ? { agentNoticeAt: cache.agentNoticeAt }
82
+ : {}),
75
83
  };
76
84
  writeUpdateCache(cache);
77
85
  }
@@ -82,3 +90,47 @@ export async function maybeRefreshUpdateCache() {
82
90
  return cachedLatestVersion();
83
91
  }
84
92
  }
93
+ const AGENT_NOTICE_INTERVAL_MS = 60 * 60 * 1000; // once per hour
94
+ /**
95
+ * The one line a coding agent is told when its CLI is behind, or undefined.
96
+ * Pure, for tests; `maybeNoticeOutdatedForAgent` is the caller.
97
+ */
98
+ export function formatAgentUpdateNotice(input) {
99
+ if (input.latestVersion === undefined) {
100
+ return undefined;
101
+ }
102
+ if (isOutdated(input.currentVersion, input.latestVersion) === false) {
103
+ return undefined;
104
+ }
105
+ if (input.lastNoticeAt !== undefined &&
106
+ input.now - input.lastNoticeAt < AGENT_NOTICE_INTERVAL_MS) {
107
+ return undefined;
108
+ }
109
+ return `cargo-ai ${input.currentVersion} is behind the latest release ${input.latestVersion}. Update with: ${UPDATE_COMMAND}`;
110
+ }
111
+ /**
112
+ * Agents never see the amber setup header (index.ts skips it for them), so the outdated
113
+ * CLI is said to them as one stderr line instead, at most hourly, from the
114
+ * cache alone: no network on the command's path.
115
+ */
116
+ export function maybeNoticeOutdatedForAgent(currentVersion) {
117
+ if (isDisabled() === true || isCodingAgent() === false) {
118
+ return;
119
+ }
120
+ const cache = readUpdateCache();
121
+ if (cache === undefined) {
122
+ return;
123
+ }
124
+ const now = Date.now();
125
+ const line = formatAgentUpdateNotice({
126
+ currentVersion,
127
+ latestVersion: cache.latestVersion,
128
+ lastNoticeAt: cache.agentNoticeAt,
129
+ now,
130
+ });
131
+ if (line === undefined) {
132
+ return;
133
+ }
134
+ process.stderr.write(`${line}\n`);
135
+ writeUpdateCache({ ...cache, agentNoticeAt: now });
136
+ }
@@ -1,7 +1,21 @@
1
+ /// <reference types="node" resolution-mode="require"/>
1
2
  /**
2
3
  * Best-effort browser launch. Every caller also prints the URL, so a failure
3
4
  * here is never fatal: headless boxes and sandboxes simply fall back to the
4
5
  * user opening the link themselves.
5
6
  */
6
7
  export declare function openBrowser(url: string): void;
8
+ /**
9
+ * The command that hands `url` to the default browser.
10
+ *
11
+ * Windows goes through rundll32 rather than `cmd /c start`: cmd parses its
12
+ * command line, so every `&` in a query string ends the command there and the
13
+ * browser opens a truncated URL. rundll32 takes the URL as a plain argument.
14
+ *
15
+ * Exported for tests.
16
+ */
17
+ export declare function buildOpenCommand(os: NodeJS.Platform, url: string): {
18
+ command: string;
19
+ args: string[];
20
+ };
7
21
  //# sourceMappingURL=openBrowser.d.ts.map
@@ -1 +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"}
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,CAa7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,CAAC,QAAQ,EACnB,GAAG,EAAE,MAAM,GACV;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CASrC"}
@@ -6,8 +6,7 @@ import { platform } from "node:process";
6
6
  * user opening the link themselves.
7
7
  */
8
8
  export function openBrowser(url) {
9
- const command = pickOpenCommand();
10
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
9
+ const { command, args } = buildOpenCommand(platform, url);
11
10
  try {
12
11
  const child = spawn(command, args, {
13
12
  detached: true,
@@ -20,10 +19,23 @@ export function openBrowser(url) {
20
19
  // Intentionally ignored — see above.
21
20
  }
22
21
  }
23
- function pickOpenCommand() {
24
- if (platform === "darwin")
25
- return "open";
26
- if (platform === "win32")
27
- return "cmd";
28
- return "xdg-open";
22
+ /**
23
+ * The command that hands `url` to the default browser.
24
+ *
25
+ * Windows goes through rundll32 rather than `cmd /c start`: cmd parses its
26
+ * command line, so every `&` in a query string ends the command there and the
27
+ * browser opens a truncated URL. rundll32 takes the URL as a plain argument.
28
+ *
29
+ * Exported for tests.
30
+ */
31
+ export function buildOpenCommand(os, url) {
32
+ if (os === "darwin")
33
+ return { command: "open", args: [url] };
34
+ if (os === "win32") {
35
+ return {
36
+ command: "rundll32",
37
+ args: ["url.dll,FileProtocolHandler", url],
38
+ };
39
+ }
40
+ return { command: "xdg-open", args: [url] };
29
41
  }
@@ -1 +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,CAkB7B"}
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/utils/prompt.ts"],"names":[],"mappings":";AAMA,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAWF,wBAAgB,eAAe,IAAI,WAAW,GAAG,SAAS,CAgBzD;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,CAAC,cAAc,EAC5B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAkB7B"}
@@ -1,11 +1,20 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as readline from "node:readline/promises";
3
3
  import * as tty from "node:tty";
4
+ import { mayWaitForPerson } from "../agentMode.js";
4
5
  // Interactive prompts must not assume stdin is a terminal: `curl | sh`
5
6
  // installers and agent-driven shells pipe stdin while the controlling
6
7
  // terminal stays reachable via /dev/tty. Prefer stdin when it is a TTY,
7
8
  // otherwise fall back to opening /dev/tty directly.
9
+ //
10
+ // Never for a coding agent or CI: their shells often still reach /dev/tty (the
11
+ // person's terminal, behind the agent), so a prompt there waits for an answer
12
+ // the agent cannot type and the command hangs until the harness kills it.
13
+ // Every caller already has a non-interactive path that names the flag to pass.
8
14
  export function openPromptInput() {
15
+ if (mayWaitForPerson() === false) {
16
+ return undefined;
17
+ }
9
18
  if (process.stdin.isTTY === true) {
10
19
  return { stream: process.stdin, close: () => undefined };
11
20
  }
@@ -9,6 +9,11 @@ export type UpdateCache = {
9
9
  * caches written before it existed, and when that fetch failed.
10
10
  */
11
11
  latestSkillsPin?: string;
12
+ /**
13
+ * Epoch milliseconds of the last time a coding agent was told the CLI is
14
+ * behind, so the line is repeated hourly rather than on every command.
15
+ */
16
+ agentNoticeAt?: number;
12
17
  };
13
18
  /**
14
19
  * Fetch the latest published version of the CLI from the npm registry.
@@ -1 +1 @@
1
- {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,cAAc,wCAAwC,CAAC;AAEpE,MAAM,MAAM,WAAW,GAAG;IACxB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,GAAE,MAAiC,GAC3C,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,GAAE,MAAiC,GAC3C,OAAO,CAAC,MAAM,CAAC,CAgBjB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B1D;AAED,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,wBAAgB,eAAe,IAAI,WAAW,GAAG,SAAS,CAyBzD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAOzD"}
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,cAAc,wCAAwC,CAAC;AAEpE,MAAM,MAAM,WAAW,GAAG;IACxB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,GAAE,MAAiC,GAC3C,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,GAAE,MAAiC,GAC3C,OAAO,CAAC,MAAM,CAAC,CAgBjB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B1D;AAED,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,wBAAgB,eAAe,IAAI,WAAW,GAAG,SAAS,CA4BzD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAOzD"}
package/build/version.js CHANGED
@@ -95,6 +95,9 @@ export function readUpdateCache() {
95
95
  ...(typeof parsed.latestSkillsPin === "string"
96
96
  ? { latestSkillsPin: parsed.latestSkillsPin }
97
97
  : {}),
98
+ ...(typeof parsed.agentNoticeAt === "number"
99
+ ? { agentNoticeAt: parsed.agentNoticeAt }
100
+ : {}),
98
101
  };
99
102
  }
100
103
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cargo-ai/cli",
3
- "version": "1.0.143",
3
+ "version": "1.0.146",
4
4
  "mcpName": "io.github.getcargohq/cargo",
5
5
  "private": false,
6
6
  "license": "UNLICENSED",
@@ -75,9 +75,9 @@
75
75
  "@ai-sdk/harness": "1.0.144",
76
76
  "@ai-sdk/harness-claude-code": "1.0.148",
77
77
  "@cargo-ai/api": "^1.0.107",
78
- "@cargo-ai/app-sdk": "^1.0.11",
79
- "@cargo-ai/cdk": "^1.0.137",
80
- "@cargo-ai/types": "^1.0.106",
78
+ "@cargo-ai/app-sdk": "^1.0.12",
79
+ "@cargo-ai/cdk": "^1.0.140",
80
+ "@cargo-ai/types": "^1.0.108",
81
81
  "@cargo-ai/utils": "^1.0.37",
82
82
  "@cargo-ai/worker-sdk": "^1.0.21",
83
83
  "@modelcontextprotocol/sdk": "^1.32.0",