@cargo-ai/cli 1.0.53 → 1.0.55
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/agentSkills.d.ts +34 -0
- package/build/agentSkills.d.ts.map +1 -0
- package/build/agentSkills.js +37 -0
- package/build/commands/auth/email.d.ts.map +1 -1
- package/build/commands/auth/email.js +2 -0
- package/build/commands/auth/index.d.ts.map +1 -1
- package/build/commands/auth/index.js +3 -0
- package/build/commands/orchestration/node.d.ts.map +1 -1
- package/build/commands/orchestration/node.js +145 -6
- package/build/commands/orchestration/nodeDiagram.d.ts +45 -0
- package/build/commands/orchestration/nodeDiagram.d.ts.map +1 -0
- package/build/commands/orchestration/nodeDiagram.js +373 -0
- package/build/index.js +4 -6
- package/package.json +2 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this CLI tells a coding agent that the skill bundles exist.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS ITS OWN MODULE. The pointer used to live as a const inside
|
|
5
|
+
* `index.ts`, reachable only from `--help` and from the footer of an
|
|
6
|
+
* unknown-subcommand error. Both are paths an agent takes while it is still
|
|
7
|
+
* *exploring*. The moment someone has decided to use Cargo, and the only moment
|
|
8
|
+
* we know for certain a human or agent is watching, is the line right after
|
|
9
|
+
* sign-in succeeds, and that line said nothing.
|
|
10
|
+
*
|
|
11
|
+
* That gap is measurable and it is large: `@cargo-ai/cli` runs at roughly 1,900
|
|
12
|
+
* npm downloads a week while `getcargohq/cargo-skills` gains on the order of 35
|
|
13
|
+
* installs a week. People are already running this binary and almost none of
|
|
14
|
+
* them install the skills that make it usable by an agent.
|
|
15
|
+
*
|
|
16
|
+
* Keep the strings here rather than at each call site, so a change to how the
|
|
17
|
+
* bundles are named or installed cannot land in `--help` and miss sign-in.
|
|
18
|
+
*/
|
|
19
|
+
/** One line, for a footer where vertical space is somebody else's error. */
|
|
20
|
+
export declare const AGENT_SKILLS_HINT = "Coding agents: install the Cargo skills with `npx skills add getcargohq/cargo-skills`";
|
|
21
|
+
/**
|
|
22
|
+
* Printed after a successful sign-in.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately two lines and no more. A sign-in banner that scrolls is a banner
|
|
25
|
+
* people configure away, and this fires at most a handful of times per machine
|
|
26
|
+
* because signing in is rare, so it does not need a suppression flag to be
|
|
27
|
+
* polite.
|
|
28
|
+
*
|
|
29
|
+
* No skill count. The help epilogue carried "sixteen skills" while the bundle
|
|
30
|
+
* held seventeen: a hand-maintained number in prose goes stale the next time
|
|
31
|
+
* somebody adds one, and nothing in CI was ever going to catch it here.
|
|
32
|
+
*/
|
|
33
|
+
export declare const AGENT_SKILLS_NEXT_STEP: string;
|
|
34
|
+
//# sourceMappingURL=agentSkills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentSkills.d.ts","sourceRoot":"","sources":["../src/agentSkills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,4EAA4E;AAC5E,eAAO,MAAM,iBAAiB,0FAC2D,CAAC;AAE1F;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,QAIvB,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this CLI tells a coding agent that the skill bundles exist.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS ITS OWN MODULE. The pointer used to live as a const inside
|
|
5
|
+
* `index.ts`, reachable only from `--help` and from the footer of an
|
|
6
|
+
* unknown-subcommand error. Both are paths an agent takes while it is still
|
|
7
|
+
* *exploring*. The moment someone has decided to use Cargo, and the only moment
|
|
8
|
+
* we know for certain a human or agent is watching, is the line right after
|
|
9
|
+
* sign-in succeeds, and that line said nothing.
|
|
10
|
+
*
|
|
11
|
+
* That gap is measurable and it is large: `@cargo-ai/cli` runs at roughly 1,900
|
|
12
|
+
* npm downloads a week while `getcargohq/cargo-skills` gains on the order of 35
|
|
13
|
+
* installs a week. People are already running this binary and almost none of
|
|
14
|
+
* them install the skills that make it usable by an agent.
|
|
15
|
+
*
|
|
16
|
+
* Keep the strings here rather than at each call site, so a change to how the
|
|
17
|
+
* bundles are named or installed cannot land in `--help` and miss sign-in.
|
|
18
|
+
*/
|
|
19
|
+
/** One line, for a footer where vertical space is somebody else's error. */
|
|
20
|
+
export const AGENT_SKILLS_HINT = "Coding agents: install the Cargo skills with `npx skills add getcargohq/cargo-skills`";
|
|
21
|
+
/**
|
|
22
|
+
* Printed after a successful sign-in.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately two lines and no more. A sign-in banner that scrolls is a banner
|
|
25
|
+
* people configure away, and this fires at most a handful of times per machine
|
|
26
|
+
* because signing in is rare, so it does not need a suppression flag to be
|
|
27
|
+
* polite.
|
|
28
|
+
*
|
|
29
|
+
* No skill count. The help epilogue carried "sixteen skills" while the bundle
|
|
30
|
+
* held seventeen: a hand-maintained number in prose goes stale the next time
|
|
31
|
+
* somebody adds one, and nothing in CI was ever going to catch it here.
|
|
32
|
+
*/
|
|
33
|
+
export const AGENT_SKILLS_NEXT_STEP = [
|
|
34
|
+
"Next: teach your coding agent this CLI.",
|
|
35
|
+
" npx skills add getcargohq/cargo-skills every command, as agent skills",
|
|
36
|
+
" npx skills add getcargohq/gtm-skills standalone one-job GTM skills",
|
|
37
|
+
].join("\n");
|
|
@@ -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":"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,3 +1,4 @@
|
|
|
1
|
+
import { AGENT_SKILLS_NEXT_STEP } from "../../agentSkills.js";
|
|
1
2
|
import { getCredentialsPath } from "../../credentials.js";
|
|
2
3
|
import { askQuestion, openPromptInput } from "../../utils/prompt.js";
|
|
3
4
|
import { ExitCodes, failWith, info, outputJson, startSpinner, success, } from "../runHandler.js";
|
|
@@ -38,6 +39,7 @@ export async function runEmailLogin(opts) {
|
|
|
38
39
|
workspaceName: opts.workspaceName,
|
|
39
40
|
});
|
|
40
41
|
success(`Signed in as ${email}. Workspace: ${workspace.name}.`);
|
|
42
|
+
info(AGENT_SKILLS_NEXT_STEP);
|
|
41
43
|
outputJson({
|
|
42
44
|
ok: true,
|
|
43
45
|
status: "signedIn",
|
|
@@ -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;
|
|
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;AAwBxD,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,CAkGN;AAmFD,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,5 +1,6 @@
|
|
|
1
1
|
import { revokeSession } from "@cargo-ai/cdk/cli";
|
|
2
2
|
import { Option } from "commander";
|
|
3
|
+
import { AGENT_SKILLS_NEXT_STEP } from "../../agentSkills.js";
|
|
3
4
|
import { getConfig } from "../../config.js";
|
|
4
5
|
import { clearCredentials, getCredentialsPath, loadCredentials, saveCredentials, } from "../../credentials.js";
|
|
5
6
|
import { ExitCodes, failWith, handleApiCall, info, outputJson, success, } from "../runHandler.js";
|
|
@@ -89,6 +90,7 @@ async function runLogin(opts) {
|
|
|
89
90
|
baseUrl: opts.baseUrl,
|
|
90
91
|
});
|
|
91
92
|
success("Signed in with API token.");
|
|
93
|
+
info(AGENT_SKILLS_NEXT_STEP);
|
|
92
94
|
outputJson({
|
|
93
95
|
ok: true,
|
|
94
96
|
status: "signedIn",
|
|
@@ -107,6 +109,7 @@ async function runLogin(opts) {
|
|
|
107
109
|
workspaceName: opts.workspaceName,
|
|
108
110
|
});
|
|
109
111
|
success(`Signed in. Default workspace: ${workspace.name}.`);
|
|
112
|
+
info(AGENT_SKILLS_NEXT_STEP);
|
|
110
113
|
outputJson({
|
|
111
114
|
ok: true,
|
|
112
115
|
status: "signedIn",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../../src/commands/orchestration/node.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../../src/commands/orchestration/node.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAexC,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,IAAI,CAoK7E"}
|
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { ExitCodes, failWith, handleApiCall, outputJson, parseJson, } from "../runHandler.js";
|
|
3
|
+
import { extractNodes, renderNodeDiagram, toMermaidBlock, } from "./nodeDiagram.js";
|
|
2
4
|
export function registerNodeCommands(parent, getApi) {
|
|
3
5
|
const node = parent
|
|
4
6
|
.command("node")
|
|
5
|
-
.description("
|
|
7
|
+
.description("Work with a workflow's node graph: draw it, validate it, compute a node's config, execute a single node")
|
|
6
8
|
.addHelpText("after", `
|
|
7
|
-
Use these commands to debug
|
|
9
|
+
Use these commands to inspect or debug the node graph of a workflow. To just
|
|
8
10
|
run a tool or connector action, no workflow needed, use instead:
|
|
9
11
|
$ cargo-ai orchestration action execute --action '<json>' --data '<json>'
|
|
10
12
|
|
|
11
|
-
'compute' and 'validate' are free and run nothing. 'execute' is not
|
|
12
|
-
it performs the node's real action and consumes credits.
|
|
13
|
+
'diagram', 'compute' and 'validate' are free and run nothing. 'execute' is not
|
|
14
|
+
a dry run: it performs the node's real action and consumes credits.
|
|
15
|
+
|
|
16
|
+
Before deploying a graph:
|
|
17
|
+
$ cargo-ai orchestration node validate --nodes '<json>'
|
|
18
|
+
$ cargo-ai orchestration node diagram --nodes '<json>' --raw
|
|
13
19
|
|
|
14
20
|
Debugging loop:
|
|
15
21
|
$ cargo-ai orchestration span list --run-uuid <uuid> --execution-started-after <iso-date>
|
|
16
|
-
$ cargo-ai orchestration
|
|
22
|
+
$ cargo-ai orchestration node diagram --run-uuid <uuid> --highlight <slug> --raw
|
|
17
23
|
$ cargo-ai orchestration node compute --node '<json>' --context '<json>'
|
|
18
24
|
$ cargo-ai orchestration node execute --workflow-uuid <uuid> --release-uuid <uuid> --node '<json>' --computed-config '<json>' --context '<json>'`);
|
|
25
|
+
registerDiagramCommand(node, getApi);
|
|
19
26
|
node
|
|
20
27
|
.command("compute")
|
|
21
28
|
.description("Compute a node's configuration from its definition and context")
|
|
@@ -95,3 +102,135 @@ Examples:
|
|
|
95
102
|
outputJson(result);
|
|
96
103
|
});
|
|
97
104
|
}
|
|
105
|
+
function registerDiagramCommand(node, getApi) {
|
|
106
|
+
node
|
|
107
|
+
.command("diagram")
|
|
108
|
+
.description("Draw a node graph as a Mermaid flowchart (free, runs nothing, no credits)")
|
|
109
|
+
.option("--nodes <json>", "Node definitions (JSON array, or '-' to read from stdin)")
|
|
110
|
+
.option("--file <path>", "Read a JSON payload or node array from a file")
|
|
111
|
+
.option("--workflow-uuid <uuid>", "Diagram a workflow's deployed release (add --draft for its draft)")
|
|
112
|
+
.option("--draft", "With --workflow-uuid, use the draft release")
|
|
113
|
+
.option("--release-uuid <uuid>", "Diagram a specific release")
|
|
114
|
+
.option("--run-uuid <uuid>", "Diagram the graph a run executed (follows the run's release when it has one)")
|
|
115
|
+
.option("--title <text>", "Title rendered above the diagram")
|
|
116
|
+
.option("--direction <TD|LR>", "Flow direction (default: TD)", "TD")
|
|
117
|
+
.option("--paid <slugs>", "Comma-separated node slugs/uuids that bill credits — marked 💳")
|
|
118
|
+
.option("--highlight <slugs>", "Comma-separated node slugs/uuids to mark red, e.g. a failing node")
|
|
119
|
+
.option("--raw", "Print the fenced Mermaid block instead of JSON")
|
|
120
|
+
.addHelpText("after", `
|
|
121
|
+
Free, runs nothing: turns a node graph into a picture so a reviewer can see the
|
|
122
|
+
routing, the fallback paths and which steps bill before approving or deploying
|
|
123
|
+
it. Pass exactly one source.
|
|
124
|
+
|
|
125
|
+
Returns {"diagram":"flowchart TD…","format":"mermaid","warnings":[…]}, or the
|
|
126
|
+
fenced block itself with --raw. 'warnings' reports structural problems worth
|
|
127
|
+
saying out loud — nodes unreachable from start, dangling childrenUuids.
|
|
128
|
+
|
|
129
|
+
A run created by 'action execute' carries its own graph; a run of a deployed
|
|
130
|
+
tool or play carries only a releaseUuid, and --run-uuid follows it either way.
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
$ cargo-ai orchestration node diagram --workflow-uuid <uuid> --raw
|
|
134
|
+
$ cargo-ai orchestration node diagram --workflow-uuid <uuid> --draft --paid enrich,verify
|
|
135
|
+
$ cargo-ai orchestration node diagram --run-uuid <uuid> --highlight branch_1 --raw
|
|
136
|
+
$ cargo-ai orchestration release get-deployed --workflow-uuid <uuid> | cargo-ai orchestration node diagram --nodes -`)
|
|
137
|
+
.action(async (opts) => {
|
|
138
|
+
const nodes = await resolveNodes(getApi, opts);
|
|
139
|
+
const result = renderNodeDiagram(nodes, {
|
|
140
|
+
title: opts.title,
|
|
141
|
+
direction: (opts.direction ?? "TD").toUpperCase(),
|
|
142
|
+
paid: splitList(opts.paid),
|
|
143
|
+
highlight: splitList(opts.highlight),
|
|
144
|
+
});
|
|
145
|
+
if (opts.raw === true) {
|
|
146
|
+
console.log(toMermaidBlock(result.diagram));
|
|
147
|
+
for (const warning of result.warnings)
|
|
148
|
+
console.error(`⚠ ${warning}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
outputJson(result);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function splitList(value) {
|
|
155
|
+
if (value === undefined)
|
|
156
|
+
return [];
|
|
157
|
+
return value
|
|
158
|
+
.split(",")
|
|
159
|
+
.map((entry) => entry.trim())
|
|
160
|
+
.filter((entry) => entry.length > 0);
|
|
161
|
+
}
|
|
162
|
+
async function resolveNodes(getApi, opts) {
|
|
163
|
+
const sources = [
|
|
164
|
+
opts.nodes !== undefined ? "--nodes" : undefined,
|
|
165
|
+
opts.file !== undefined ? "--file" : undefined,
|
|
166
|
+
opts.workflowUuid !== undefined ? "--workflow-uuid" : undefined,
|
|
167
|
+
opts.releaseUuid !== undefined ? "--release-uuid" : undefined,
|
|
168
|
+
opts.runUuid !== undefined ? "--run-uuid" : undefined,
|
|
169
|
+
].filter((source) => source !== undefined);
|
|
170
|
+
if (sources.length === 0) {
|
|
171
|
+
failWith("pass one of --nodes, --file, --workflow-uuid, --release-uuid or --run-uuid", { code: ExitCodes.InvalidUsage });
|
|
172
|
+
}
|
|
173
|
+
if (sources.length > 1) {
|
|
174
|
+
failWith(`${sources.join(" and ")} are mutually exclusive`, {
|
|
175
|
+
code: ExitCodes.InvalidUsage,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (opts.nodes !== undefined) {
|
|
179
|
+
const raw = opts.nodes === "-" ? await readStdin() : opts.nodes;
|
|
180
|
+
return fromPayload(parseJson(raw, "--nodes"), "--nodes");
|
|
181
|
+
}
|
|
182
|
+
if (opts.file !== undefined) {
|
|
183
|
+
return fromPayload(parseJson(readFileSync(opts.file, "utf8"), "--file"), "--file");
|
|
184
|
+
}
|
|
185
|
+
const api = getApi();
|
|
186
|
+
if (opts.workflowUuid !== undefined) {
|
|
187
|
+
const workflowUuid = opts.workflowUuid;
|
|
188
|
+
const result = await handleApiCall(() => opts.draft === true
|
|
189
|
+
? api.orchestration.draftRelease.get({ workflowUuid })
|
|
190
|
+
: api.orchestration.release.getDeployed({ workflowUuid }));
|
|
191
|
+
return fromPayload(result, "--workflow-uuid");
|
|
192
|
+
}
|
|
193
|
+
if (opts.releaseUuid !== undefined) {
|
|
194
|
+
const releaseUuid = opts.releaseUuid;
|
|
195
|
+
const result = await handleApiCall(() => api.orchestration.release.get(releaseUuid));
|
|
196
|
+
return fromPayload(result, "--release-uuid");
|
|
197
|
+
}
|
|
198
|
+
// A run either carries its own graph (`action execute`, `run create --nodes`)
|
|
199
|
+
// or points at the release that holds one — never both. Follow whichever it
|
|
200
|
+
// has, so the caller does not have to know which kind of run they were given.
|
|
201
|
+
const runUuid = opts.runUuid;
|
|
202
|
+
if (runUuid === undefined) {
|
|
203
|
+
failWith("no graph source resolved", { code: ExitCodes.InvalidUsage });
|
|
204
|
+
}
|
|
205
|
+
const run = await handleApiCall(() => api.orchestration.run.get(runUuid));
|
|
206
|
+
const inline = extractNodes(run);
|
|
207
|
+
if (inline !== undefined)
|
|
208
|
+
return inline;
|
|
209
|
+
const releaseUuid = run.run
|
|
210
|
+
?.releaseUuid;
|
|
211
|
+
if (releaseUuid === null || releaseUuid === undefined) {
|
|
212
|
+
failWith(`run ${runUuid} carries neither a node graph nor a release`, {
|
|
213
|
+
code: ExitCodes.NotFound,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const release = await handleApiCall(() => api.orchestration.release.get(releaseUuid));
|
|
217
|
+
return fromPayload(release, "--run-uuid");
|
|
218
|
+
}
|
|
219
|
+
function fromPayload(payload, source) {
|
|
220
|
+
const nodes = extractNodes(payload);
|
|
221
|
+
if (nodes === undefined) {
|
|
222
|
+
failWith(`no node graph found in the payload passed to ${source} — expected a node array, or the output of 'release get', 'release get-draft', 'template get' or 'run get'`, { code: ExitCodes.InvalidUsage });
|
|
223
|
+
}
|
|
224
|
+
return nodes;
|
|
225
|
+
}
|
|
226
|
+
async function readStdin() {
|
|
227
|
+
if (process.stdin.isTTY === true) {
|
|
228
|
+
failWith("--nodes - expects JSON on stdin", {
|
|
229
|
+
code: ExitCodes.InvalidUsage,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const chunks = [];
|
|
233
|
+
for await (const chunk of process.stdin)
|
|
234
|
+
chunks.push(chunk);
|
|
235
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
236
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Structural shape of a node — deliberately loose, so this module stays
|
|
2
|
+
* decoupled from the generated API types and can also render a hand-written
|
|
3
|
+
* graph that has not been deployed yet. */
|
|
4
|
+
export type DiagramNode = {
|
|
5
|
+
uuid: string;
|
|
6
|
+
slug?: string | null;
|
|
7
|
+
name?: string | null;
|
|
8
|
+
kind?: string | null;
|
|
9
|
+
actionSlug?: string | null;
|
|
10
|
+
integrationSlug?: string | null;
|
|
11
|
+
toolUuid?: string | null;
|
|
12
|
+
agentUuid?: string | null;
|
|
13
|
+
/** Template slug for tool/agent nodes sourced from a template graph. */
|
|
14
|
+
templateSlug?: string | null;
|
|
15
|
+
childrenUuids?: (string | null)[] | null;
|
|
16
|
+
fallbackChildUuid?: string | null;
|
|
17
|
+
config?: Record<string, unknown> | null;
|
|
18
|
+
};
|
|
19
|
+
export type DiagramOptions = {
|
|
20
|
+
title?: string;
|
|
21
|
+
direction?: string;
|
|
22
|
+
/** Node slugs or uuids that bill credits — rendered with a 💳. */
|
|
23
|
+
paid?: string[];
|
|
24
|
+
/** Node slugs or uuids to mark red, e.g. the failing node in a trace. */
|
|
25
|
+
highlight?: string[];
|
|
26
|
+
};
|
|
27
|
+
export type DiagramResult = {
|
|
28
|
+
diagram: string;
|
|
29
|
+
format: "mermaid";
|
|
30
|
+
/** Structural problems worth saying out loud rather than drawing over. */
|
|
31
|
+
warnings: string[];
|
|
32
|
+
};
|
|
33
|
+
/** What the node is, in words a user recognises. */
|
|
34
|
+
export declare function labelFor(node: DiagramNode): string;
|
|
35
|
+
export declare function renderNodeDiagram(nodes: DiagramNode[], options?: DiagramOptions): DiagramResult;
|
|
36
|
+
/** Node-shaped means: a uuid plus the fields a graph is walked by. */
|
|
37
|
+
export declare function isNodeArray(value: unknown): value is DiagramNode[];
|
|
38
|
+
/**
|
|
39
|
+
* Pull the graph out of a CLI payload: `release get`, `release get-draft`,
|
|
40
|
+
* `template get`, an ad-hoc `run get`, or a bare node array.
|
|
41
|
+
*/
|
|
42
|
+
export declare function extractNodes(payload: unknown): DiagramNode[] | undefined;
|
|
43
|
+
/** The fenced block, ready to paste into a message, a PR, or the docs. */
|
|
44
|
+
export declare function toMermaidBlock(diagram: string): string;
|
|
45
|
+
//# sourceMappingURL=nodeDiagram.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nodeDiagram.d.ts","sourceRoot":"","sources":["../../../src/commands/orchestration/nodeDiagram.ts"],"names":[],"mappings":"AAgBA;;2CAE2C;AAC3C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;IACzC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAsHF,oDAAoD;AACpD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CASlD;AAyND,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAkCf;AAMD,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,EAAE,CAYlE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,EAAE,GAAG,SAAS,CAexE;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD"}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// Render a workflow node graph as a Mermaid flowchart.
|
|
2
|
+
//
|
|
3
|
+
// A node graph carries routing, fallback paths and paid steps; prose flattens
|
|
4
|
+
// all three, so "what does this play do?" is best answered with a picture. Every
|
|
5
|
+
// surface the CLI prints into — coding agents, GitHub, the docs — renders
|
|
6
|
+
// Mermaid, and this is a pure local transform: no API call, no credits.
|
|
7
|
+
//
|
|
8
|
+
// Two properties of real releases drive the implementation:
|
|
9
|
+
//
|
|
10
|
+
// * `slug` is NOT unique within a release. A shipped waterfall carries six
|
|
11
|
+
// nodes slugged `variables`. Everything here keys on `uuid`; slugs are used
|
|
12
|
+
// for labels and for matching user-supplied --paid / --highlight only.
|
|
13
|
+
// * `childrenUuids` order carries the routing semantics (index 0 of a
|
|
14
|
+
// `branch` is the matched path), so edge labels are table-driven rather
|
|
15
|
+
// than inferred from node names.
|
|
16
|
+
const ROUTING_ACTIONS = [
|
|
17
|
+
"branch",
|
|
18
|
+
"filter",
|
|
19
|
+
"switch",
|
|
20
|
+
"split",
|
|
21
|
+
"balance",
|
|
22
|
+
"humanReview",
|
|
23
|
+
];
|
|
24
|
+
const CODE_ACTIONS = ["python", "script"];
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Shapes and labels
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
function shapeFor(node) {
|
|
29
|
+
const action = node.actionSlug ?? "";
|
|
30
|
+
if (node.kind === "native") {
|
|
31
|
+
if (action === "start" || action === "end")
|
|
32
|
+
return ["([", "])"];
|
|
33
|
+
if (ROUTING_ACTIONS.includes(action))
|
|
34
|
+
return ["{", "}"];
|
|
35
|
+
if (CODE_ACTIONS.includes(action))
|
|
36
|
+
return ["[/", "/]"];
|
|
37
|
+
if (action === "agent")
|
|
38
|
+
return ["{{", "}}"];
|
|
39
|
+
if (action === "group")
|
|
40
|
+
return ["[", "]"];
|
|
41
|
+
return ["(", ")"];
|
|
42
|
+
}
|
|
43
|
+
if (node.kind === "tool")
|
|
44
|
+
return ["[[", "]]"];
|
|
45
|
+
if (node.kind === "agent")
|
|
46
|
+
return ["{{", "}}"];
|
|
47
|
+
return ["[", "]"]; // connector, and anything unrecognised
|
|
48
|
+
}
|
|
49
|
+
// Labels are emitted quoted, so brackets and parens inside them are safe; a raw
|
|
50
|
+
// quote, angle bracket, pipe or newline is not — those end the label early or
|
|
51
|
+
// inject markup into Mermaid's HTML labels. Pipe also breaks edge labels
|
|
52
|
+
// (|…|), so it must be escaped everywhere.
|
|
53
|
+
function escapeLabel(text) {
|
|
54
|
+
return text
|
|
55
|
+
.replace(/\s+/g, " ")
|
|
56
|
+
.replace(/"/g, "#quot;")
|
|
57
|
+
.replace(/</g, "#lt;")
|
|
58
|
+
.replace(/>/g, "#gt;")
|
|
59
|
+
.replace(/\|/g, "#124;")
|
|
60
|
+
.trim();
|
|
61
|
+
}
|
|
62
|
+
function truncate(text, max = 60) {
|
|
63
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
64
|
+
}
|
|
65
|
+
function configString(node, key) {
|
|
66
|
+
const value = node.config?.[key];
|
|
67
|
+
return typeof value === "string" ? value : undefined;
|
|
68
|
+
}
|
|
69
|
+
// A constant config number, however the graph happens to spell it: `30`, the
|
|
70
|
+
// string `"30"`, or a `{{ 30 }}` template body. Anchored, so a real expression
|
|
71
|
+
// (`{{ nodes.start.pct }}`) has no match and stays unlabelled rather than
|
|
72
|
+
// showing a number the run will not use.
|
|
73
|
+
const CONSTANT_NUMBER = /^\s*(?:\{\{\s*([+-]?\d+(?:\.\d+)?)\s*\}\}|([+-]?\d+(?:\.\d+)?))\s*$/;
|
|
74
|
+
/**
|
|
75
|
+
* A numeric config value. Only the canvas writes these as raw JSON numbers: a
|
|
76
|
+
* stored release and an SDK-compiled graph both wrap them in the wire's
|
|
77
|
+
* expression object — `{ kind: "templateExpression", expression: "{{ 30 }}" }`
|
|
78
|
+
* — which the engine resolves before the action sees it. Reading only raw
|
|
79
|
+
* numbers loses the value on every graph that was not hand-written.
|
|
80
|
+
*/
|
|
81
|
+
function configNumber(node, key) {
|
|
82
|
+
const value = node.config?.[key];
|
|
83
|
+
if (typeof value === "number") {
|
|
84
|
+
return Number.isFinite(value) ? value : undefined;
|
|
85
|
+
}
|
|
86
|
+
let source;
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
source = value;
|
|
89
|
+
}
|
|
90
|
+
else if (value !== null && typeof value === "object") {
|
|
91
|
+
const expression = value.expression;
|
|
92
|
+
if (typeof expression === "string")
|
|
93
|
+
source = expression;
|
|
94
|
+
}
|
|
95
|
+
if (source === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
const match = CONSTANT_NUMBER.exec(source);
|
|
98
|
+
if (match === null)
|
|
99
|
+
return undefined;
|
|
100
|
+
const digits = match[1] !== undefined ? match[1] : match[2];
|
|
101
|
+
if (digits === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
const parsed = Number(digits);
|
|
104
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
105
|
+
}
|
|
106
|
+
/** The second label line: what the node actually calls. */
|
|
107
|
+
function detailFor(node) {
|
|
108
|
+
if (node.kind === "connector") {
|
|
109
|
+
return `${node.integrationSlug ?? "?"}.${node.actionSlug ?? "?"}`;
|
|
110
|
+
}
|
|
111
|
+
if (node.kind === "tool") {
|
|
112
|
+
const uuid = node.toolUuid ?? configString(node, "toolUuid");
|
|
113
|
+
const slug = node.templateSlug ?? configString(node, "templateSlug");
|
|
114
|
+
return `tool ${uuid !== undefined && uuid !== null ? uuid.slice(0, 8) : (slug ?? "?")}`;
|
|
115
|
+
}
|
|
116
|
+
if (node.kind === "agent") {
|
|
117
|
+
const uuid = node.agentUuid ?? configString(node, "agentUuid");
|
|
118
|
+
const slug = node.templateSlug ?? configString(node, "templateSlug");
|
|
119
|
+
return `agent ${uuid !== undefined && uuid !== null ? uuid.slice(0, 8) : (slug ?? "?")}`;
|
|
120
|
+
}
|
|
121
|
+
if (node.kind === "native") {
|
|
122
|
+
const action = node.actionSlug ?? "";
|
|
123
|
+
if (action === "start" || action === "end")
|
|
124
|
+
return undefined;
|
|
125
|
+
if (action === "delay") {
|
|
126
|
+
const minutes = configNumber(node, "minutes");
|
|
127
|
+
return minutes !== undefined ? `delay ${String(minutes)}m` : "delay";
|
|
128
|
+
}
|
|
129
|
+
return action.length > 0 ? action : undefined;
|
|
130
|
+
}
|
|
131
|
+
return node.kind ?? undefined;
|
|
132
|
+
}
|
|
133
|
+
/** What the node is, in words a user recognises. */
|
|
134
|
+
export function labelFor(node) {
|
|
135
|
+
const headline = node.name !== null && node.name !== undefined && node.name.trim().length > 0
|
|
136
|
+
? node.name.trim()
|
|
137
|
+
: (node.slug ?? node.kind ?? "node");
|
|
138
|
+
const lines = [truncate(headline)];
|
|
139
|
+
const detail = detailFor(node);
|
|
140
|
+
if (detail !== undefined && detail !== lines[0])
|
|
141
|
+
lines.push(truncate(detail));
|
|
142
|
+
return lines.map(escapeLabel).join("<br/>");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* `childrenUuids` order carries the routing semantics — index 0 of a `branch`
|
|
146
|
+
* is the matched path, index 1 the unmatched one. Getting this backwards
|
|
147
|
+
* inverts the diagram's meaning, so it is table-driven, never inferred.
|
|
148
|
+
*/
|
|
149
|
+
function edgeLabels(node, count) {
|
|
150
|
+
const action = node.kind === "native" ? (node.actionSlug ?? "") : "";
|
|
151
|
+
if (action === "branch")
|
|
152
|
+
return ["yes", "no"];
|
|
153
|
+
if (action === "humanReview")
|
|
154
|
+
return ["approve", "decline"];
|
|
155
|
+
if (action === "filter")
|
|
156
|
+
return ["if true"];
|
|
157
|
+
if (action === "split") {
|
|
158
|
+
const pct = configNumber(node, "percentage");
|
|
159
|
+
if (pct === undefined)
|
|
160
|
+
return ["A", "B"];
|
|
161
|
+
return [`A ${String(pct)}%`, `B ${String(100 - pct)}%`];
|
|
162
|
+
}
|
|
163
|
+
if (action === "switch" || action === "balance") {
|
|
164
|
+
const rawRoutes = node.config?.["routes"];
|
|
165
|
+
const routes = Array.isArray(rawRoutes) ? rawRoutes : [];
|
|
166
|
+
return Array.from({ length: count }, (_, index) => {
|
|
167
|
+
// A switch often has one more child than routes — the default/else path.
|
|
168
|
+
if (index >= routes.length)
|
|
169
|
+
return "default";
|
|
170
|
+
const route = routes[index];
|
|
171
|
+
const name = route !== null && typeof route === "object" && "name" in route
|
|
172
|
+
? route.name
|
|
173
|
+
: undefined;
|
|
174
|
+
return typeof name === "string" ? name : `route ${String(index + 1)}`;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return Array.from({ length: count }, () => undefined);
|
|
178
|
+
}
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Rendering
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
function matches(node, needles) {
|
|
183
|
+
return needles.some((needle) => needle === node.uuid || needle === (node.slug ?? undefined));
|
|
184
|
+
}
|
|
185
|
+
function subNodes(node) {
|
|
186
|
+
const nested = node.config?.["_nodes"];
|
|
187
|
+
return Array.isArray(nested) && nested.length > 0
|
|
188
|
+
? nested
|
|
189
|
+
: undefined;
|
|
190
|
+
}
|
|
191
|
+
function renderNodes(nodes, context) {
|
|
192
|
+
const warnings = [];
|
|
193
|
+
const highlighted = [];
|
|
194
|
+
const direction = context.direction;
|
|
195
|
+
const byUuid = new Map();
|
|
196
|
+
for (const node of nodes) {
|
|
197
|
+
if (typeof node.uuid === "string")
|
|
198
|
+
byUuid.set(node.uuid, node);
|
|
199
|
+
}
|
|
200
|
+
// Ids come from a breadth-first walk from `start`, so the diagram reads in
|
|
201
|
+
// execution order and is stable across runs. Slugs are never used as ids:
|
|
202
|
+
// they repeat within a single release.
|
|
203
|
+
const order = [];
|
|
204
|
+
const ids = new Map();
|
|
205
|
+
const seen = new Set();
|
|
206
|
+
const queue = [];
|
|
207
|
+
const start = nodes.find((node) => node.kind === "native" && node.actionSlug === "start") ?? nodes[0];
|
|
208
|
+
if (start !== undefined)
|
|
209
|
+
queue.push(start.uuid);
|
|
210
|
+
while (queue.length > 0) {
|
|
211
|
+
const uuid = queue.shift();
|
|
212
|
+
if (uuid === undefined || seen.has(uuid))
|
|
213
|
+
continue;
|
|
214
|
+
const node = byUuid.get(uuid);
|
|
215
|
+
if (node === undefined)
|
|
216
|
+
continue;
|
|
217
|
+
seen.add(uuid);
|
|
218
|
+
const id = `${context.idPrefix}n${String(order.length)}`;
|
|
219
|
+
ids.set(uuid, id);
|
|
220
|
+
order.push({ id, node });
|
|
221
|
+
for (const child of node.childrenUuids ?? []) {
|
|
222
|
+
if (child !== null && child !== undefined)
|
|
223
|
+
queue.push(child);
|
|
224
|
+
}
|
|
225
|
+
if (node.fallbackChildUuid !== null &&
|
|
226
|
+
node.fallbackChildUuid !== undefined) {
|
|
227
|
+
queue.push(node.fallbackChildUuid);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const orphans = nodes.filter((node) => typeof node.uuid === "string" && !seen.has(node.uuid));
|
|
231
|
+
for (const node of orphans) {
|
|
232
|
+
const id = `${context.idPrefix}n${String(order.length)}`;
|
|
233
|
+
ids.set(node.uuid, id);
|
|
234
|
+
order.push({ id, node });
|
|
235
|
+
}
|
|
236
|
+
if (orphans.length > 0) {
|
|
237
|
+
warnings.push(`unreachable from start: ${orphans.map((node) => node.slug ?? node.uuid).join(", ")} — these nodes never run`);
|
|
238
|
+
}
|
|
239
|
+
const edges = [];
|
|
240
|
+
const dangling = new Set();
|
|
241
|
+
for (const { id, node } of order) {
|
|
242
|
+
const children = node.childrenUuids ?? [];
|
|
243
|
+
const labels = edgeLabels(node, children.length);
|
|
244
|
+
children.forEach((child, index) => {
|
|
245
|
+
const target = child !== null && child !== undefined ? ids.get(child) : undefined;
|
|
246
|
+
if (target === undefined) {
|
|
247
|
+
dangling.add(node.slug ?? id);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
edges.push({ from: id, to: target, label: labels[index] });
|
|
251
|
+
});
|
|
252
|
+
// A fallback pointing at the node's own next step means "a failure here
|
|
253
|
+
// does not stop the run" — the same arrow, not a second one. Drawing it
|
|
254
|
+
// twice clutters the graph, so it becomes a ↷ marker on the label instead.
|
|
255
|
+
const fallbackUuid = node.fallbackChildUuid;
|
|
256
|
+
if (fallbackUuid !== null && fallbackUuid !== undefined) {
|
|
257
|
+
const target = ids.get(fallbackUuid);
|
|
258
|
+
if (target !== undefined && !children.includes(fallbackUuid)) {
|
|
259
|
+
edges.push({ from: id, to: target, label: "on failure", dashed: true });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (dangling.size > 0) {
|
|
264
|
+
warnings.push(`dangling childrenUuids (null or unknown) on: ${[...dangling].join(", ")} — the graph stops there`);
|
|
265
|
+
}
|
|
266
|
+
const lines = [];
|
|
267
|
+
for (const { id, node } of order) {
|
|
268
|
+
const [open, close] = shapeFor(node);
|
|
269
|
+
const paid = matches(node, context.paid) ? "💳 " : "";
|
|
270
|
+
if (matches(node, context.highlight))
|
|
271
|
+
highlighted.push(id);
|
|
272
|
+
const children = node.childrenUuids ?? [];
|
|
273
|
+
const continuesOnFailure = node.fallbackChildUuid !== null &&
|
|
274
|
+
node.fallbackChildUuid !== undefined &&
|
|
275
|
+
children.includes(node.fallbackChildUuid);
|
|
276
|
+
lines.push(` ${id}${open}"${paid}${labelFor(node)}${continuesOnFailure ? " ↷" : ""}"${close}`);
|
|
277
|
+
// A group's steps are drawn inside a sub-graph, under an id namespace of
|
|
278
|
+
// their own — `--paid` and `--highlight` apply just as they do out here,
|
|
279
|
+
// since a billing step buried in a loop is exactly the one worth seeing.
|
|
280
|
+
const nested = subNodes(node);
|
|
281
|
+
if (nested !== undefined) {
|
|
282
|
+
const inner = renderNodes(nested, { ...context, idPrefix: `${id}s` });
|
|
283
|
+
warnings.push(...inner.warnings.map((w) => `${node.slug ?? id}: ${w}`));
|
|
284
|
+
highlighted.push(...inner.highlighted);
|
|
285
|
+
lines.push(` subgraph ${id}_sub["per item"]`);
|
|
286
|
+
lines.push(` direction ${direction}`);
|
|
287
|
+
lines.push(...inner.lines.map((line) => ` ${line}`));
|
|
288
|
+
lines.push(" end");
|
|
289
|
+
lines.push(` ${id} --> ${id}_sub`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
for (const edge of edges) {
|
|
293
|
+
const arrow = edge.dashed === true
|
|
294
|
+
? edge.label !== undefined
|
|
295
|
+
? `-. ${edge.label} .->`
|
|
296
|
+
: "-.->"
|
|
297
|
+
: edge.label !== undefined
|
|
298
|
+
? `-->|${escapeLabel(edge.label)}|`
|
|
299
|
+
: "-->";
|
|
300
|
+
lines.push(` ${edge.from} ${arrow} ${edge.to}`);
|
|
301
|
+
}
|
|
302
|
+
return { lines, warnings, highlighted };
|
|
303
|
+
}
|
|
304
|
+
export function renderNodeDiagram(nodes, options = {}) {
|
|
305
|
+
const direction = options.direction ?? "TD";
|
|
306
|
+
const lines = [];
|
|
307
|
+
if (options.title !== undefined && options.title.length > 0) {
|
|
308
|
+
// Quote the title if it contains YAML-special characters (colon-space, #,
|
|
309
|
+
// quotes, leading/trailing whitespace) to keep the frontmatter valid.
|
|
310
|
+
const raw = options.title.replace(/\n/g, " ");
|
|
311
|
+
const needsQuotes = /[:#"']|^\s|\s$/.test(raw);
|
|
312
|
+
const escaped = needsQuotes ? `"${raw.replace(/"/g, '\\"')}"` : raw;
|
|
313
|
+
lines.push("---", `title: ${escaped}`, "---");
|
|
314
|
+
}
|
|
315
|
+
lines.push(`flowchart ${direction}`);
|
|
316
|
+
const body = renderNodes(nodes, {
|
|
317
|
+
direction,
|
|
318
|
+
paid: options.paid ?? [],
|
|
319
|
+
highlight: options.highlight ?? [],
|
|
320
|
+
idPrefix: "",
|
|
321
|
+
});
|
|
322
|
+
lines.push(...body.lines);
|
|
323
|
+
// Declared once for the whole diagram, nested levels included — Mermaid takes
|
|
324
|
+
// a `classDef` name a single time.
|
|
325
|
+
if (body.highlighted.length > 0) {
|
|
326
|
+
lines.push(" classDef failing fill:#fee,stroke:#c00,stroke-width:2px");
|
|
327
|
+
lines.push(` class ${body.highlighted.join(",")} failing`);
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
diagram: lines.join("\n"),
|
|
331
|
+
format: "mermaid",
|
|
332
|
+
warnings: body.warnings,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// Locating the graph in a payload
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
/** Node-shaped means: a uuid plus the fields a graph is walked by. */
|
|
339
|
+
export function isNodeArray(value) {
|
|
340
|
+
return (Array.isArray(value) &&
|
|
341
|
+
value.length > 0 &&
|
|
342
|
+
value.every((item) => item !== null &&
|
|
343
|
+
typeof item === "object" &&
|
|
344
|
+
typeof item.uuid === "string" &&
|
|
345
|
+
("childrenUuids" in item || "kind" in item)));
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Pull the graph out of a CLI payload: `release get`, `release get-draft`,
|
|
349
|
+
* `template get`, an ad-hoc `run get`, or a bare node array.
|
|
350
|
+
*/
|
|
351
|
+
export function extractNodes(payload) {
|
|
352
|
+
if (isNodeArray(payload))
|
|
353
|
+
return payload;
|
|
354
|
+
if (payload === null || typeof payload !== "object")
|
|
355
|
+
return undefined;
|
|
356
|
+
const record = payload;
|
|
357
|
+
const containers = [
|
|
358
|
+
record["nodes"],
|
|
359
|
+
record["release"]?.["nodes"],
|
|
360
|
+
record["draftRelease"]?.["nodes"],
|
|
361
|
+
record["template"]?.["nodes"],
|
|
362
|
+
record["run"]?.["nodes"],
|
|
363
|
+
];
|
|
364
|
+
for (const container of containers) {
|
|
365
|
+
if (isNodeArray(container))
|
|
366
|
+
return container;
|
|
367
|
+
}
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
/** The fenced block, ready to paste into a message, a PR, or the docs. */
|
|
371
|
+
export function toMermaidBlock(diagram) {
|
|
372
|
+
return `\`\`\`mermaid\n${diagram}\n\`\`\``;
|
|
373
|
+
}
|
package/build/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { registerCommands as registerCdkCommands, registerManifestCommands, SessionExpiredError, } from "@cargo-ai/cdk/cli";
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
import { AGENT_SKILLS_HINT } from "./agentSkills.js";
|
|
5
6
|
import { createApi } from "./api.js";
|
|
6
7
|
import { registerAiCommands } from "./commands/ai/index.js";
|
|
7
8
|
import { registerAuthCommands } from "./commands/auth/index.js";
|
|
@@ -29,11 +30,6 @@ import { maybeNotifyUpdate } from "./updateNotifier.js";
|
|
|
29
30
|
const require = createRequire(import.meta.url);
|
|
30
31
|
const { version } = require("../package.json");
|
|
31
32
|
const program = new Command();
|
|
32
|
-
// Coding agents meet this CLI cold and explore it by running `--help` or by
|
|
33
|
-
// guessing a subcommand and reading the error. Both paths are the only chance to
|
|
34
|
-
// tell them the skill bundle exists, so the pointer lives in the help epilogue
|
|
35
|
-
// and in the error footer rather than in a command they would have to know to run.
|
|
36
|
-
const AGENT_SKILLS_HINT = "Coding agents: install the Cargo skills with `npx skills add getcargohq/cargo-skills`";
|
|
37
33
|
program
|
|
38
34
|
.name("cargo-ai")
|
|
39
35
|
.description("Cargo API CLI")
|
|
@@ -41,12 +37,14 @@ program
|
|
|
41
37
|
.showHelpAfterError(`${AGENT_SKILLS_HINT}\nRun "cargo-ai --help" for the full command list.`)
|
|
42
38
|
.addHelpText("after", `
|
|
43
39
|
Agent skills:
|
|
44
|
-
Teach your coding agent this CLI —
|
|
40
|
+
Teach your coding agent this CLI — skills covering sourcing, enrichment,
|
|
45
41
|
orchestration, storage, connectors, workspace-as-code (CDK), diagnostics and alerts.
|
|
46
42
|
Any agent (skills.sh): npx skills add getcargohq/cargo-skills
|
|
47
43
|
Claude Code: /plugin marketplace add getcargohq/cargo-skills
|
|
48
44
|
Codex: codex plugin marketplace add getcargohq/cargo-skills
|
|
49
45
|
Source: https://github.com/getcargohq/cargo-skills
|
|
46
|
+
Standalone one-job GTM skills (find leads, work emails, TAM, signals):
|
|
47
|
+
Any agent (skills.sh): npx skills add getcargohq/gtm-skills
|
|
50
48
|
|
|
51
49
|
Authentication:
|
|
52
50
|
Emailed code: cargo-ai login --email you@company.com (no browser; signs you up on first use)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cargo-ai/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.55",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://getcargo.ai",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@cargo-ai/api": "^1.0.60",
|
|
63
63
|
"@cargo-ai/app-sdk": "^1.0.6",
|
|
64
|
-
"@cargo-ai/cdk": "^1.0.
|
|
64
|
+
"@cargo-ai/cdk": "^1.0.45",
|
|
65
65
|
"@cargo-ai/types": "^1.0.57",
|
|
66
66
|
"@cargo-ai/worker-sdk": "^1.0.12",
|
|
67
67
|
"@modelcontextprotocol/sdk": "1.29.0",
|