@cargo-ai/cli 1.0.126 → 1.0.128
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/ai/prompt.d.ts.map +1 -1
- package/build/commands/ai/prompt.js +2 -2
- package/build/commands/ask/harness/bridgeOutput.d.ts +19 -0
- package/build/commands/ask/harness/bridgeOutput.d.ts.map +1 -0
- package/build/commands/ask/harness/bridgeOutput.js +42 -0
- package/build/commands/ask/harness/index.d.ts +3 -2
- package/build/commands/ask/harness/index.d.ts.map +1 -1
- package/build/commands/ask/harness/index.js +2 -1
- package/build/commands/ask/harness/turn.d.ts +17 -0
- package/build/commands/ask/harness/turn.d.ts.map +1 -1
- package/build/commands/ask/harness/turn.js +85 -3
- package/build/commands/ask/harness/usage.d.ts +19 -0
- package/build/commands/ask/harness/usage.d.ts.map +1 -1
- package/build/commands/ask/harness/usage.js +46 -0
- package/build/commands/ask/input/completer.d.ts +1 -1
- package/build/commands/ask/input/completer.d.ts.map +1 -1
- package/build/commands/ask/input/completer.js +2 -0
- package/build/commands/ask/input/prompt.d.ts.map +1 -1
- package/build/commands/ask/input/prompt.js +54 -8
- package/build/commands/ask/input/slash.d.ts +7 -1
- package/build/commands/ask/input/slash.d.ts.map +1 -1
- package/build/commands/ask/input/slash.js +10 -0
- package/build/commands/ask/session/index.d.ts +2 -1
- package/build/commands/ask/session/index.d.ts.map +1 -1
- package/build/commands/ask/session/index.js +2 -1
- package/build/commands/ask/session/markdown.d.ts +20 -0
- package/build/commands/ask/session/markdown.d.ts.map +1 -0
- package/build/commands/ask/session/markdown.js +451 -0
- package/build/commands/ask/session/run.d.ts.map +1 -1
- package/build/commands/ask/session/run.js +194 -12
- package/build/commands/ask/session/tree.d.ts +81 -0
- package/build/commands/ask/session/tree.d.ts.map +1 -1
- package/build/commands/ask/session/tree.js +322 -51
- package/build/commands/auth/index.js +1 -1
- package/build/commands/connection/connector.d.ts.map +1 -1
- package/build/commands/connection/connector.js +1 -1
- package/build/commands/hosting/envVar.d.ts.map +1 -1
- package/build/commands/hosting/envVar.js +15 -6
- package/build/commands/orchestration/span.js +2 -2
- package/build/commands/orchestration/trace.js +2 -2
- package/build/commands/runHandler.d.ts +1 -0
- package/build/commands/runHandler.d.ts.map +1 -1
- package/build/commands/runHandler.js +6 -3
- package/build/commands/segmentation/change.js +1 -1
- package/build/commands/workspaceManagement/user.d.ts.map +1 -1
- package/build/commands/workspaceManagement/user.js +8 -5
- package/build/utils/textWidth.d.ts +25 -0
- package/build/utils/textWidth.d.ts.map +1 -0
- package/build/utils/textWidth.js +147 -0
- package/package.json +2 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { isAbsolute, relative } from "node:path";
|
|
1
|
+
import { basename, isAbsolute, relative } from "node:path";
|
|
2
2
|
import { selectOne } from "@cargo-ai/cdk/cli";
|
|
3
|
+
import { clipToCells, textWidth } from "../../../utils/textWidth.js";
|
|
3
4
|
import { clipToWidth, colors } from "../../runHandler.js";
|
|
4
5
|
import { ASK_USER_QUESTION_TOOL_NAME, readAskUserQuestions, } from "../harness/askUserQuestion.js";
|
|
6
|
+
import { renderMarkdown } from "./markdown.js";
|
|
5
7
|
/**
|
|
6
8
|
* What an assistant turn is marked with, alongside `◇` for a step and `│` for
|
|
7
9
|
* the rail.
|
|
@@ -35,7 +37,7 @@ export function wrapLine(text, width) {
|
|
|
35
37
|
const lines = [];
|
|
36
38
|
for (const word of text.split(/\s+/).filter((part) => part.length > 0)) {
|
|
37
39
|
const current = lines[lines.length - 1];
|
|
38
|
-
if (current === undefined || `${current} ${word}
|
|
40
|
+
if (current === undefined || textWidth(`${current} ${word}`) > width) {
|
|
39
41
|
lines.push(word);
|
|
40
42
|
continue;
|
|
41
43
|
}
|
|
@@ -63,16 +65,6 @@ function writeRailWrapped(body) {
|
|
|
63
65
|
writeRail(line);
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Light cleanup so model markdown does not fight the tree. Keeps the words;
|
|
68
|
-
* drops heading marks, bold markers, and horizontal rules.
|
|
69
|
-
*/
|
|
70
|
-
function forTerminal(text) {
|
|
71
|
-
return text
|
|
72
|
-
.replace(/^#{1,6}\s+/gm, "")
|
|
73
|
-
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
74
|
-
.replace(/^---+$/gm, "");
|
|
75
|
-
}
|
|
76
68
|
/**
|
|
77
69
|
* A `◇` step: the first line beside the marker, the rest continuing on the
|
|
78
70
|
* rail under it.
|
|
@@ -115,44 +107,66 @@ export function showUserTurn(body) {
|
|
|
115
107
|
writeDiamondBlock(body);
|
|
116
108
|
}
|
|
117
109
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
110
|
+
* Where the finished part of a reply ends: just past its last blank line, as
|
|
111
|
+
* long as that line is not inside a code fence. Never before `from`.
|
|
120
112
|
*/
|
|
121
|
-
function
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
lines.push("");
|
|
131
|
-
continue;
|
|
113
|
+
function commitPoint(buffer, from) {
|
|
114
|
+
const cursor = { at: buffer.lastIndexOf("\n\n") };
|
|
115
|
+
while (cursor.at >= from) {
|
|
116
|
+
const fences = buffer
|
|
117
|
+
.slice(0, cursor.at)
|
|
118
|
+
.split("\n")
|
|
119
|
+
.filter((line) => /^\s*(```|~~~)/.test(line) === true).length;
|
|
120
|
+
if (fences % 2 === 0) {
|
|
121
|
+
return cursor.at + 2;
|
|
132
122
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
123
|
+
cursor.at = buffer.lastIndexOf("\n\n", cursor.at - 1);
|
|
124
|
+
}
|
|
125
|
+
return from;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Write reply lines: the first beside `■` until one has been printed for
|
|
129
|
+
* good, the rest on the │ rail. Returns how many rows were written.
|
|
130
|
+
*/
|
|
131
|
+
function writeAssistantLines(live, lines) {
|
|
132
|
+
lines.forEach((line, index) => {
|
|
133
|
+
if (index === 0 && live.markerCommitted === false) {
|
|
134
|
+
process.stderr.write(`${ASSISTANT_MARKER} ${line}\n`);
|
|
135
|
+
return;
|
|
140
136
|
}
|
|
137
|
+
writeRail(line);
|
|
138
|
+
});
|
|
139
|
+
return lines.length;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Markdown lines for part of a reply, after the blank row that separates it
|
|
143
|
+
* from paragraphs already printed for good.
|
|
144
|
+
*/
|
|
145
|
+
function assistantLines(text, withGap) {
|
|
146
|
+
const body = text.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
147
|
+
if (body === "") {
|
|
148
|
+
return [];
|
|
141
149
|
}
|
|
150
|
+
const lines = renderMarkdown(body, treeBodyWidth());
|
|
151
|
+
return withGap === true ? ["", ...lines] : lines;
|
|
152
|
+
}
|
|
153
|
+
/** Print reply lines for good: they are never cleared or drawn again. */
|
|
154
|
+
function commitAssistantLines(live, lines) {
|
|
142
155
|
if (lines.length === 0) {
|
|
143
|
-
return
|
|
156
|
+
return;
|
|
144
157
|
}
|
|
145
|
-
|
|
158
|
+
writeAssistantLines(live, lines);
|
|
159
|
+
live.markerCommitted = true;
|
|
146
160
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
/**
|
|
162
|
+
* How many rows the redrawn part of a reply may take. Clearing it means
|
|
163
|
+
* moving the cursor up past every row, and the cursor stops at the top of the
|
|
164
|
+
* screen: rows that scrolled off would be drawn again under themselves. Two
|
|
165
|
+
* rows are kept for the typed line and the margin.
|
|
166
|
+
*/
|
|
167
|
+
function liveRowLimit() {
|
|
168
|
+
const rows = process.stderr.rows === undefined ? 24 : process.stderr.rows;
|
|
169
|
+
return Math.max(3, rows - 2);
|
|
156
170
|
}
|
|
157
171
|
function clearPaintedLines(count) {
|
|
158
172
|
if (count <= 0 || process.stderr.isTTY !== true) {
|
|
@@ -164,7 +178,15 @@ function clearPaintedLines(count) {
|
|
|
164
178
|
export function beginAssistantTurn() {
|
|
165
179
|
writeRail("");
|
|
166
180
|
process.stderr.write(`${ASSISTANT_MARKER} \n`);
|
|
167
|
-
return {
|
|
181
|
+
return {
|
|
182
|
+
buffer: "",
|
|
183
|
+
committed: 0,
|
|
184
|
+
tailLinesCommitted: 0,
|
|
185
|
+
tailHasGap: false,
|
|
186
|
+
markerCommitted: false,
|
|
187
|
+
paintedLines: 1,
|
|
188
|
+
footer: "",
|
|
189
|
+
};
|
|
168
190
|
}
|
|
169
191
|
export function appendAssistantDelta(live, delta) {
|
|
170
192
|
// A tool between two pieces of narration inserts a paragraph break, so that
|
|
@@ -182,10 +204,27 @@ export function appendAssistantDelta(live, delta) {
|
|
|
182
204
|
}
|
|
183
205
|
/** Draw the block again from its buffer, with the typed line under it. */
|
|
184
206
|
function repaintAssistantTurn(live) {
|
|
185
|
-
const lines = assistantDisplayLines(live.buffer);
|
|
186
207
|
clearPaintedLines(live.paintedLines);
|
|
187
|
-
|
|
188
|
-
|
|
208
|
+
live.paintedLines = 0;
|
|
209
|
+
const cut = commitPoint(live.buffer, live.committed);
|
|
210
|
+
if (cut > live.committed) {
|
|
211
|
+
// Rows of it may already be out, from when it was the unfinished part.
|
|
212
|
+
commitAssistantLines(live, assistantLines(live.buffer.slice(live.committed, cut), live.tailHasGap).slice(live.tailLinesCommitted));
|
|
213
|
+
live.committed = cut;
|
|
214
|
+
live.tailLinesCommitted = 0;
|
|
215
|
+
live.tailHasGap = live.markerCommitted;
|
|
216
|
+
}
|
|
217
|
+
const tail = assistantLines(live.buffer.slice(live.committed), live.tailHasGap).slice(live.tailLinesCommitted);
|
|
218
|
+
const overflow = Math.max(0, tail.length - liveRowLimit());
|
|
219
|
+
// A table or a code block taller than the screen: its top rows go out for
|
|
220
|
+
// good. A table can still widen below them; that beats drawing it twice.
|
|
221
|
+
if (overflow > 0) {
|
|
222
|
+
commitAssistantLines(live, tail.slice(0, overflow));
|
|
223
|
+
live.tailLinesCommitted += overflow;
|
|
224
|
+
}
|
|
225
|
+
const visible = tail.slice(overflow);
|
|
226
|
+
// Nothing said yet: keep the bare marker up, as `beginAssistantTurn` drew it.
|
|
227
|
+
live.paintedLines = writeAssistantLines(live, visible.length === 0 && live.markerCommitted === false ? [""] : visible);
|
|
189
228
|
if (live.footer !== "") {
|
|
190
229
|
// Clipped for the same reason the spinner's is: it has to occupy exactly
|
|
191
230
|
// one row, or the next repaint clears from the wrong line.
|
|
@@ -216,6 +255,171 @@ export function showAssistantTurn(body) {
|
|
|
216
255
|
appendAssistantDelta(live, body);
|
|
217
256
|
endAssistantTurn(live);
|
|
218
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* How Claude Code's thinking is drawn, the same choices `@ai-sdk/tui` offers:
|
|
260
|
+
*
|
|
261
|
+
* - `full`: the whole summary stays in the transcript.
|
|
262
|
+
* - `auto-collapsed`: the latest lines show while it thinks, then fold into
|
|
263
|
+
* one "Thought for 4s" line once it moves on.
|
|
264
|
+
* - `collapsed`: only that one line.
|
|
265
|
+
* - `hidden`: nothing beyond the spinner.
|
|
266
|
+
*/
|
|
267
|
+
export const REASONING_DISPLAYS = [
|
|
268
|
+
"full",
|
|
269
|
+
"auto-collapsed",
|
|
270
|
+
"collapsed",
|
|
271
|
+
"hidden",
|
|
272
|
+
];
|
|
273
|
+
/**
|
|
274
|
+
* How finished tool calls are drawn: `full` adds the start of each result
|
|
275
|
+
* under its line, `collapsed` keeps one line per call (and the first line of
|
|
276
|
+
* an error), `hidden` leaves only the spinner while they run.
|
|
277
|
+
*/
|
|
278
|
+
export const TOOL_DISPLAYS = ["full", "collapsed", "hidden"];
|
|
279
|
+
const REASONING_MARKER = "∴";
|
|
280
|
+
/** Rows of thinking kept on screen while it streams. */
|
|
281
|
+
const REASONING_LIVE_LINES = 6;
|
|
282
|
+
/** Rows of a tool result shown under it in `full`. */
|
|
283
|
+
const TOOL_OUTPUT_PREVIEW_LINES = 6;
|
|
284
|
+
/**
|
|
285
|
+
* Open a thinking block. Only the expanded displays draw while it streams;
|
|
286
|
+
* `collapsed` leaves the spinner up and adds its one line at the end.
|
|
287
|
+
*/
|
|
288
|
+
export function beginReasoning(display) {
|
|
289
|
+
const live = {
|
|
290
|
+
buffer: "",
|
|
291
|
+
display,
|
|
292
|
+
startedAtMs: Date.now(),
|
|
293
|
+
paintedLines: 0,
|
|
294
|
+
footer: "",
|
|
295
|
+
};
|
|
296
|
+
// No gap before it: thinking sits with the tool lines, not the replies.
|
|
297
|
+
if (drawsWhileThinking(display) === true) {
|
|
298
|
+
repaintReasoning(live);
|
|
299
|
+
}
|
|
300
|
+
return live;
|
|
301
|
+
}
|
|
302
|
+
export function appendReasoningDelta(live, delta) {
|
|
303
|
+
live.buffer += delta;
|
|
304
|
+
if (drawsWhileThinking(live.display) === true) {
|
|
305
|
+
repaintReasoning(live);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
export function setReasoningFooter(live, footer) {
|
|
309
|
+
if (live.footer === footer) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
live.footer = footer;
|
|
313
|
+
if (drawsWhileThinking(live.display) === true) {
|
|
314
|
+
repaintReasoning(live);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/** Close the block, leaving what its display keeps in the transcript. */
|
|
318
|
+
export function endReasoning(live, spinner) {
|
|
319
|
+
clearPaintedLines(live.paintedLines);
|
|
320
|
+
live.paintedLines = 0;
|
|
321
|
+
if (live.display === "hidden") {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const header = `${colors.dim(REASONING_MARKER)} ${colors.dim(`Thought for ${formatDuration(Date.now() - live.startedAtMs)}`)}`;
|
|
325
|
+
if (live.display === "collapsed") {
|
|
326
|
+
// The spinner stayed up through this one; log above it.
|
|
327
|
+
if (spinner !== undefined) {
|
|
328
|
+
spinner.log(header);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
process.stderr.write(`${header}\n`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
process.stderr.write(`${header}\n`);
|
|
335
|
+
if (live.display === "full") {
|
|
336
|
+
for (const line of reasoningLines(live.buffer)) {
|
|
337
|
+
writeRail(line);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function drawsWhileThinking(display) {
|
|
342
|
+
return display === "full" || display === "auto-collapsed";
|
|
343
|
+
}
|
|
344
|
+
function repaintReasoning(live) {
|
|
345
|
+
clearPaintedLines(live.paintedLines);
|
|
346
|
+
const lines = reasoningLines(live.buffer).slice(-REASONING_LIVE_LINES);
|
|
347
|
+
process.stderr.write(`${colors.dim(REASONING_MARKER)} ${colors.dim("Thinking…")}\n`);
|
|
348
|
+
for (const line of lines) {
|
|
349
|
+
writeRail(line);
|
|
350
|
+
}
|
|
351
|
+
live.paintedLines = lines.length + 1;
|
|
352
|
+
if (live.footer !== "") {
|
|
353
|
+
process.stderr.write(`${colors.dim(clipToWidth(live.footer))}\n`);
|
|
354
|
+
live.paintedLines += 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function reasoningLines(text) {
|
|
358
|
+
const body = text.trim();
|
|
359
|
+
if (body === "") {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
return renderMarkdown(body, treeBodyWidth(), "dim");
|
|
363
|
+
}
|
|
364
|
+
/** `4s`, `1m 12s`. */
|
|
365
|
+
function formatDuration(ms) {
|
|
366
|
+
const seconds = Math.max(1, Math.round(ms / 1000));
|
|
367
|
+
if (seconds < 60) {
|
|
368
|
+
return `${String(seconds)}s`;
|
|
369
|
+
}
|
|
370
|
+
return `${String(Math.floor(seconds / 60))}m ${String(seconds % 60)}s`;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* What a turn cost, under the reply: tokens written, how fast they came, how
|
|
374
|
+
* full the context window is, and the credits it was billed.
|
|
375
|
+
*
|
|
376
|
+
* │ 2.4k tokens written · 38 tok/s · 92k of 1M context (9%) · 6.10 credits
|
|
377
|
+
*/
|
|
378
|
+
export function showTurnStats(stats) {
|
|
379
|
+
if (stats.outputTokens === 0) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
// Written, not read: what the model read is the context share below.
|
|
383
|
+
const parts = [`${formatTokens(stats.outputTokens)} tokens written`];
|
|
384
|
+
// Under a second of generation is too short to measure a rate from.
|
|
385
|
+
if (stats.generationMs >= 1000) {
|
|
386
|
+
parts.push(`${String(Math.round(stats.outputTokens / (stats.generationMs / 1000)))} tok/s`);
|
|
387
|
+
}
|
|
388
|
+
if (stats.contextTokens !== undefined && stats.contextTokens > 0) {
|
|
389
|
+
const percent = Math.min(100, Math.max(1, Math.round((stats.contextTokens / stats.contextWindow) * 100)));
|
|
390
|
+
parts.push(`${formatTokens(stats.contextTokens)} of ${formatTokens(stats.contextWindow)} context (${String(percent)}%)`);
|
|
391
|
+
}
|
|
392
|
+
if (stats.credits > 0) {
|
|
393
|
+
parts.push(`${stats.credits.toFixed(2)} credit${stats.credits === 1 ? "" : "s"}`);
|
|
394
|
+
}
|
|
395
|
+
// Broken between items, never inside one, when the line is wider than the
|
|
396
|
+
// rail leaves room for: wrapped by the terminal it would start its next row
|
|
397
|
+
// at column zero, outside the tree.
|
|
398
|
+
const width = treeBodyWidth();
|
|
399
|
+
const rows = [];
|
|
400
|
+
for (const part of parts.map((entry) => clipToCells(entry, width))) {
|
|
401
|
+
const last = rows[rows.length - 1];
|
|
402
|
+
if (last !== undefined && textWidth(`${last} · ${part}`) <= width) {
|
|
403
|
+
rows[rows.length - 1] = `${last} · ${part}`;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
rows.push(part);
|
|
407
|
+
}
|
|
408
|
+
for (const row of rows) {
|
|
409
|
+
writeRail(colors.dim(row));
|
|
410
|
+
}
|
|
411
|
+
writeRail("");
|
|
412
|
+
}
|
|
413
|
+
/** `950`, `2.4k`, `1M`. */
|
|
414
|
+
function formatTokens(count) {
|
|
415
|
+
if (count >= 1_000_000) {
|
|
416
|
+
return `${String(Math.round(count / 100_000) / 10)}M`;
|
|
417
|
+
}
|
|
418
|
+
if (count >= 1_000) {
|
|
419
|
+
return `${String(Math.round(count / 100) / 10)}k`;
|
|
420
|
+
}
|
|
421
|
+
return String(count);
|
|
422
|
+
}
|
|
219
423
|
/** Common tool names arrive lowercase (`bash`); natives keep their case. */
|
|
220
424
|
function toolLabel(toolName) {
|
|
221
425
|
if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
@@ -233,9 +437,7 @@ export function describeToolActivity(toolName, summary) {
|
|
|
233
437
|
return label;
|
|
234
438
|
}
|
|
235
439
|
const line = `${label}: ${summary}`;
|
|
236
|
-
return line
|
|
237
|
-
? `${line.slice(0, ACTIVITY_MAX_LENGTH - 1)}…`
|
|
238
|
-
: line;
|
|
440
|
+
return clipToCells(line, ACTIVITY_MAX_LENGTH);
|
|
239
441
|
}
|
|
240
442
|
/**
|
|
241
443
|
* A tool that has finished, left in the transcript.
|
|
@@ -250,15 +452,84 @@ export function describeToolActivity(toolName, summary) {
|
|
|
250
452
|
* line, leaves this behind, and lets the next frame redraw underneath.
|
|
251
453
|
*/
|
|
252
454
|
export function showToolRun(input) {
|
|
455
|
+
if (input.display === "hidden") {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
253
458
|
const marker = input.isError === true ? colors.red("✗") : colors.dim(TOOL_MARKER);
|
|
254
459
|
const label = describeToolActivity(input.toolName, input.summary);
|
|
255
|
-
const
|
|
460
|
+
const lines = [
|
|
461
|
+
`${marker} ${colors.dim(label)}`,
|
|
462
|
+
...toolOutputPreview(input.output, input.display, input.isError).map((line) => `${colors.dim("│")} ${colors.dim(line)}`),
|
|
463
|
+
];
|
|
464
|
+
for (const line of lines) {
|
|
465
|
+
if (input.spinner !== undefined) {
|
|
466
|
+
input.spinner.log(line);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
process.stderr.write(`${line}\n`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Several calls of one tool that finished in a row, as one line:
|
|
475
|
+
*
|
|
476
|
+
* ⏺ Read 6 files: run.ts, tree.ts, markdown.ts, …
|
|
477
|
+
*
|
|
478
|
+
* Six `Read` lines say less than this one, and push the reply off screen.
|
|
479
|
+
* Only for successful calls under `collapsed`: a failure keeps its own line
|
|
480
|
+
* and its error, and `full` shows each result.
|
|
481
|
+
*/
|
|
482
|
+
export function showToolGroup(input) {
|
|
483
|
+
const count = input.summaries.length;
|
|
484
|
+
const verb = TOOL_GROUP_VERBS.find((entry) => entry.toolName === input.toolName.toLowerCase());
|
|
485
|
+
const head = verb === undefined
|
|
486
|
+
? `${toolLabel(input.toolName)} ×${String(count)}`
|
|
487
|
+
: `${verb.label} ${String(count)} ${verb.noun}`;
|
|
488
|
+
// Paths shown by file name: the directory is usually the same for all.
|
|
489
|
+
const items = input.summaries
|
|
490
|
+
.filter((summary) => summary !== "")
|
|
491
|
+
.map((summary) => isAbsolute(summary) === true ? basename(summary) : summary);
|
|
492
|
+
const label = clipToCells(items.length === 0 ? head : `${head}: ${items.join(", ")}`, treeBodyWidth());
|
|
493
|
+
const line = `${colors.dim(TOOL_MARKER)} ${colors.dim(label)}`;
|
|
256
494
|
if (input.spinner !== undefined) {
|
|
257
495
|
input.spinner.log(line);
|
|
258
496
|
return;
|
|
259
497
|
}
|
|
260
498
|
process.stderr.write(`${line}\n`);
|
|
261
499
|
}
|
|
500
|
+
/** How a run of one built-in tool is counted. Names arrive lowercase. */
|
|
501
|
+
const TOOL_GROUP_VERBS = [
|
|
502
|
+
{ toolName: "read", label: "Read", noun: "files" },
|
|
503
|
+
{ toolName: "edit", label: "Edited", noun: "files" },
|
|
504
|
+
{ toolName: "multiedit", label: "Edited", noun: "files" },
|
|
505
|
+
{ toolName: "write", label: "Wrote", noun: "files" },
|
|
506
|
+
{ toolName: "bash", label: "Ran", noun: "commands" },
|
|
507
|
+
{ toolName: "grep", label: "Ran", noun: "searches" },
|
|
508
|
+
{ toolName: "glob", label: "Listed", noun: "patterns" },
|
|
509
|
+
{ toolName: "webfetch", label: "Fetched", noun: "pages" },
|
|
510
|
+
{ toolName: "websearch", label: "Ran", noun: "web searches" },
|
|
511
|
+
];
|
|
512
|
+
/**
|
|
513
|
+
* The start of a tool's result, clipped to the rail. A failure always gets
|
|
514
|
+
* its first line, so a ✗ says why without switching the display.
|
|
515
|
+
*/
|
|
516
|
+
function toolOutputPreview(output, display, isError) {
|
|
517
|
+
const limit = display === "full" ? TOOL_OUTPUT_PREVIEW_LINES : isError === true ? 1 : 0;
|
|
518
|
+
if (limit === 0) {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
const lines = output
|
|
522
|
+
.replace(/\t/g, " ")
|
|
523
|
+
.split("\n")
|
|
524
|
+
.map((line) => line.trimEnd())
|
|
525
|
+
.filter((line) => line !== "");
|
|
526
|
+
const width = treeBodyWidth();
|
|
527
|
+
const shown = lines.slice(0, limit).map((line) => clipToCells(line, width));
|
|
528
|
+
if (lines.length > limit) {
|
|
529
|
+
shown.push(`… ${String(lines.length - limit)} more line${lines.length - limit === 1 ? "" : "s"}`);
|
|
530
|
+
}
|
|
531
|
+
return shown;
|
|
532
|
+
}
|
|
262
533
|
/**
|
|
263
534
|
* A path inside the session directory reads better relative to it; anything
|
|
264
535
|
* outside stays absolute so it is obvious the turn left the project.
|
|
@@ -20,7 +20,7 @@ export function registerAuthCommands(program, getApi) {
|
|
|
20
20
|
.option("--oauth [true|false]", "Sign in via browser using the OAuth 2.0 Device Authorization Flow", parseBooleanOption)
|
|
21
21
|
.option("--agentid [true|false]", "Sign in as an agent through the AgentID connection; prints an Auth0 URL and waits for it to redirect back to this machine", parseBooleanOption)
|
|
22
22
|
.option("--token <token>", "Existing Cargo API token")
|
|
23
|
-
.option("--workspace-name <name>", "Workspace to use
|
|
23
|
+
.option("--workspace-name <name>", "Workspace to use (skips the workspace prompt): one of that name if it exists; otherwise the Personal workspace a first sign-in creates is renamed to it (when under 10 minutes old and you are its only member); otherwise it is created")
|
|
24
24
|
.option("--workspace-uuid <uuid>", "Workspace UUID to sign in for (skips the workspace prompt)")
|
|
25
25
|
.option("--base-url <url>", `Cargo API base URL (string, default: ${DEFAULT_BASE_URL})`)
|
|
26
26
|
.addHelpText("after", `
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/commands/connection/connector.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAcxC,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,
|
|
1
|
+
{"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/commands/connection/connector.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAcxC,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAgUN"}
|
|
@@ -69,7 +69,7 @@ export function registerConnectorCommands(parent, getApi) {
|
|
|
69
69
|
.command("get-dynamic-schema")
|
|
70
70
|
.description("Get dynamic schema for a connector action")
|
|
71
71
|
.requiredOption("--connector-uuid <uuid>", "Connector UUID (string, required)")
|
|
72
|
-
.requiredOption("--slug <slug>", "
|
|
72
|
+
.requiredOption("--slug <slug>", "Dynamic-schema slug as the integration declares it (e.g. hubspot's getConsentOptions) — not the action slug (string, required)")
|
|
73
73
|
.requiredOption("--params <json>", "Schema parameters (JSON object, required)")
|
|
74
74
|
.action(async (opts) => {
|
|
75
75
|
const api = getApi();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"envVar.d.ts","sourceRoot":"","sources":["../../../src/commands/hosting/envVar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AASxC,KAAK,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;AAehC;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,EACjB,OAAO,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1C,IAAI,
|
|
1
|
+
{"version":3,"file":"envVar.d.ts","sourceRoot":"","sources":["../../../src/commands/hosting/envVar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AASxC,KAAK,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;AAehC;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,EACjB,OAAO,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1C,IAAI,CAkJN"}
|
|
@@ -37,18 +37,27 @@ ${note}`);
|
|
|
37
37
|
: { workerUuid: subjectUuid }));
|
|
38
38
|
outputJson(result);
|
|
39
39
|
});
|
|
40
|
-
env
|
|
40
|
+
const set = env
|
|
41
41
|
.command("set")
|
|
42
42
|
.description(`Create an environment variable on a ${subject}, or update it if the key already exists. Takes effect on the next deployment`)
|
|
43
43
|
.requiredOption(`${flag} <uuid>`, `${capitalize(subject)} UUID`)
|
|
44
44
|
.requiredOption("--key <key>", "Environment variable name")
|
|
45
|
-
.option("--value <value>", "Value. Pass '-' to read it from stdin, or omit it to read the local environment variable of the same name — both keep a secret out of shell history")
|
|
46
|
-
|
|
45
|
+
.option("--value <value>", "Value. Pass '-' to read it from stdin, or omit it to read the local environment variable of the same name — both keep a secret out of shell history");
|
|
46
|
+
// An app's values are compiled into its public bundle, so the API refuses a
|
|
47
|
+
// secret one (secretNotSupportedForApp); only a worker gets the flag.
|
|
48
|
+
if (subject === "worker") {
|
|
49
|
+
set.option("--secret [true|false]", "Encrypt the value at rest and never return it. On an existing key, omit to keep its current setting", parseBooleanOption);
|
|
50
|
+
}
|
|
51
|
+
const examples = subject === "worker"
|
|
52
|
+
? ` $ cargo-ai hosting ${subject} ${name} set ${flag} <uuid> --key LOG_LEVEL --value debug
|
|
53
|
+
$ printf %s "$TOKEN" | cargo-ai hosting ${subject} ${name} set ${flag} <uuid> --key CARGO_API_TOKEN --value - --secret
|
|
54
|
+
$ CARGO_API_TOKEN=... cargo-ai hosting ${subject} ${name} set ${flag} <uuid> --key CARGO_API_TOKEN --secret`
|
|
55
|
+
: ` $ cargo-ai hosting ${subject} ${name} set ${flag} <uuid> --key VITE_API_URL --value https://api.example.com
|
|
56
|
+
$ cargo-ai hosting ${subject} ${name} set ${flag} <uuid> --key VITE_WORKER_URL --value https://my-worker.example.com`;
|
|
57
|
+
set
|
|
47
58
|
.addHelpText("after", `
|
|
48
59
|
Examples:
|
|
49
|
-
|
|
50
|
-
$ printf %s "$TOKEN" | cargo-ai hosting ${subject} ${name} set ${flag} <uuid> --key CARGO_API_TOKEN --value - --secret
|
|
51
|
-
$ CARGO_API_TOKEN=... cargo-ai hosting ${subject} ${name} set ${flag} <uuid> --key CARGO_API_TOKEN --secret
|
|
60
|
+
${examples}
|
|
52
61
|
|
|
53
62
|
${note}`)
|
|
54
63
|
.action(async (opts) => {
|
|
@@ -24,7 +24,7 @@ export function registerSpanCommands(parent, getApi) {
|
|
|
24
24
|
.option("--node-release-uuid <uuid>", "Release UUID on the node")
|
|
25
25
|
.option("--execution-statuses <pending,success,error>", "Filter by execution statuses")
|
|
26
26
|
.option("--parent-run-uuid <uuid>", "Parent run UUID")
|
|
27
|
-
.requiredOption("--execution-started-after <date>", "Execution started after (ISO date)")
|
|
27
|
+
.requiredOption("--execution-started-after <date>", "Execution started after (ISO date, required)")
|
|
28
28
|
.option("--execution-started-before <date>", "Execution started on or before (ISO date)")
|
|
29
29
|
.option("--search <text>", "Case-insensitive partial match on record title, record ID, execution title, or error message")
|
|
30
30
|
.option("--limit <n>", "Limit", "30")
|
|
@@ -89,7 +89,7 @@ export function registerSpanCommands(parent, getApi) {
|
|
|
89
89
|
.option("--node-release-uuid <uuid>", "Release UUID on the node")
|
|
90
90
|
.option("--execution-statuses <pending,success,error>", "Filter by execution statuses")
|
|
91
91
|
.option("--parent-run-uuid <uuid>", "Parent run UUID")
|
|
92
|
-
.requiredOption("--execution-started-after <date>", "Execution started after (ISO date)")
|
|
92
|
+
.requiredOption("--execution-started-after <date>", "Execution started after (ISO date, required)")
|
|
93
93
|
.option("--execution-started-before <date>", "Execution started on or before (ISO date)")
|
|
94
94
|
.action(async (opts) => {
|
|
95
95
|
const api = getApi();
|
|
@@ -8,7 +8,7 @@ export function registerTraceCommands(parent, getApi) {
|
|
|
8
8
|
.description("List traces (aggregated over their spans)")
|
|
9
9
|
.option("--workflow-uuid <uuid>", "Workflow UUID (omit to include all workflows in the workspace)")
|
|
10
10
|
.option("--statuses <pending,error,degraded,success>", "Trace statuses")
|
|
11
|
-
.requiredOption("--started-after <date>", "Trace's first span started after (ISO date)")
|
|
11
|
+
.requiredOption("--started-after <date>", "Trace's first span started after (ISO date, required)")
|
|
12
12
|
.option("--started-before <date>", "Trace's first span started on or before (ISO date)")
|
|
13
13
|
.option("--limit <n>", "Limit", "30")
|
|
14
14
|
.option("--offset <n>", "Offset", "0")
|
|
@@ -31,7 +31,7 @@ export function registerTraceCommands(parent, getApi) {
|
|
|
31
31
|
.description("Count traces matching the filter")
|
|
32
32
|
.option("--workflow-uuid <uuid>", "Workflow UUID (omit to include all workflows in the workspace)")
|
|
33
33
|
.option("--statuses <pending,error,degraded,success>", "Trace statuses")
|
|
34
|
-
.requiredOption("--started-after <date>", "Trace's first span started after (ISO date)")
|
|
34
|
+
.requiredOption("--started-after <date>", "Trace's first span started after (ISO date, required)")
|
|
35
35
|
.option("--started-before <date>", "Trace's first span started on or before (ISO date)")
|
|
36
36
|
.action(async (opts) => {
|
|
37
37
|
const api = getApi();
|
|
@@ -7,6 +7,7 @@ export declare const ExitCodes: {
|
|
|
7
7
|
readonly NotFound: 5;
|
|
8
8
|
};
|
|
9
9
|
export type ExitCode = (typeof ExitCodes)[keyof typeof ExitCodes];
|
|
10
|
+
export declare const colorize: (text: string, code: string) => string;
|
|
10
11
|
export declare const colors: {
|
|
11
12
|
red: (s: string) => string;
|
|
12
13
|
green: (s: string) => string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runHandler.d.ts","sourceRoot":"","sources":["../../src/commands/runHandler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runHandler.d.ts","sourceRoot":"","sources":["../../src/commands/runHandler.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,SAAS;;;;;;;CAOZ,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAOlE,eAAO,MAAM,QAAQ,SAAU,MAAM,QAAQ,MAAM,KAAG,MAKrD,CAAC;AAEF,eAAO,MAAM,MAAM;aACR,MAAM,KAAG,MAAM;eACb,MAAM,KAAG,MAAM;gBACd,MAAM,KAAG,MAAM;cACjB,MAAM,KAAG,MAAM;aAChB,MAAM,KAAG,MAAM;cACd,MAAM,KAAG,MAAM;CAC1B,CAAC;AAEF,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAM/C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAID,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUhE;AAED,KAAK,YAAY,GAAG;IAClB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,KAAK,CAgCpE;AAQD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAmBlE;AAED,MAAM,MAAM,OAAO,GAAG;IAEpB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAGlC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAU5B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAIF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAwBrD;AAgED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,GAAG,CAQhE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ1E;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,OAAO,EAAE,CAAC,EAAE,EACZ,KAAK,EAAE,MAAM,GACZ;IAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CASxD;AAKD,KAAK,iBAAiB,GAAG;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACzB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,wBAAsB,aAAa,CAAC,CAAC,EACnC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,CAAC,CAAC,CAgEZ;AA0ED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,aAAa,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9D,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC,CAyBZ"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { determineIfIsFetcherError } from "@cargo-ai/api";
|
|
2
|
+
import { clipToCells } from "../utils/textWidth.js";
|
|
2
3
|
export const ExitCodes = {
|
|
3
4
|
Success: 0,
|
|
4
5
|
GenericError: 1,
|
|
@@ -10,7 +11,7 @@ export const ExitCodes = {
|
|
|
10
11
|
const useColor = process.stderr.isTTY === true &&
|
|
11
12
|
process.env["NO_COLOR"] === undefined &&
|
|
12
13
|
process.env["CI"] !== "true";
|
|
13
|
-
const colorize = (text, code) => {
|
|
14
|
+
export const colorize = (text, code) => {
|
|
14
15
|
if (useColor !== true) {
|
|
15
16
|
return text;
|
|
16
17
|
}
|
|
@@ -99,10 +100,12 @@ export function clipToWidth(text, columns) {
|
|
|
99
100
|
// One column short of the edge: writing into the last cell makes some
|
|
100
101
|
// terminals wrap to the next row, which is the thing being avoided.
|
|
101
102
|
const limit = width - 1;
|
|
102
|
-
if (limit <= 1
|
|
103
|
+
if (limit <= 1) {
|
|
103
104
|
return text;
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
+
// Counted in columns: an emoji or a CJK character is two of them, and a
|
|
107
|
+
// line that wraps throws off every redraw that assumed one row.
|
|
108
|
+
return clipToCells(text, limit);
|
|
106
109
|
}
|
|
107
110
|
let activeSpinner;
|
|
108
111
|
export function startSpinner(message) {
|
|
@@ -6,7 +6,7 @@ export function registerChangeCommands(parent, getApi) {
|
|
|
6
6
|
.command("list")
|
|
7
7
|
.description("List changes")
|
|
8
8
|
.option("--segment-uuid <uuid>", "Segment UUID")
|
|
9
|
-
.option("--not-empty [true|false]", "Only
|
|
9
|
+
.option("--not-empty [true|false]", "Only changes where a record was added, updated or removed", parseBooleanOption)
|
|
10
10
|
.option("--has-records <added|updated|removed|unchanged>", "Has records of kind")
|
|
11
11
|
.option("--limit <n>", "Limit")
|
|
12
12
|
.option("--offset <n>", "Offset")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../../src/commands/workspaceManagement/user.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAGxC,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../../src/commands/workspaceManagement/user.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAGxC,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,IAAI,CAiN7E"}
|
|
@@ -76,18 +76,21 @@ Example:
|
|
|
76
76
|
.command("upsert-permission")
|
|
77
77
|
.description("Create or update a user permission")
|
|
78
78
|
.option("--user-uuids <uuids>", "Target user UUIDs (comma-separated strings). At least one of --user-uuids or --emails is required")
|
|
79
|
-
.option("--emails <emails>", "Target user emails (comma-separated strings). At least one of --user-uuids or --emails is required")
|
|
80
|
-
.requiredOption("--action <action>", "Permission action (string, required)")
|
|
79
|
+
.option("--emails <emails>", "Target user emails (comma-separated strings). Invites any address that is not a member yet. At least one of --user-uuids or --emails is required")
|
|
80
|
+
.requiredOption("--action <action>", "Permission action, e.g. orchestration:workflow:read (string, required)")
|
|
81
81
|
.requiredOption("--resource-uuid <uuid>", "Resource UUID to grant/deny access to (string, required)")
|
|
82
|
-
.requiredOption("--effect <allow|deny>", "Permission effect")
|
|
82
|
+
.requiredOption("--effect <allow|deny>", "Permission effect. deny applies to existing members only, so it takes --user-uuids")
|
|
83
83
|
.addHelpText("after", `
|
|
84
84
|
Examples:
|
|
85
|
-
$ cargo-ai workspaceManagement user upsert-permission --emails alice@example.com --action read --resource-uuid 550e8400-... --effect allow
|
|
86
|
-
$ cargo-ai workspaceManagement user upsert-permission --user-uuids uuid1,uuid2 --action write --resource-uuid 550e8400-... --effect deny`)
|
|
85
|
+
$ cargo-ai workspaceManagement user upsert-permission --emails alice@example.com --action orchestration:workflow:read --resource-uuid 550e8400-... --effect allow
|
|
86
|
+
$ cargo-ai workspaceManagement user upsert-permission --user-uuids uuid1,uuid2 --action orchestration:workflow:write --resource-uuid 550e8400-... --effect deny`)
|
|
87
87
|
.action(async (opts) => {
|
|
88
88
|
if (opts.userUuids === undefined && opts.emails === undefined) {
|
|
89
89
|
throw new Error("--user-uuids or --emails is required");
|
|
90
90
|
}
|
|
91
|
+
if (opts.effect === "deny" && opts.emails !== undefined) {
|
|
92
|
+
throw new Error("--effect deny applies to existing members only: pass --user-uuids, not --emails");
|
|
93
|
+
}
|
|
91
94
|
const api = getApi();
|
|
92
95
|
const result = await handleApiCall(() => api.workspaceManagement.user.upsertPermission({
|
|
93
96
|
userUuids: opts.userUuids !== undefined
|