@cargo-ai/cli 1.0.71 → 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.
Files changed (34) hide show
  1. package/build/commands/auth/email.d.ts +24 -0
  2. package/build/commands/auth/email.d.ts.map +1 -1
  3. package/build/commands/auth/email.js +27 -14
  4. package/build/commands/auth/index.d.ts.map +1 -1
  5. package/build/commands/auth/index.js +7 -1
  6. package/build/commands/auth/workspace.d.ts.map +1 -1
  7. package/build/commands/auth/workspace.js +99 -26
  8. package/build/commands/connection/connector.d.ts.map +1 -1
  9. package/build/commands/connection/connector.js +144 -21
  10. package/build/commands/orchestration/action.d.ts.map +1 -1
  11. package/build/commands/orchestration/action.js +68 -3
  12. package/build/commands/orchestration/costTable.d.ts +121 -0
  13. package/build/commands/orchestration/costTable.d.ts.map +1 -0
  14. package/build/commands/orchestration/costTable.js +363 -0
  15. package/build/commands/start/brief.d.ts +107 -0
  16. package/build/commands/start/brief.d.ts.map +1 -0
  17. package/build/commands/start/brief.js +222 -0
  18. package/build/commands/start/index.d.ts +44 -0
  19. package/build/commands/start/index.d.ts.map +1 -0
  20. package/build/commands/start/index.js +431 -0
  21. package/build/commands/start/state.d.ts +50 -0
  22. package/build/commands/start/state.d.ts.map +1 -0
  23. package/build/commands/start/state.js +101 -0
  24. package/build/config.d.ts +1 -0
  25. package/build/config.d.ts.map +1 -1
  26. package/build/config.js +1 -0
  27. package/build/index.js +50 -6
  28. package/build/paletteLayout.d.ts +16 -0
  29. package/build/paletteLayout.d.ts.map +1 -0
  30. package/build/paletteLayout.js +131 -0
  31. package/build/setupIssues.d.ts +4 -16
  32. package/build/setupIssues.d.ts.map +1 -1
  33. package/build/setupIssues.js +3 -52
  34. package/package.json +3 -3
@@ -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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../../src/commands/orchestration/action.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAcxC,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAuRN"}
1
+ {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../../src/commands/orchestration/action.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAqBxC,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAuSN"}
@@ -1,5 +1,6 @@
1
1
  import { parseBooleanOption } from "../../utils/booleanOption.js";
2
2
  import { applyRecordsLimit, ExitCodes, failWith, handleApiCall, outputJson, parseJson, parsePositiveInt, pollBatchUntilFinished, pollRunUntilFinished, } from "../runHandler.js";
3
+ import { buildCostRows, groupActionsByIntegration, renderCostTable, summarize, } from "./costTable.js";
3
4
  export function registerActionCommands(parent, getApi) {
4
5
  const action = parent
5
6
  .command("action")
@@ -17,6 +18,7 @@ graph needed. Creates a run and consumes credits.
17
18
 
18
19
  To debug a node inside an existing workflow instead, use
19
20
  'cargo-ai orchestration node'.`);
21
+ registerCostTableCommand(action, getApi);
20
22
  const listableKinds = ["connector", "native", "tool", "agent"];
21
23
  action
22
24
  .command("list")
@@ -24,7 +26,8 @@ To debug a node inside an existing workflow instead, use
24
26
  .argument("[query...]", "Keywords to match (space-separated, optional)")
25
27
  .option("--kind <kind>", "Restrict to one kind: connector, native, tool, or agent (enum)")
26
28
  .option("--integration-slug <slug>", "Restrict connector actions to this integration (string)")
27
- .option("--limit <n>", "Maximum results to return (integer, default: 20, max: 50)", "20")
29
+ .option("--category <category>", "Restrict connector actions to integrations in this category (e.g. enrichment, sales, freeform)")
30
+ .option("--limit <n>", "Maximum results to return (integer). Omit to return all results.")
28
31
  .addHelpText("after", `
29
32
  Each result includes an 'action' object you can pass to execute, execute-batch,
30
33
  or get-output-schema. All query terms must match (AND). Matches on the action
@@ -33,7 +36,9 @@ slug or name rank above integration matches, which rank above description.
33
36
  Examples:
34
37
  $ cargo-ai orchestration action list enrich
35
38
  $ cargo-ai orchestration action list --kind tool
36
- $ cargo-ai orchestration action list send --kind connector --integration-slug slack`)
39
+ $ cargo-ai orchestration action list send --kind connector --integration-slug slack
40
+ $ cargo-ai orchestration action list --category enrichment
41
+ $ cargo-ai orchestration action list --limit 10`)
37
42
  .action(async (queryTerms, opts) => {
38
43
  let kind;
39
44
  if (opts.kind !== undefined) {
@@ -42,12 +47,15 @@ Examples:
42
47
  failWith('--kind must be "connector", "native", "tool", or "agent".', { code: ExitCodes.InvalidUsage });
43
48
  }
44
49
  }
45
- const limit = parsePositiveInt(opts.limit, "--limit");
50
+ const limit = opts.limit === undefined
51
+ ? undefined
52
+ : parsePositiveInt(opts.limit, "--limit");
46
53
  const query = queryTerms.length === 0 ? undefined : queryTerms.join(" ");
47
54
  const result = await handleApiCall(() => getApi().orchestration.action.list({
48
55
  query,
49
56
  kind,
50
57
  integrationSlug: opts.integrationSlug,
58
+ category: opts.category,
51
59
  limit,
52
60
  }));
53
61
  outputJson(result);
@@ -152,3 +160,60 @@ Examples:
152
160
  outputJson(result);
153
161
  });
154
162
  }
163
+ function registerCostTableCommand(action, getApi) {
164
+ action
165
+ .command("cost-table")
166
+ .description("Price list of every credits-based action in the catalog, sorted by cost (free, reads the catalog)")
167
+ .option("--category <category>", "Filter by integration category (string)")
168
+ .option("--integration <slug>", "Filter by integration slug (string)")
169
+ .option("--raw", "Print the markdown document instead of JSON")
170
+ .addHelpText("after", `
171
+ Free, spends nothing: the catalog is where pricing actually lives, so this is
172
+ the answer to "what does <action> cost" and "what is the cheapest way to do
173
+ <stage>" without pricing anyone copied by hand.
174
+
175
+ Covers connector actions and Cargo's own native actions: sendEmail, modelAsk
176
+ and fileSearch are billed, so a connector-only price list is not a complete one.
177
+ Filtering by --category or --integration drops the natives.
178
+
179
+ Actions absent from the output cost no credits. Actions whose price depends on
180
+ config (an LLM model, a phone-or-email toggle) show a range, with the
181
+ per-config prices in the document's second half.
182
+
183
+ Examples:
184
+ $ cargo-ai orchestration action cost-table
185
+ $ cargo-ai orchestration action cost-table --category enrichment --raw
186
+ $ cargo-ai orchestration action cost-table --raw > credits-cost-table.md`)
187
+ .action(async (opts) => {
188
+ const api = getApi();
189
+ const isFiltered = opts.category !== undefined || opts.integration !== undefined;
190
+ // Fetch all connector and native actions. Omitting limit returns all.
191
+ // When filtering by category or integration, only connectors match —
192
+ // natives have no category and no integrationSlug.
193
+ const kinds = isFiltered
194
+ ? ["connector"]
195
+ : ["connector", "native"];
196
+ const allActions = [];
197
+ for (const kind of kinds) {
198
+ const result = await handleApiCall(() => api.orchestration.action.list({
199
+ kind,
200
+ category: opts.category,
201
+ integrationSlug: opts.integration,
202
+ }));
203
+ allActions.push(...result.results);
204
+ }
205
+ const catalog = groupActionsByIntegration(allActions);
206
+ const rows = buildCostRows(catalog);
207
+ const summary = summarize(catalog);
208
+ if (opts.raw === true) {
209
+ console.log(renderCostTable(rows, summary, {
210
+ generatedAt: new Date().toISOString().slice(0, 10),
211
+ filter: isFiltered
212
+ ? { category: opts.category, integration: opts.integration }
213
+ : undefined,
214
+ }));
215
+ return;
216
+ }
217
+ outputJson({ summary, rows });
218
+ });
219
+ }