@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.
- package/build/commands/auth/email.d.ts +24 -0
- package/build/commands/auth/email.d.ts.map +1 -1
- package/build/commands/auth/email.js +27 -14
- package/build/commands/auth/index.d.ts.map +1 -1
- package/build/commands/auth/index.js +7 -1
- package/build/commands/auth/workspace.d.ts.map +1 -1
- package/build/commands/auth/workspace.js +99 -26
- package/build/commands/connection/connector.d.ts.map +1 -1
- package/build/commands/connection/connector.js +144 -21
- package/build/commands/orchestration/action.d.ts.map +1 -1
- package/build/commands/orchestration/action.js +68 -3
- package/build/commands/orchestration/costTable.d.ts +121 -0
- package/build/commands/orchestration/costTable.d.ts.map +1 -0
- package/build/commands/orchestration/costTable.js +363 -0
- package/build/commands/start/brief.d.ts +107 -0
- package/build/commands/start/brief.d.ts.map +1 -0
- package/build/commands/start/brief.js +222 -0
- package/build/commands/start/index.d.ts +44 -0
- package/build/commands/start/index.d.ts.map +1 -0
- package/build/commands/start/index.js +431 -0
- package/build/commands/start/state.d.ts +50 -0
- package/build/commands/start/state.d.ts.map +1 -0
- package/build/commands/start/state.js +101 -0
- package/build/config.d.ts +1 -0
- package/build/config.d.ts.map +1 -1
- package/build/config.js +1 -0
- package/build/index.js +50 -6
- package/build/paletteLayout.d.ts +16 -0
- package/build/paletteLayout.d.ts.map +1 -0
- package/build/paletteLayout.js +131 -0
- package/build/setupIssues.d.ts +4 -16
- package/build/setupIssues.d.ts.map +1 -1
- package/build/setupIssues.js +3 -52
- 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":"
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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;
|
|
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":"
|
|
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 {
|
|
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
|
-
.
|
|
84
|
-
.
|
|
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
|
-
|
|
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
|
-
--
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
108
|
+
: undefined;
|
|
109
|
+
const cacheTtlMilliseconds = opts.cacheTtlMilliseconds !== undefined
|
|
107
110
|
? parseInt(opts.cacheTtlMilliseconds, 10)
|
|
108
|
-
: undefined
|
|
109
|
-
|
|
110
|
-
|
|
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;
|
|
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("--
|
|
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 =
|
|
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
|
+
}
|