@cargo-ai/cli 1.0.72 → 1.0.73

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.
@@ -1,3 +1,4 @@
1
+ import type { ResolvedWorkspace } from "./workspace.js";
1
2
  /** `--code -` reads the code from stdin, keeping it out of shell history. */
2
3
  export declare const STDIN_CODE = "-";
3
4
  export type EmailLoginOptions = {
@@ -6,11 +7,34 @@ export type EmailLoginOptions = {
6
7
  workspaceName?: string;
7
8
  workspaceUuid?: string;
8
9
  baseUrl?: string;
10
+ /**
11
+ * What to re-run when a code was sent but there is no terminal to type it
12
+ * into. Defaults to the `login` form. `start` passes its own, because
13
+ * finishing through `login` would sign the user in and stop there, losing
14
+ * the destination the run was headed for.
15
+ */
16
+ resumeCommand?: string;
17
+ };
18
+ export type EmailSignInResult = {
19
+ outcome: "signedIn";
20
+ email: string;
21
+ workspace: ResolvedWorkspace;
22
+ }
23
+ /** The code was sent, but there is no terminal to type it back into. */
24
+ | {
25
+ outcome: "codeSent";
26
+ email: string;
27
+ next: string;
9
28
  };
10
29
  /**
11
30
  * Signs in with a code emailed by the identity provider, creating the account
12
31
  * on first use. This is the only channel that needs neither a browser nor an
13
32
  * existing credential.
33
+ *
34
+ * Returns rather than prints, because two callers need the same exchange and
35
+ * report it differently: `login` writes the JSON result a script reads, while
36
+ * `start` carries on to the next question.
14
37
  */
38
+ export declare function signInWithEmail(opts: EmailLoginOptions): Promise<EmailSignInResult>;
15
39
  export declare function runEmailLogin(opts: EmailLoginOptions): Promise<void>;
16
40
  //# sourceMappingURL=email.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/email.ts"],"names":[],"mappings":"AAcA,6EAA6E;AAC7E,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8C1E"}
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/email.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,6EAA6E;AAC7E,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,iBAAiB,CAAA;CAAE;AACtE,wEAAwE;GACtE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,iBAAiB,CAAC,CA8B5B;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B1E"}
@@ -10,24 +10,22 @@ export const STDIN_CODE = "-";
10
10
  * Signs in with a code emailed by the identity provider, creating the account
11
11
  * on first use. This is the only channel that needs neither a browser nor an
12
12
  * existing credential.
13
+ *
14
+ * Returns rather than prints, because two callers need the same exchange and
15
+ * report it differently: `login` writes the JSON result a script reads, while
16
+ * `start` carries on to the next question.
13
17
  */
14
- export async function runEmailLogin(opts) {
18
+ export async function signInWithEmail(opts) {
15
19
  const email = opts.email;
16
20
  if (email === undefined || email.length === 0) {
17
21
  failWith("Must pass --email <email>.", { code: ExitCodes.InvalidUsage });
18
22
  }
19
- const resumeCommand = `cargo-ai login --email ${email} --code <code>`;
23
+ const resumeCommand = opts.resumeCommand !== undefined
24
+ ? opts.resumeCommand
25
+ : `cargo-ai login --email ${email} --code <code>`;
20
26
  const code = await resolveCode({ email, code: opts.code, resumeCommand });
21
27
  if (code === undefined) {
22
- // The code was sent but there is no terminal to type it into. Exit
23
- // successfully so an agent can relay the code and re-run non-interactively.
24
- outputJson({
25
- ok: true,
26
- status: "codeSent",
27
- email,
28
- next: resumeCommand,
29
- });
30
- return;
28
+ return { outcome: "codeSent", email, next: resumeCommand };
31
29
  }
32
30
  const spinner = startSpinner("Verifying code...");
33
31
  const tokens = await exchangeEmailOtp({ email, otp: code });
@@ -38,15 +36,30 @@ export async function runEmailLogin(opts) {
38
36
  workspaceUuid: opts.workspaceUuid,
39
37
  workspaceName: opts.workspaceName,
40
38
  });
41
- success(`Signed in as ${email}. Workspace: ${workspace.name}.`);
39
+ return { outcome: "signedIn", email, workspace };
40
+ }
41
+ export async function runEmailLogin(opts) {
42
+ const result = await signInWithEmail(opts);
43
+ if (result.outcome === "codeSent") {
44
+ // Exit successfully so an agent can relay the code and re-run
45
+ // non-interactively.
46
+ outputJson({
47
+ ok: true,
48
+ status: "codeSent",
49
+ email: result.email,
50
+ next: result.next,
51
+ });
52
+ return;
53
+ }
54
+ success(`Signed in as ${result.email}. Workspace: ${result.workspace.name}.`);
42
55
  info(AGENT_SKILLS_NEXT_STEP);
43
56
  outputJson({
44
57
  ok: true,
45
58
  status: "signedIn",
46
59
  credentialsFile: getCredentialsPath(),
47
60
  baseUrl: resolveBaseUrl(opts.baseUrl),
48
- email,
49
- workspace,
61
+ email: result.email,
62
+ workspace: result.workspace,
50
63
  tokenType: "session",
51
64
  });
52
65
  }
@@ -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;AA0BxD,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;AAiGD,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;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"}
@@ -5,6 +5,7 @@ import { getConfig } from "../../config.js";
5
5
  import { clearCredentials, CredentialsNotClearedError, getCredentialsPath, loadCredentials, saveCredentials, } from "../../credentials.js";
6
6
  import { parseBooleanOption } from "../../utils/booleanOption.js";
7
7
  import { ExitCodes, failWith, handleApiCall, info, outputJson, success, } from "../runHandler.js";
8
+ import { clearStartState } from "../start/state.js";
8
9
  import { runEmailLogin } from "./email.js";
9
10
  import { selectLoginChannel } from "./loginChannel.js";
10
11
  import { runOAuthDeviceFlow } from "./oauth.js";
@@ -171,16 +172,21 @@ async function runLogout(opts) {
171
172
  }
172
173
  throw error;
173
174
  }
175
+ // Not conditional on `removed`: someone whose credential was an environment
176
+ // variable, or who is signing out a second time, still means "start over".
177
+ const startStateCleared = clearStartState();
174
178
  if (removed === true) {
175
179
  success(revoked === true ? "Signed out and revoked access." : "Signed out.");
176
180
  }
177
181
  else {
178
- info("No saved credentials to remove.");
182
+ info("No saved credentials to remove — nothing was signed in through this CLI.");
183
+ info("If commands still work, the token is coming from CARGO_API_TOKEN or a project .env.");
179
184
  }
180
185
  outputJson({
181
186
  ok: true,
182
187
  removed,
183
188
  revoked,
189
+ startStateCleared,
184
190
  credentialsFile: getCredentialsPath(),
185
191
  });
186
192
  }
@@ -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;AAYzD,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;AAEF,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,CA2H/B;AAcD,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,SAAS,mBAAmB,CAAC,SAAS,EAAE,EACpD,IAAI,EAAE,MAAM,GACX,iBAAiB,GAAG,SAAS,CAY/B"}
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;AAazD,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;AAUF,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,CAkG/B;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,SAAS,mBAAmB,CAAC,SAAS,EAAE,EACpD,IAAI,EAAE,MAAM,GACX,iBAAiB,GAAG,SAAS,CAY/B"}
@@ -1,7 +1,14 @@
1
1
  import * as readline from "node:readline/promises";
2
+ import { askText, isInteractive, selectOne } from "@cargo-ai/cdk/cli";
2
3
  import { createApi } from "../../api.js";
3
4
  import { askQuestion, openPromptInput } from "../../utils/prompt.js";
4
5
  import { ExitCodes, failWith, handleApiCall, info, success, } from "../runHandler.js";
6
+ /**
7
+ * The "make me a new one" row. A leading space keeps it out of the uuid
8
+ * namespace, so it can never collide with a real workspace.
9
+ */
10
+ const CREATE = " create";
11
+ const DEFAULT_WORKSPACE_NAME = "My workspace";
5
12
  export async function resolveWorkspace(opts) {
6
13
  const accessToken = opts.accessToken;
7
14
  const api = createApi({
@@ -34,28 +41,6 @@ export async function resolveWorkspace(opts) {
34
41
  wasCreated: false,
35
42
  };
36
43
  }
37
- if (workspaces.length === 0) {
38
- const promptInput = openPromptInput();
39
- if (promptInput === undefined) {
40
- failWith("No workspaces found. Re-run with --workspace-name <name> to create one.", { code: ExitCodes.NotFound });
41
- }
42
- let name;
43
- try {
44
- info("You don't have any workspaces yet. Let's create one.");
45
- name = await askQuestion(promptInput.stream, "Workspace name (or press Enter to skip): ");
46
- }
47
- finally {
48
- promptInput.close();
49
- }
50
- if (name === undefined) {
51
- failWith("No workspace name provided.", {
52
- code: ExitCodes.InvalidUsage,
53
- });
54
- }
55
- const workspace = await createWorkspace(api, name);
56
- success(`Created workspace: ${workspace.name} (${workspace.uuid})`);
57
- return { workspace, wasCreated: true };
58
- }
59
44
  if (workspaces.length === 1) {
60
45
  const [firstWorkspace] = workspaces;
61
46
  if (firstWorkspace === undefined) {
@@ -66,6 +51,15 @@ export async function resolveWorkspace(opts) {
66
51
  wasCreated: false,
67
52
  };
68
53
  }
54
+ // Picking an existing workspace and creating one are the same decision, so
55
+ // they are one question rather than two branches with two different pieces
56
+ // of UI. The readline path below stays for the terminals clack cannot use.
57
+ if (isInteractive() === true) {
58
+ return await chooseWorkspace(api, workspaces);
59
+ }
60
+ if (workspaces.length === 0) {
61
+ return await createWorkspaceByAsking(api);
62
+ }
69
63
  const promptInput = openPromptInput();
70
64
  if (promptInput === undefined) {
71
65
  failWith("Multiple workspaces available and no interactive terminal; pass --workspace-uuid <uuid> (or set CARGO_WORKSPACE_UUID when using install.sh) to select one.", { code: ExitCodes.InvalidUsage });
@@ -82,10 +76,6 @@ export async function resolveWorkspace(opts) {
82
76
  }
83
77
  return { workspace: selectedWorkspace, wasCreated: false };
84
78
  }
85
- async function createWorkspace(api, name) {
86
- const result = await handleApiCall(() => api.workspaceManagement.workspace.create({ name }), { spinner: "Creating workspace..." });
87
- return { uuid: result.workspace.uuid, name: result.workspace.name };
88
- }
89
79
  export function findWorkspaceByName(workspaces, name) {
90
80
  const wanted = name.trim().toLowerCase();
91
81
  const matched = workspaces.find((workspace) => {
@@ -96,6 +86,89 @@ export function findWorkspaceByName(workspaces, name) {
96
86
  }
97
87
  return { uuid: matched.uuid, name: matched.name };
98
88
  }
89
+ /** Pick from the workspaces you have, or name a new one, in one question. */
90
+ async function chooseWorkspace(api, workspaces) {
91
+ if (workspaces.length === 0) {
92
+ info("You don't have any workspaces yet. Let's create one.");
93
+ return {
94
+ workspace: await createWorkspace(api, await askWorkspaceName()),
95
+ wasCreated: true,
96
+ };
97
+ }
98
+ const chosen = await selectOne({
99
+ // First, not last: past a dozen workspaces the list scrolls, and a create
100
+ // row at the bottom is off-screen — reachable only by someone who already
101
+ // guessed to type "create". The cursor still starts on the first
102
+ // workspace, so the row is visible without Enter landing on it.
103
+ message: "Which workspace should this CLI work in?",
104
+ recommended: workspaces[0] === undefined ? CREATE : workspaces[0].uuid,
105
+ choices: [
106
+ {
107
+ value: CREATE,
108
+ label: "+ Create a new workspace",
109
+ hint: "you name it",
110
+ fallback: true,
111
+ },
112
+ ...workspaces.map((workspace) => ({
113
+ value: workspace.uuid,
114
+ label: workspace.name,
115
+ hint: workspace.uuid,
116
+ })),
117
+ ],
118
+ });
119
+ if (chosen === CREATE) {
120
+ return {
121
+ workspace: await createWorkspace(api, await askWorkspaceName()),
122
+ wasCreated: true,
123
+ };
124
+ }
125
+ const selected = workspaces.find((workspace) => workspace.uuid === chosen);
126
+ if (selected === undefined) {
127
+ failWith("No workspace selected.", { code: ExitCodes.InvalidUsage });
128
+ }
129
+ return {
130
+ workspace: { uuid: selected.uuid, name: selected.name },
131
+ wasCreated: false,
132
+ };
133
+ }
134
+ async function askWorkspaceName() {
135
+ return await askText({
136
+ message: "Name it",
137
+ placeholder: DEFAULT_WORKSPACE_NAME,
138
+ defaultValue: DEFAULT_WORKSPACE_NAME,
139
+ });
140
+ }
141
+ /**
142
+ * The zero-workspace path for terminals clack cannot drive — `curl | sh`
143
+ * installers pipe stdin while the controlling terminal stays reachable through
144
+ * /dev/tty, and this is what reads it.
145
+ */
146
+ async function createWorkspaceByAsking(api) {
147
+ const promptInput = openPromptInput();
148
+ if (promptInput === undefined) {
149
+ failWith("No workspaces found. Re-run with --workspace-name <name> to create one.", { code: ExitCodes.NotFound });
150
+ }
151
+ let name;
152
+ try {
153
+ info("You don't have any workspaces yet. Let's create one.");
154
+ name = await askQuestion(promptInput.stream, "Workspace name (or press Enter to skip): ");
155
+ }
156
+ finally {
157
+ promptInput.close();
158
+ }
159
+ if (name === undefined) {
160
+ failWith("No workspace name provided.", {
161
+ code: ExitCodes.InvalidUsage,
162
+ });
163
+ }
164
+ const workspace = await createWorkspace(api, name);
165
+ success(`Created workspace: ${workspace.name} (${workspace.uuid})`);
166
+ return { workspace, wasCreated: true };
167
+ }
168
+ async function createWorkspace(api, name) {
169
+ const result = await handleApiCall(() => api.workspaceManagement.workspace.create({ name }), { spinner: "Creating workspace..." });
170
+ return { uuid: result.workspace.uuid, name: result.workspace.name };
171
+ }
99
172
  async function promptWorkspaceSelection(workspaces, input) {
100
173
  const rl = readline.createInterface({
101
174
  input,
@@ -1 +1 @@
1
- {"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/commands/connection/connector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAIxC,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAwPN"}
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,CA6TN"}
@@ -1,5 +1,8 @@
1
+ import { awaitBrowserStep, buildCliConnectorCreateUrl, buildCliConnectorEditUrl, isCodingAgent, openBrowser, resolveUserUuid, shouldWaitForPerson, } from "@cargo-ai/cdk/cli";
2
+ import { getConfig } from "../../config.js";
1
3
  import { parseBooleanOption } from "../../utils/booleanOption.js";
2
- import { handleApiCall, outputJson, parseJson } from "../runHandler.js";
4
+ import { openPromptInput } from "../../utils/prompt.js";
5
+ import { colors, ExitCodes, failWith, handleApiCall, info, outputJson, parseJson, } from "../runHandler.js";
3
6
  export function registerConnectorCommands(parent, getApi) {
4
7
  const connector = parent
5
8
  .command("connector")
@@ -79,39 +82,70 @@ export function registerConnectorCommands(parent, getApi) {
79
82
  });
80
83
  connector
81
84
  .command("create")
82
- .description("Create a connector")
83
- .requiredOption("--name <name>", "Connector name (string, required)")
84
- .requiredOption("--slug <slug>", "Connector slug identifier (string, required)")
85
+ .description("Create a connector. Without --config, opens the hosted form in a browser and waits.")
86
+ .option("--name <name>", "Connector name (string, required with --config)")
87
+ .option("--slug <slug>", "Connector slug identifier (string, required with --config)")
85
88
  .requiredOption("--integration-slug <slug>", "Integration slug to connect to (string, required)")
86
- .option("--config <json>", "Connector configuration (JSON object, shape depends on integration)")
89
+ .option("--config <json>", "Connector configuration (JSON object, shape depends on integration). Skips the browser.")
87
90
  .option("--rate-limit <json>", 'Rate limit configuration (JSON object). Shape: {"maxRequests": integer, "windowMs": integer}')
88
91
  .option("--cache-ttl-milliseconds <n>", "Cache TTL in milliseconds (integer)")
89
92
  .addHelpText("after", `
90
93
  Examples:
91
- $ cargo-ai connection connector create --name "HubSpot" --slug hubspot-main --integration-slug hubspot
94
+ # Open the hosted form, wait, then write connectors/hubspot.ts and bind it
95
+ $ cargo-ai connection connector create --integration-slug hubspot
96
+
97
+ # Fully non-interactive — no browser
92
98
  $ cargo-ai connection connector create --name "HubSpot" --slug hubspot-main --integration-slug hubspot \\
93
- --rate-limit '{"maxRequests":100,"windowMs":60000}' --cache-ttl-milliseconds 300000`)
99
+ --config '{"method":"privateApp","accessToken":"pat-..."}'
100
+ $ cargo-ai connection connector create --name "HubSpot" --slug hubspot-main --integration-slug hubspot \\
101
+ --config '{...}' --rate-limit '{"maxRequests":100,"windowMs":60000}' --cache-ttl-milliseconds 300000`)
94
102
  .action(async (opts) => {
95
103
  const api = getApi();
96
- const result = await handleApiCall(() => api.connection.connector.create({
97
- name: opts.name,
98
- slug: opts.slug,
99
- integrationSlug: opts.integrationSlug,
100
- config: opts.config !== undefined
101
- ? parseJson(opts.config, "--config")
102
- : undefined,
103
- rateLimit: opts.rateLimit !== undefined
104
+ // `--config` keeps the original, fully scriptable behaviour.
105
+ if (opts.config !== undefined) {
106
+ const rateLimit = opts.rateLimit !== undefined
104
107
  ? parseJson(opts.rateLimit, "--rate-limit")
105
- : undefined,
106
- cacheTtlMilliseconds: opts.cacheTtlMilliseconds !== undefined
108
+ : undefined;
109
+ const cacheTtlMilliseconds = opts.cacheTtlMilliseconds !== undefined
107
110
  ? parseInt(opts.cacheTtlMilliseconds, 10)
108
- : undefined,
109
- }));
110
- outputJson(result);
111
+ : undefined;
112
+ const name = opts.name;
113
+ const slug = opts.slug;
114
+ if (name === undefined || slug === undefined) {
115
+ failWith("--name and --slug are required when --config is passed.", { code: ExitCodes.InvalidUsage });
116
+ return;
117
+ }
118
+ const config = parseJson(opts.config, "--config");
119
+ const result = await handleApiCall(() => api.connection.connector.create({
120
+ name,
121
+ slug,
122
+ integrationSlug: opts.integrationSlug,
123
+ config,
124
+ rateLimit,
125
+ cacheTtlMilliseconds,
126
+ }));
127
+ outputJson(result);
128
+ return;
129
+ }
130
+ const ignored = [
131
+ opts.name === undefined ? undefined : "--name",
132
+ opts.slug === undefined ? undefined : "--slug",
133
+ opts.rateLimit === undefined ? undefined : "--rate-limit",
134
+ opts.cacheTtlMilliseconds === undefined
135
+ ? undefined
136
+ : "--cache-ttl-milliseconds",
137
+ ].filter((flag) => flag !== undefined);
138
+ // The hosted form asks for all of these itself, so accepting them here
139
+ // and dropping them would silently produce a connector the caller did
140
+ // not describe.
141
+ if (ignored.length > 0) {
142
+ failWith(`${ignored.join(", ")} only applies with --config — the browser form asks for it.`, { code: ExitCodes.InvalidUsage });
143
+ }
144
+ await runCreateHandoff({ api, integrationSlug: opts.integrationSlug });
111
145
  });
112
146
  connector
113
147
  .command("update")
114
- .description("Update a connector")
148
+ .description("Update a connector. With no field flags, opens the hosted form in a browser (use it to re-authorize).")
115
149
  .requiredOption("--uuid <uuid>", "Connector UUID")
116
150
  .option("--name <name>", "Connector name")
117
151
  .option("--config <json>", "Configuration (JSON object)")
@@ -119,6 +153,19 @@ Examples:
119
153
  .option("--cache-ttl-milliseconds <n>", "Cache TTL in milliseconds")
120
154
  .action(async (opts) => {
121
155
  const api = getApi();
156
+ // No field to change means "let me edit it in the form" — the only way
157
+ // to re-run an OAuth flow for an existing connector.
158
+ const hasFieldFlag = opts.name !== undefined ||
159
+ opts.config !== undefined ||
160
+ opts.rateLimit !== undefined ||
161
+ opts.cacheTtlMilliseconds !== undefined;
162
+ if (hasFieldFlag === false) {
163
+ // Re-auth changes credentials, not the binding: the CDK file already
164
+ // exists and binds by slug, so there is nothing to write and nothing
165
+ // to wait for. Open the form and get out of the way.
166
+ openConnectorEditForm(opts.uuid);
167
+ return;
168
+ }
122
169
  const result = await handleApiCall(() => api.connection.connector.update({
123
170
  uuid: opts.uuid,
124
171
  name: opts.name,
@@ -144,3 +191,79 @@ Examples:
144
191
  outputJson(result);
145
192
  });
146
193
  }
194
+ /**
195
+ * Send the user to the connectors page, wait for them to confirm, then report
196
+ * the connector they made.
197
+ *
198
+ * Imperative only: it creates a connector and prints it. Writing
199
+ * `connectors/<slug>.ts` and binding cargo.state.json is
200
+ * `cargo-ai cdk add connector/<integration>`, so running this inside a repo
201
+ * never touches the repo.
202
+ */
203
+ async function runCreateHandoff(input) {
204
+ const { api, integrationSlug } = input;
205
+ const url = buildCliConnectorCreateUrl({
206
+ ...resolveAppContext(),
207
+ integrationSlug,
208
+ });
209
+ // Nothing can drive a browser for an agent: hand back the URL and where to
210
+ // look once a human has clicked through. `openPromptInput` still finds
211
+ // `/dev/tty` when stdin is piped; `shouldWaitForPerson` additionally
212
+ // refuses coding agents, whose stdin often looks like a TTY.
213
+ if (shouldWaitForPerson({
214
+ isCodingAgent: isCodingAgent(),
215
+ isCi: process.env["CI"] === "true",
216
+ hasPromptInput: openPromptInput() !== undefined,
217
+ }) === false) {
218
+ outputJson({
219
+ ok: true,
220
+ status: "actionRequired",
221
+ action: "openBrowser",
222
+ integrationSlug,
223
+ url,
224
+ reason: "Creating this connector needs the hosted form, which cannot be driven headlessly.",
225
+ next: `cargo-ai connection connector list --integration-slug ${integrationSlug}`,
226
+ });
227
+ return;
228
+ }
229
+ const [before, userUuid] = await Promise.all([
230
+ handleApiCall(() => api.connection.connector.list({ integrationSlug })),
231
+ resolveUserUuid(api),
232
+ ]);
233
+ const connector = await awaitBrowserStep({
234
+ api,
235
+ url,
236
+ integrationSlug,
237
+ before: before.connectors,
238
+ userUuid,
239
+ });
240
+ if (connector === undefined) {
241
+ return;
242
+ }
243
+ outputJson({ ok: true, status: "connected", connector });
244
+ }
245
+ /** Open this connector's edit form so the user can re-run its auth flow. */
246
+ function openConnectorEditForm(connectorUuid) {
247
+ const url = buildCliConnectorEditUrl({
248
+ ...resolveAppContext(),
249
+ connectorUuid,
250
+ });
251
+ info(["", "Re-authorize the connector here:", ` ${colors.cyan(url)}`, ""].join("\n"));
252
+ openBrowser(url);
253
+ outputJson({
254
+ ok: true,
255
+ status: "actionRequired",
256
+ action: "openBrowser",
257
+ url,
258
+ });
259
+ }
260
+ /** The workspace to act in; the app URL is configuration, not a guess. */
261
+ function resolveAppContext() {
262
+ const { appUrl, workspaceUuid } = getConfig();
263
+ if (workspaceUuid === undefined) {
264
+ return failWith("No workspace selected. Run 'cargo-ai login' first.", {
265
+ code: ExitCodes.NotAuthenticated,
266
+ });
267
+ }
268
+ return { appUrl, workspaceUuid };
269
+ }
@@ -0,0 +1,107 @@
1
+ import { CDK_SKILL, type CodingAgent } from "@cargo-ai/cdk/cli";
2
+ /**
3
+ * Where the agent is being opened, which decides what it is told to do.
4
+ *
5
+ * Three cases, because the right first move is different in each: outside a
6
+ * project there is nothing to read and the work starts with a conversation;
7
+ * inside a CDK repo the workspace is already declared in files worth reading
8
+ * before touching; and inside one carrying a cookbook, that cookbook's own
9
+ * procedure is the authority and re-deriving intent is the wrong move.
10
+ *
11
+ * Naming the same skill in all three, which is what this used to do, sent an
12
+ * agent into a repo full of answers and told it to start asking questions.
13
+ */
14
+ export type ProjectContext = {
15
+ kind: "none";
16
+ } | {
17
+ kind: "cdk";
18
+ root: string;
19
+ companyContextPath: string | undefined;
20
+ } | {
21
+ kind: "cookbook";
22
+ root: string;
23
+ companyContextPath: string | undefined;
24
+ cookbooks: readonly string[];
25
+ };
26
+ export type StartFacts = {
27
+ workspace: {
28
+ uuid: string;
29
+ name: string;
30
+ };
31
+ /** Whether the Cargo skills bundle is already installed for a coding agent. */
32
+ skillsInstalled: boolean;
33
+ };
34
+ /**
35
+ * The command group "Explore in the CLI" opens the palette on. The action
36
+ * catalog is the shortest path from a menu to something real: search one, run
37
+ * one, see a result.
38
+ */
39
+ export declare const EXPLORE_PATH: readonly string[];
40
+ /**
41
+ * The skill a handoff loads before the agent's first turn. `cargo` routes; the
42
+ * other two own their task outright.
43
+ *
44
+ * This type is the one place the whole story lives, because everything about
45
+ * the handoff depends on getting it right, and it has been got wrong twice.
46
+ *
47
+ * A skill reaches the agent by two paths at once: the launch passes it as a
48
+ * `/<skill>` command, which only Claude Code expands, and the brief names it in
49
+ * prose, which is all every other agent has. So the two must agree. The first
50
+ * version of the demo brief described its *outcome* instead of naming a skill
51
+ * and let the router choose; the router picked `cargo-gtm`, the general
52
+ * sourcing surface, and the agent spent four of its eight minutes rediscovering
53
+ * action names and dumping integration JSON to find a config schema. The
54
+ * second version named `cargo-quickstart` in the launch but left the brief
55
+ * saying "load `cargo`", which put every non-Claude agent back through the
56
+ * router and made the demo ask for two different skills in two steps.
57
+ *
58
+ * Hence a union rather than `string`: a mistyped or mismatched skill name is
59
+ * not something the CLI can notice at runtime, and the symptom is slow rather
60
+ * than broken, which is the kind of bug that survives.
61
+ */
62
+ export type HandoffSkill = "cargo" | typeof CDK_SKILL | "cargo-quickstart";
63
+ export declare const ROUTER_SKILL: HandoffSkill;
64
+ export declare const DEMO_SKILL: HandoffSkill;
65
+ export { CDK_SKILL };
66
+ export type Destination = {
67
+ kind: "agent";
68
+ agent: CodingAgent;
69
+ } | {
70
+ kind: "code";
71
+ } | {
72
+ kind: "cli";
73
+ } | {
74
+ kind: "demo";
75
+ } | {
76
+ kind: "none";
77
+ };
78
+ /**
79
+ * `--continue <where>` as a destination. Agents are named the way a person
80
+ * refers to them ("cursor"), not the way they are installed ("cursor-agent"),
81
+ * and one that is not on this machine is a usage error rather than a silent
82
+ * fallback to something else.
83
+ */
84
+ export declare function parseDestination(value: string, agents: readonly CodingAgent[]): Destination | undefined;
85
+ /**
86
+ * The brief a coding agent is opened with. An agent launched in a bare
87
+ * directory has to rediscover the workspace, the skills and the cost rules;
88
+ * this is the whole reason the handoff beats telling someone to open Claude
89
+ * Code themselves.
90
+ *
91
+ * What it says depends on `project` — see `ProjectContext` for why all three
92
+ * cases need different words rather than one brief and a different skill name.
93
+ */
94
+ export declare function buildBrief(facts: StartFacts, skill: HandoffSkill, project?: ProjectContext): string;
95
+ /**
96
+ * The brief behind the guided demo.
97
+ *
98
+ * It names `cargo-quickstart` rather than describing the demo, because that
99
+ * skill *is* this demo — a persona, ~25 leads, a cost receipt, then saving the
100
+ * pull as a play. Describing the outcome instead is what cost the first version
101
+ * half its runtime (see `HandoffSkill`), so this brief spends its length on
102
+ * pointing at the procedure and forbidding the detour.
103
+ */
104
+ export declare function buildDemoBrief(facts: StartFacts, icp: string | undefined): string;
105
+ /** The block printed when nobody wanted a handoff. */
106
+ export declare function formatStartNextSteps(facts: StartFacts): string;
107
+ //# sourceMappingURL=brief.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brief.d.ts","sourceRoot":"","sources":["../../../src/commands/start/brief.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,SAAS,EACT,KAAK,WAAW,EAEjB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACrE;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B,CAAC;AAEN,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,+EAA+E;IAC/E,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAgC,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,SAAS,GAAG,kBAAkB,CAAC;AAE3E,eAAO,MAAM,YAAY,EAAE,YAAsB,CAAC;AAClD,eAAO,MAAM,UAAU,EAAE,YAAiC,CAAC;AAE3D,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,SAAS,WAAW,EAAE,GAC7B,WAAW,GAAG,SAAS,CA8BzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE,cAAiC,GACzC,MAAM,CAaR;AAyED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,MAAM,CA+BR;AAED,sDAAsD;AACtD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAgB9D"}