@bli-cockpit/cli 0.2.119 → 0.2.122
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/dist/commands/analyze.js +74 -54
- package/dist/commands/brief-rewrite.js +164 -101
- package/dist/commands/brief.js +38 -13
- package/dist/commands/careers.js +81 -9
- package/dist/commands/correct.js +38 -21
- package/dist/commands/docs.js +13 -10
- package/dist/commands/editor.js +59 -30
- package/dist/commands/install-receipts.js +106 -91
- package/dist/commands/local-args-tower-admin.js +4 -0
- package/dist/commands/local-args-tower-cal.js +28 -3
- package/dist/commands/local-args-tower-careers.js +56 -9
- package/dist/commands/local-args-tower-chat.js +55 -28
- package/dist/commands/local-args-tower-docs-msg.js +39 -8
- package/dist/commands/local-args-tower-mail.js +27 -1
- package/dist/commands/local-args-tower-models.js +14 -17
- package/dist/commands/local-args-tower-work.js +37 -6
- package/dist/commands/local-help-commands-tower.js +15 -1
- package/dist/commands/local-help-commands.js +2 -1
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/mcp-stdio-probe.js +92 -73
- package/dist/commands/memory-hook-performance.js +135 -101
- package/dist/commands/memory-install-claude.js +15 -14
- package/dist/commands/memory-install-codex.js +10 -6
- package/dist/commands/memory-install-config.js +5 -4
- package/dist/commands/memory-install-contract.js +56 -10
- package/dist/commands/memory-install-report.js +16 -11
- package/dist/commands/memory-install-skills.js +11 -11
- package/dist/commands/memory-log.js +22 -5
- package/dist/commands/msg.js +11 -5
- package/dist/commands/onboard-setup.js +16 -1
- package/dist/commands/ops-sections.js +89 -0
- package/dist/commands/ops.js +117 -120
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/scout.js +90 -68
- package/dist/commands/session-sync-failures.js +19 -13
- package/dist/commands/session-sync-record.js +53 -52
- package/dist/commands/session-sync-upload.js +15 -11
- package/dist/commands/sessions.js +61 -51
- package/dist/commands/slack.js +90 -61
- package/dist/commands/status.js +53 -41
- package/dist/commands/workbook.js +23 -20
- package/package.json +2 -2
|
@@ -77,12 +77,25 @@ export function parseCalArgs(args) {
|
|
|
77
77
|
"--attendee",
|
|
78
78
|
],
|
|
79
79
|
});
|
|
80
|
+
const { action, rest } = readCalAction(values);
|
|
81
|
+
const { subject, query } = readCalSubjectOrQuery(action, rest);
|
|
82
|
+
const { title, startsAt, endsAt } = readCalCreateDetails(action, values);
|
|
83
|
+
validateCalSharing(action, values);
|
|
84
|
+
// Read in the order the flags have always been checked, so a caller who got
|
|
85
|
+
// two of them wrong is told about --limit first, as before.
|
|
86
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
87
|
+
const offset = readCalOffset(values);
|
|
88
|
+
return buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit);
|
|
89
|
+
}
|
|
90
|
+
function readCalAction(values) {
|
|
80
91
|
const first = values.positionals[0];
|
|
81
92
|
const action = (first === undefined ? "today" : first);
|
|
82
93
|
if (!CAL_ACTIONS.has(action)) {
|
|
83
94
|
throw new Error(`Unknown cal command: ${first}. Try today, week, next, find, calendars, add-ical, create, share, detach, or sync.`);
|
|
84
95
|
}
|
|
85
|
-
|
|
96
|
+
return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
|
|
97
|
+
}
|
|
98
|
+
function readCalSubjectOrQuery(action, rest) {
|
|
86
99
|
let subject;
|
|
87
100
|
let query;
|
|
88
101
|
if (CAL_ACTIONS_NEEDING_A_SUBJECT.has(action)) {
|
|
@@ -100,6 +113,9 @@ export function parseCalArgs(args) {
|
|
|
100
113
|
else if (rest.length > 0) {
|
|
101
114
|
throw new Error(`cal ${action} does not take "${rest[0]}".`);
|
|
102
115
|
}
|
|
116
|
+
return { subject, query };
|
|
117
|
+
}
|
|
118
|
+
function readCalCreateDetails(action, values) {
|
|
103
119
|
const title = optionalNonEmpty(values.flags.get("--title"));
|
|
104
120
|
const startsAt = optionalNonEmpty(values.flags.get("--at"));
|
|
105
121
|
const endsAt = optionalNonEmpty(values.flags.get("--until"));
|
|
@@ -114,15 +130,24 @@ export function parseCalArgs(args) {
|
|
|
114
130
|
if (!endsAt)
|
|
115
131
|
throw new Error("cal create needs --until <when it ends>.");
|
|
116
132
|
}
|
|
117
|
-
|
|
133
|
+
return { title, startsAt, endsAt };
|
|
134
|
+
}
|
|
135
|
+
function validateCalSharing(action, values) {
|
|
136
|
+
if (action === "share" &&
|
|
137
|
+
!values.booleans.has("--org-visible") &&
|
|
138
|
+
!values.booleans.has("--private")) {
|
|
118
139
|
throw new Error("cal share needs --org-visible (every member may read it) or --private (only you). Sharing is a decision, not a default.");
|
|
119
140
|
}
|
|
120
|
-
|
|
141
|
+
}
|
|
142
|
+
function readCalOffset(values) {
|
|
121
143
|
const offsetRaw = optionalNonEmpty(values.flags.get("--offset"));
|
|
122
144
|
const offset = offsetRaw === undefined ? undefined : Number.parseInt(offsetRaw, 10);
|
|
123
145
|
if (offset !== undefined && !Number.isFinite(offset)) {
|
|
124
146
|
throw new Error("--offset takes a whole number of days (today/next) or weeks (week).");
|
|
125
147
|
}
|
|
148
|
+
return offset;
|
|
149
|
+
}
|
|
150
|
+
function buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit) {
|
|
126
151
|
return {
|
|
127
152
|
kind: "cal",
|
|
128
153
|
action,
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from './local-arg-values.js';
|
|
2
|
+
const ACTIONS = ['list', 'show', 'rescreen', 'invite', 'decide', 'takehome-show', 'takehome-set', 'settings-show', 'settings-set'];
|
|
3
|
+
const DECISIONS = ['passed_on', 'archived', 'reopen'];
|
|
4
|
+
/** Verbs that name an application id, a role slug, or nothing at all. */
|
|
5
|
+
const NEEDS_ID = ['show', 'rescreen', 'invite', 'decide'];
|
|
6
|
+
const NEEDS_ROLE = ['takehome-show', 'takehome-set'];
|
|
2
7
|
export function parseCareersArgs(args) {
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const valueFlags = ['--role', '--min-score', '--since', '--home', '--dashboard-url', '--decision', '--stage', '--subject', '--body-file', '--link', '--pass-score', '--account', '--auto-invite-min-score', '--attention-min-score', '--enabled'];
|
|
9
|
+
const values = parseNamedArgs(args, { allowedFlags: [...valueFlags, '--json'], valueFlags });
|
|
10
|
+
const positionals = [...values.positionals];
|
|
11
|
+
// `takehome show` and `settings set` are two words a person types and one
|
|
12
|
+
// action everything downstream reads.
|
|
13
|
+
// A bare `careers takehome` or `careers settings` reads as its `show`, the way
|
|
14
|
+
// `team device` reads as `team device list`: the noun alone is a question.
|
|
15
|
+
const first = positionals[0] ?? 'list';
|
|
16
|
+
const paired = first === 'takehome' || first === 'settings';
|
|
17
|
+
const action = paired
|
|
18
|
+
? `${first}-${positionals.splice(0, positionals[1] === undefined ? 1 : 2)[1] ?? 'show'}`
|
|
19
|
+
: (positionals.shift() ?? 'list');
|
|
20
|
+
if (!ACTIONS.includes(action))
|
|
21
|
+
throw new Error(`careers takes ${ACTIONS.join(', ')}.`);
|
|
22
|
+
const verb = action;
|
|
23
|
+
const target = positionals.shift();
|
|
24
|
+
if (NEEDS_ID.includes(verb) && !target)
|
|
25
|
+
throw new Error(`careers ${verb} needs an application id.`);
|
|
26
|
+
if (NEEDS_ROLE.includes(verb) && !target)
|
|
27
|
+
throw new Error(`careers ${verb.replace('-', ' ')} needs a role slug.`);
|
|
28
|
+
if (positionals.length > 0)
|
|
11
29
|
throw new Error('Unexpected careers argument.');
|
|
12
30
|
const rawScore = values.flags.get('--min-score');
|
|
13
31
|
const minScore = rawScore === undefined ? undefined : Number(rawScore);
|
|
@@ -16,5 +34,34 @@ export function parseCareersArgs(args) {
|
|
|
16
34
|
const since = optionalNonEmpty(values.flags.get('--since'));
|
|
17
35
|
if (since && !Number.isFinite(Date.parse(since)))
|
|
18
36
|
throw new Error('--since must be a date.');
|
|
19
|
-
|
|
37
|
+
const decision = optionalNonEmpty(values.flags.get('--decision'));
|
|
38
|
+
if (verb === 'decide' && (!decision || !DECISIONS.includes(decision)))
|
|
39
|
+
throw new Error(`careers decide needs --decision ${DECISIONS.join('|')}.`);
|
|
40
|
+
const passScore = numberFlag(values.flags.get('--pass-score'), '--pass-score', 0, 10);
|
|
41
|
+
const autoInviteMinScore = numberFlag(values.flags.get('--auto-invite-min-score'), '--auto-invite-min-score', 0, 100);
|
|
42
|
+
const attentionMinScore = numberFlag(values.flags.get('--attention-min-score'), '--attention-min-score', 0, 100);
|
|
43
|
+
const enabledRaw = optionalNonEmpty(values.flags.get('--enabled'));
|
|
44
|
+
if (enabledRaw !== undefined && enabledRaw !== 'true' && enabledRaw !== 'false')
|
|
45
|
+
throw new Error('--enabled must be true or false.');
|
|
46
|
+
return {
|
|
47
|
+
kind: 'careers', action: verb,
|
|
48
|
+
id: NEEDS_ID.includes(verb) ? target : undefined,
|
|
49
|
+
takehomeRole: NEEDS_ROLE.includes(verb) ? target : undefined,
|
|
50
|
+
role: optionalNonEmpty(values.flags.get('--role')), minScore, since: since ? new Date(since).toISOString() : undefined,
|
|
51
|
+
stage: optionalNonEmpty(values.flags.get('--stage')), decision,
|
|
52
|
+
subject: optionalNonEmpty(values.flags.get('--subject')), bodyFile: optionalNonEmpty(values.flags.get('--body-file')),
|
|
53
|
+
link: optionalNonEmpty(values.flags.get('--link')), passScore,
|
|
54
|
+
inviteAccountId: optionalNonEmpty(values.flags.get('--account')),
|
|
55
|
+
autoInviteMinScore, attentionMinScore,
|
|
56
|
+
autoInviteEnabled: enabledRaw === undefined ? undefined : enabledRaw === 'true',
|
|
57
|
+
homeDir: optionalNonEmpty(values.flags.get('--home')), dashboardUrl: optionalUrl(values.flags.get('--dashboard-url')), json: values.booleans.has('--json'),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function numberFlag(raw, flag, min, max) {
|
|
61
|
+
if (raw === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
const value = Number(raw);
|
|
64
|
+
if (!Number.isFinite(value) || value < min || value > max)
|
|
65
|
+
throw new Error(`${flag} must be ${min} to ${max}.`);
|
|
66
|
+
return value;
|
|
20
67
|
}
|
|
@@ -10,7 +10,39 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
12
12
|
export function parseJarvisArgs(args) {
|
|
13
|
-
const values =
|
|
13
|
+
const values = parseJarvisNamedArgs(args);
|
|
14
|
+
const prompt = parseJarvisPrompt(values);
|
|
15
|
+
const thread = parseJarvisThread(values);
|
|
16
|
+
const imagePath = parseJarvisAttachment(values);
|
|
17
|
+
const { threads, history } = parseJarvisReadback(values, prompt, imagePath);
|
|
18
|
+
const date = parseJarvisDate(values, threads, history);
|
|
19
|
+
const trace = parseJarvisTrace(values, threads, history, prompt, imagePath);
|
|
20
|
+
return {
|
|
21
|
+
kind: "jarvis",
|
|
22
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
23
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
24
|
+
prompt,
|
|
25
|
+
subject: optionalNonEmpty(values.flags.get("--as")),
|
|
26
|
+
thread,
|
|
27
|
+
threads,
|
|
28
|
+
history,
|
|
29
|
+
...(trace ? { trace } : {}),
|
|
30
|
+
limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit"),
|
|
31
|
+
// BLI-3381: no client-side allowlist — the dashboard forwards this key
|
|
32
|
+
// to the inference server's own allowlist and relays its refusal.
|
|
33
|
+
model: optionalNonEmpty(values.flags.get("--model")),
|
|
34
|
+
...(date ? { date } : {}),
|
|
35
|
+
imagePath,
|
|
36
|
+
// BLI-3457: streaming is on unless a caller opts out. A dashboard that
|
|
37
|
+
// does not stream yet still answers plain JSON, so this flag is for
|
|
38
|
+
// callers that want the single-body shape on purpose, not a compat knob.
|
|
39
|
+
stream: !values.booleans.has("--no-stream"),
|
|
40
|
+
json: values.booleans.has("--json"),
|
|
41
|
+
showApprovalCode: values.booleans.has("--show-approval-code"),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function parseJarvisNamedArgs(args) {
|
|
45
|
+
return parseNamedArgs(args, {
|
|
14
46
|
allowedFlags: [
|
|
15
47
|
"--home",
|
|
16
48
|
"--dashboard-url",
|
|
@@ -50,15 +82,23 @@ export function parseJarvisArgs(args) {
|
|
|
50
82
|
"--limit",
|
|
51
83
|
],
|
|
52
84
|
});
|
|
85
|
+
}
|
|
86
|
+
function parseJarvisPrompt(values) {
|
|
53
87
|
const flaggedPrompt = optionalNonEmpty(values.flags.get("--prompt"));
|
|
54
88
|
const positionalPrompt = optionalNonEmpty(values.positionals.join(" "));
|
|
55
89
|
if (flaggedPrompt && positionalPrompt) {
|
|
56
90
|
throw new Error("jarvis accepts either --prompt or positional text, not both.");
|
|
57
91
|
}
|
|
92
|
+
return flaggedPrompt ?? positionalPrompt;
|
|
93
|
+
}
|
|
94
|
+
function parseJarvisThread(values) {
|
|
58
95
|
const thread = optionalNonEmpty(values.flags.get("--thread")) ?? "main";
|
|
59
96
|
if (!/^[A-Za-z0-9_-]{1,40}$/.test(thread)) {
|
|
60
97
|
throw new Error("jarvis --thread must use 1 to 40 letters, numbers, underscores, or hyphens.");
|
|
61
98
|
}
|
|
99
|
+
return thread;
|
|
100
|
+
}
|
|
101
|
+
function parseJarvisAttachment(values) {
|
|
62
102
|
// BLI-3414: `--file` is a plain alias for `--image` — same flag, whichever
|
|
63
103
|
// word a person reaches for first.
|
|
64
104
|
const image = optionalNonEmpty(values.flags.get("--image"));
|
|
@@ -66,6 +106,9 @@ export function parseJarvisArgs(args) {
|
|
|
66
106
|
if (image && file) {
|
|
67
107
|
throw new Error("jarvis accepts either --image or --file, not both — they are the same flag.");
|
|
68
108
|
}
|
|
109
|
+
return image ?? file;
|
|
110
|
+
}
|
|
111
|
+
function parseJarvisReadback(values, prompt, imagePath) {
|
|
69
112
|
// BLI-3458. Reading history and asking a question are different acts, and a
|
|
70
113
|
// command that quietly did one while you asked for the other would be worse
|
|
71
114
|
// than a refusal — `--threads` with a question would silently drop the
|
|
@@ -75,12 +118,15 @@ export function parseJarvisArgs(args) {
|
|
|
75
118
|
if (threads && history) {
|
|
76
119
|
throw new Error("jarvis --threads lists every thread; --history replays one. Pass one, not both.");
|
|
77
120
|
}
|
|
78
|
-
if ((threads || history) &&
|
|
121
|
+
if ((threads || history) && prompt) {
|
|
79
122
|
throw new Error("jarvis --threads and --history read back what was already said; they do not take a question.");
|
|
80
123
|
}
|
|
81
|
-
if ((threads || history) &&
|
|
124
|
+
if ((threads || history) && imagePath) {
|
|
82
125
|
throw new Error("jarvis --threads and --history do not take an attachment.");
|
|
83
126
|
}
|
|
127
|
+
return { threads, history };
|
|
128
|
+
}
|
|
129
|
+
function parseJarvisDate(values, threads, history) {
|
|
84
130
|
// BLI-3484. `--date` binds the page that was live on one of the subject's
|
|
85
131
|
// days, so a turn can be about Sunday's page. Refused on the two reading
|
|
86
132
|
// commands for the same reason an attachment is: they replay what was said
|
|
@@ -89,6 +135,9 @@ export function parseJarvisArgs(args) {
|
|
|
89
135
|
if ((threads || history) && date) {
|
|
90
136
|
throw new Error("jarvis --threads and --history replay what was said; they bind no page.");
|
|
91
137
|
}
|
|
138
|
+
return date;
|
|
139
|
+
}
|
|
140
|
+
function parseJarvisTrace(values, threads, history, prompt, imagePath) {
|
|
92
141
|
// BLI-3560. `--trace` reads back what a turn DID, the same class of act as
|
|
93
142
|
// `--threads` and `--history`, and refused alongside them for the same
|
|
94
143
|
// reason: a command that quietly asked a question while you asked to see one
|
|
@@ -97,38 +146,16 @@ export function parseJarvisArgs(args) {
|
|
|
97
146
|
if (trace && (threads || history)) {
|
|
98
147
|
throw new Error("jarvis --trace shows one turn's steps; --threads and --history replay what was said. Pass one.");
|
|
99
148
|
}
|
|
100
|
-
if (trace &&
|
|
149
|
+
if (trace && prompt) {
|
|
101
150
|
throw new Error("jarvis --trace shows a turn that already ran; it does not take a question.");
|
|
102
151
|
}
|
|
103
|
-
if (trace &&
|
|
152
|
+
if (trace && imagePath) {
|
|
104
153
|
throw new Error("jarvis --trace does not take an attachment.");
|
|
105
154
|
}
|
|
106
155
|
if (trace !== undefined && !/^(last|[A-Za-z0-9_-]{1,64})$/.test(trace)) {
|
|
107
156
|
throw new Error("jarvis --trace takes `last` or a trace id (up to 64 letters, numbers, underscores or hyphens).");
|
|
108
157
|
}
|
|
109
|
-
return
|
|
110
|
-
kind: "jarvis",
|
|
111
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
112
|
-
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
113
|
-
prompt: flaggedPrompt ?? positionalPrompt,
|
|
114
|
-
subject: optionalNonEmpty(values.flags.get("--as")),
|
|
115
|
-
thread,
|
|
116
|
-
threads,
|
|
117
|
-
history,
|
|
118
|
-
...(trace ? { trace } : {}),
|
|
119
|
-
limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit"),
|
|
120
|
-
// BLI-3381: no client-side allowlist — the dashboard forwards this key
|
|
121
|
-
// to the inference server's own allowlist and relays its refusal.
|
|
122
|
-
model: optionalNonEmpty(values.flags.get("--model")),
|
|
123
|
-
...(date ? { date } : {}),
|
|
124
|
-
imagePath: image ?? file,
|
|
125
|
-
// BLI-3457: streaming is on unless a caller opts out. A dashboard that
|
|
126
|
-
// does not stream yet still answers plain JSON, so this flag is for
|
|
127
|
-
// callers that want the single-body shape on purpose, not a compat knob.
|
|
128
|
-
stream: !values.booleans.has("--no-stream"),
|
|
129
|
-
json: values.booleans.has("--json"),
|
|
130
|
-
showApprovalCode: values.booleans.has("--show-approval-code"),
|
|
131
|
-
};
|
|
158
|
+
return trace;
|
|
132
159
|
}
|
|
133
160
|
/**
|
|
134
161
|
* `cockpit correct` (BLI-3458) — say that one line on the page is wrong.
|
|
@@ -27,23 +27,33 @@ export function parseDocsArgs(args) {
|
|
|
27
27
|
],
|
|
28
28
|
valueFlags: ["--home", "--dashboard-url", "--title", "--parent", "--visibility", "--file", "--query", "--limit"],
|
|
29
29
|
});
|
|
30
|
-
const
|
|
30
|
+
const { action, rest } = parseDocsAction(values.positionals);
|
|
31
|
+
const docRef = parseDocsDocumentReference(action, rest);
|
|
32
|
+
const { visibility, clearParent, parentId, allowEmpty, title, query, limit } = parseDocsFlags(action, values);
|
|
33
|
+
return buildDocsCommand(values, action, docRef, visibility, clearParent, parentId, allowEmpty, title, query, limit);
|
|
34
|
+
}
|
|
35
|
+
function parseDocsAction(positionals) {
|
|
36
|
+
const first = positionals[0];
|
|
31
37
|
const action = (first === undefined ? "list" : first);
|
|
32
38
|
if (!DOCS_ACTIONS.has(action)) {
|
|
33
39
|
throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, or update.`);
|
|
34
40
|
}
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
|
|
42
|
+
}
|
|
43
|
+
function parseDocsDocumentReference(action, rest) {
|
|
37
44
|
if (DOCS_ACTIONS_NEEDING_A_DOC.has(action)) {
|
|
38
|
-
docRef = optionalNonEmpty(rest[0]);
|
|
45
|
+
const docRef = optionalNonEmpty(rest[0]);
|
|
39
46
|
if (!docRef)
|
|
40
47
|
throw new Error(`docs ${action} needs a document id or slug.`);
|
|
41
48
|
if (rest.length > 1)
|
|
42
49
|
throw new Error(`docs ${action} takes one document reference, not ${rest.length}.`);
|
|
50
|
+
return docRef;
|
|
43
51
|
}
|
|
44
|
-
|
|
52
|
+
if (rest.length > 0)
|
|
45
53
|
throw new Error(`docs ${action} does not take "${rest[0]}".`);
|
|
46
|
-
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
function parseDocsFlags(action, values) {
|
|
47
57
|
const visibility = optionalNonEmpty(values.flags.get("--visibility"));
|
|
48
58
|
if (visibility && visibility !== "org" && visibility !== "private") {
|
|
49
59
|
throw new Error('docs --visibility must be "org" or "private".');
|
|
@@ -80,6 +90,9 @@ export function parseDocsArgs(args) {
|
|
|
80
90
|
if (limit !== undefined && action !== "list") {
|
|
81
91
|
throw new Error("--limit belongs to `cockpit docs list`.");
|
|
82
92
|
}
|
|
93
|
+
return { visibility, clearParent, parentId, allowEmpty, title, query, limit };
|
|
94
|
+
}
|
|
95
|
+
function buildDocsCommand(values, action, docRef, visibility, clearParent, parentId, allowEmpty, title, query, limit) {
|
|
83
96
|
return {
|
|
84
97
|
kind: "docs",
|
|
85
98
|
action,
|
|
@@ -137,12 +150,21 @@ export function parseMsgArgs(args) {
|
|
|
137
150
|
],
|
|
138
151
|
valueFlags: ["--home", "--dashboard-url", "--channel", "--thread", "--limit", "--members", "--description"],
|
|
139
152
|
});
|
|
140
|
-
const
|
|
153
|
+
const { action, rest } = parseMsgAction(values.positionals);
|
|
154
|
+
const { channelName, dmEmail } = parseMsgCreateTarget(action, rest);
|
|
155
|
+
const { isPrivate, memberEmails, description } = parseMsgCreateFlags(action, values);
|
|
156
|
+
const { channelRef, threadId, limit } = parseMsgReadTarget(action, rest, values);
|
|
157
|
+
return buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit);
|
|
158
|
+
}
|
|
159
|
+
function parseMsgAction(positionals) {
|
|
160
|
+
const first = positionals[0];
|
|
141
161
|
const action = (first === undefined ? "channels" : first);
|
|
142
162
|
if (!MSG_ACTIONS.has(action)) {
|
|
143
163
|
throw new Error(`Unknown msg command: ${first}. Try channels, read, send, thread, create, or dm.`);
|
|
144
164
|
}
|
|
145
|
-
|
|
165
|
+
return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
|
|
166
|
+
}
|
|
167
|
+
function parseMsgCreateTarget(action, rest) {
|
|
146
168
|
let channelName;
|
|
147
169
|
let dmEmail;
|
|
148
170
|
if (action === "create") {
|
|
@@ -167,6 +189,9 @@ export function parseMsgArgs(args) {
|
|
|
167
189
|
if (!dmEmail.includes("@"))
|
|
168
190
|
throw new Error(`msg dm takes an email address; "${dmEmail}" is not one.`);
|
|
169
191
|
}
|
|
192
|
+
return { channelName, dmEmail };
|
|
193
|
+
}
|
|
194
|
+
function parseMsgCreateFlags(action, values) {
|
|
170
195
|
const isPrivate = values.booleans.has("--private");
|
|
171
196
|
if (isPrivate && action !== "create") {
|
|
172
197
|
throw new Error("--private belongs to `cockpit msg create`.");
|
|
@@ -179,6 +204,9 @@ export function parseMsgArgs(args) {
|
|
|
179
204
|
if (description && action !== "create") {
|
|
180
205
|
throw new Error("--description belongs to `cockpit msg create`.");
|
|
181
206
|
}
|
|
207
|
+
return { isPrivate, memberEmails, description };
|
|
208
|
+
}
|
|
209
|
+
function parseMsgReadTarget(action, rest, values) {
|
|
182
210
|
let channelRef;
|
|
183
211
|
if (MSG_ACTIONS_NEEDING_A_CHANNEL.has(action)) {
|
|
184
212
|
channelRef = optionalNonEmpty(rest[0]);
|
|
@@ -207,6 +235,9 @@ export function parseMsgArgs(args) {
|
|
|
207
235
|
if (limit !== undefined && action !== "read" && action !== "thread") {
|
|
208
236
|
throw new Error("--limit belongs to `cockpit msg read` or `cockpit msg thread`.");
|
|
209
237
|
}
|
|
238
|
+
return { channelRef, threadId, limit };
|
|
239
|
+
}
|
|
240
|
+
function buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit) {
|
|
210
241
|
return {
|
|
211
242
|
kind: "msg",
|
|
212
243
|
action,
|
|
@@ -70,12 +70,23 @@ export function parseMailArgs(args) {
|
|
|
70
70
|
"--out",
|
|
71
71
|
],
|
|
72
72
|
});
|
|
73
|
+
const { action, rest } = readMailAction(values);
|
|
74
|
+
const { subject, query } = readMailSubjectOrQuery(action, rest);
|
|
75
|
+
const address = readImapAddress(action, values);
|
|
76
|
+
const { to, cc } = readMailRecipients(action, values);
|
|
77
|
+
const outPath = readAttachmentPath(action, values);
|
|
78
|
+
const limit = readMailLimit(action, values);
|
|
79
|
+
return buildMailCommand(values, action, subject, query, address, to, cc, outPath, limit);
|
|
80
|
+
}
|
|
81
|
+
function readMailAction(values) {
|
|
73
82
|
const first = values.positionals[0];
|
|
74
83
|
const action = (first === undefined ? "inbox" : first);
|
|
75
84
|
if (!MAIL_ACTIONS.has(action)) {
|
|
76
85
|
throw new Error(`Unknown mail command: ${first}. Try accounts, add-imap, detach, inbox, read, search, send, attachment, or sync.`);
|
|
77
86
|
}
|
|
78
|
-
|
|
87
|
+
return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
|
|
88
|
+
}
|
|
89
|
+
function readMailSubjectOrQuery(action, rest) {
|
|
79
90
|
let subject;
|
|
80
91
|
let query;
|
|
81
92
|
if (MAIL_ACTIONS_NEEDING_A_SUBJECT.has(action)) {
|
|
@@ -96,10 +107,16 @@ export function parseMailArgs(args) {
|
|
|
96
107
|
else if (rest.length > 0) {
|
|
97
108
|
throw new Error(`mail ${action} does not take "${rest[0]}".`);
|
|
98
109
|
}
|
|
110
|
+
return { subject, query };
|
|
111
|
+
}
|
|
112
|
+
function readImapAddress(action, values) {
|
|
99
113
|
const address = optionalNonEmpty(values.flags.get("--address"));
|
|
100
114
|
if (action === "add-imap" && !address) {
|
|
101
115
|
throw new Error("mail add-imap needs --address, and reads the app password from stdin.");
|
|
102
116
|
}
|
|
117
|
+
return address;
|
|
118
|
+
}
|
|
119
|
+
function readMailRecipients(action, values) {
|
|
103
120
|
const to = splitAddresses(values.flags.get("--to"));
|
|
104
121
|
const cc = splitAddresses(values.flags.get("--cc"));
|
|
105
122
|
if (action === "send" && to.length === 0) {
|
|
@@ -108,14 +125,23 @@ export function parseMailArgs(args) {
|
|
|
108
125
|
if (action === "send" && !optionalNonEmpty(values.flags.get("--account"))) {
|
|
109
126
|
throw new Error("mail send needs --account <id>: with several mailboxes attached, which address this goes out from is yours to say. `cockpit mail accounts` lists them.");
|
|
110
127
|
}
|
|
128
|
+
return { to, cc };
|
|
129
|
+
}
|
|
130
|
+
function readAttachmentPath(action, values) {
|
|
111
131
|
const outPath = optionalNonEmpty(values.flags.get("--out"));
|
|
112
132
|
if (action === "attachment" && !outPath) {
|
|
113
133
|
throw new Error("mail attachment needs --out <path>: the file is written to disk, never to stdout.");
|
|
114
134
|
}
|
|
135
|
+
return outPath;
|
|
136
|
+
}
|
|
137
|
+
function readMailLimit(action, values) {
|
|
115
138
|
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
116
139
|
if (limit !== undefined && action !== "inbox" && action !== "search") {
|
|
117
140
|
throw new Error("--limit belongs to `cockpit mail inbox` or `cockpit mail search`.");
|
|
118
141
|
}
|
|
142
|
+
return limit;
|
|
143
|
+
}
|
|
144
|
+
function buildMailCommand(values, action, subject, query, address, to, cc, outPath, limit) {
|
|
119
145
|
return {
|
|
120
146
|
kind: "mail",
|
|
121
147
|
action,
|
|
@@ -28,23 +28,7 @@ export function parseModelsArgs(args) {
|
|
|
28
28
|
// `compare` is the one verb that takes several ids, so it is parsed before
|
|
29
29
|
// the at-most-two rule the other two live under (BLI-3919).
|
|
30
30
|
if (values.positionals[0] === "compare") {
|
|
31
|
-
|
|
32
|
-
const unique = [...new Set(modelIds)];
|
|
33
|
-
if (unique.length < COMPARE_MIN) {
|
|
34
|
-
throw new Error(`models compare needs at least ${COMPARE_MIN} model ids, e.g. ` +
|
|
35
|
-
"`models compare openai:gpt-5.6-luna openai:gpt-5.6-terra`.");
|
|
36
|
-
}
|
|
37
|
-
if (unique.length > COMPARE_MAX) {
|
|
38
|
-
throw new Error(`models compare holds at most ${COMPARE_MAX} models; ${unique.length} named. ` +
|
|
39
|
-
"A fifth column stops being readable.");
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
kind: "models",
|
|
43
|
-
action: "compare",
|
|
44
|
-
modelIds: unique,
|
|
45
|
-
highlight: values.booleans.has("--highlight"),
|
|
46
|
-
...base,
|
|
47
|
-
};
|
|
31
|
+
return parseModelComparison(values.positionals, values.booleans.has("--highlight"), base);
|
|
48
32
|
}
|
|
49
33
|
if (values.booleans.has("--highlight")) {
|
|
50
34
|
throw new Error("--highlight only means something for `models compare`.");
|
|
@@ -69,4 +53,17 @@ export function parseModelsArgs(args) {
|
|
|
69
53
|
throw new Error("models show needs a model id, e.g. `models show openai:gpt-5.6-terra`.");
|
|
70
54
|
}
|
|
71
55
|
return { kind: "models", action: "show", modelId, ...base };
|
|
56
|
+
}
|
|
57
|
+
function parseModelComparison(positionals, highlight, base) {
|
|
58
|
+
const modelIds = positionals.slice(1).map((id) => id.trim()).filter((id) => id.length > 0);
|
|
59
|
+
const unique = [...new Set(modelIds)];
|
|
60
|
+
if (unique.length < COMPARE_MIN) {
|
|
61
|
+
throw new Error(`models compare needs at least ${COMPARE_MIN} model ids, e.g. ` +
|
|
62
|
+
"`models compare openai:gpt-5.6-luna openai:gpt-5.6-terra`.");
|
|
63
|
+
}
|
|
64
|
+
if (unique.length > COMPARE_MAX) {
|
|
65
|
+
throw new Error(`models compare holds at most ${COMPARE_MAX} models; ${unique.length} named. ` +
|
|
66
|
+
"A fifth column stops being readable.");
|
|
67
|
+
}
|
|
68
|
+
return { kind: "models", action: "compare", modelIds: unique, highlight, ...base };
|
|
72
69
|
}
|
|
@@ -72,12 +72,25 @@ export function parseIssueArgs(args) {
|
|
|
72
72
|
"--file",
|
|
73
73
|
],
|
|
74
74
|
});
|
|
75
|
+
const { action, rest } = readIssueAction(values);
|
|
76
|
+
const { issueRef, moveState } = readIssueReference(action, rest);
|
|
77
|
+
validateIssueState(moveState, "issue move state must be one of: ");
|
|
78
|
+
const stateFilter = readIssueStateFilter(action, values);
|
|
79
|
+
const title = readIssueTitle(action, values);
|
|
80
|
+
const priority = readIssuePriority(values);
|
|
81
|
+
const assignee = readIssueAssignee(action, values);
|
|
82
|
+
const limit = readIssueLimit(action, values);
|
|
83
|
+
return buildIssueCommand(values, action, issueRef, moveState, stateFilter, title, priority, assignee, limit);
|
|
84
|
+
}
|
|
85
|
+
function readIssueAction(values) {
|
|
75
86
|
const first = values.positionals[0];
|
|
76
87
|
const action = (first === undefined ? "list" : first);
|
|
77
88
|
if (!ISSUE_ACTIONS.has(action)) {
|
|
78
89
|
throw new Error(`Unknown issue command: ${first}. Try list, show, create, update, move, comment, or history.`);
|
|
79
90
|
}
|
|
80
|
-
|
|
91
|
+
return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
|
|
92
|
+
}
|
|
93
|
+
function readIssueReference(action, rest) {
|
|
81
94
|
let issueRef;
|
|
82
95
|
let moveState;
|
|
83
96
|
if (ISSUE_ACTIONS_NEEDING_AN_ISSUE.has(action)) {
|
|
@@ -99,19 +112,28 @@ export function parseIssueArgs(args) {
|
|
|
99
112
|
else if (rest.length > 0) {
|
|
100
113
|
throw new Error(`issue ${action} does not take "${rest[0]}".`);
|
|
101
114
|
}
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
return { issueRef, moveState };
|
|
116
|
+
}
|
|
117
|
+
function validateIssueState(state, errorPrefix) {
|
|
118
|
+
if (state && !ISSUE_STATES.includes(state)) {
|
|
119
|
+
throw new Error(`${errorPrefix}${ISSUE_STATES.join(", ")}.`);
|
|
104
120
|
}
|
|
121
|
+
}
|
|
122
|
+
function readIssueStateFilter(action, values) {
|
|
105
123
|
const stateFilter = optionalNonEmpty(values.flags.get("--state"));
|
|
106
|
-
|
|
107
|
-
throw new Error(`--state must be one of: ${ISSUE_STATES.join(", ")}.`);
|
|
108
|
-
}
|
|
124
|
+
validateIssueState(stateFilter, "--state must be one of: ");
|
|
109
125
|
if (stateFilter !== undefined && action !== "list") {
|
|
110
126
|
throw new Error("--state filters `cockpit issue list`; to move an issue use `cockpit issue move <id> <state>`.");
|
|
111
127
|
}
|
|
128
|
+
return stateFilter;
|
|
129
|
+
}
|
|
130
|
+
function readIssueTitle(action, values) {
|
|
112
131
|
const title = optionalNonEmpty(values.flags.get("--title"));
|
|
113
132
|
if (action === "create" && !title)
|
|
114
133
|
throw new Error("issue create needs --title.");
|
|
134
|
+
return title;
|
|
135
|
+
}
|
|
136
|
+
function readIssuePriority(values) {
|
|
115
137
|
// Not `optionalPositiveInteger`: 0 is a legitimate priority ("none", the
|
|
116
138
|
// column default), and that helper refuses it as non-positive.
|
|
117
139
|
const priorityRaw = optionalNonEmpty(values.flags.get("--priority"));
|
|
@@ -122,6 +144,9 @@ export function parseIssueArgs(args) {
|
|
|
122
144
|
throw new Error("--priority must be 0-4 (0 none, 1 urgent, 2 high, 3 medium, 4 low).");
|
|
123
145
|
}
|
|
124
146
|
}
|
|
147
|
+
return priority;
|
|
148
|
+
}
|
|
149
|
+
function readIssueAssignee(action, values) {
|
|
125
150
|
// An assignee is `me`, `unassigned`, or a person's uuid. A name or an
|
|
126
151
|
// email is refused HERE rather than travelling to Postgres as a malformed
|
|
127
152
|
// uuid, which would come back as a 500 that names nothing useful.
|
|
@@ -132,10 +157,16 @@ export function parseIssueArgs(args) {
|
|
|
132
157
|
if (assignee === "unassigned" && action !== "list") {
|
|
133
158
|
throw new Error('--assignee unassigned filters `cockpit issue list`; to clear an assignee pass --assignee "" is not supported yet.');
|
|
134
159
|
}
|
|
160
|
+
return assignee;
|
|
161
|
+
}
|
|
162
|
+
function readIssueLimit(action, values) {
|
|
135
163
|
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
136
164
|
if (limit !== undefined && action !== "list" && action !== "history") {
|
|
137
165
|
throw new Error("--limit belongs to `cockpit issue list` or `cockpit issue history`.");
|
|
138
166
|
}
|
|
167
|
+
return limit;
|
|
168
|
+
}
|
|
169
|
+
function buildIssueCommand(values, action, issueRef, moveState, stateFilter, title, priority, assignee, limit) {
|
|
139
170
|
return {
|
|
140
171
|
kind: "issue",
|
|
141
172
|
action,
|
|
@@ -22,7 +22,21 @@ const SEARCH_KIND_LIST = SEARCH_KINDS.join(",");
|
|
|
22
22
|
const SEARCH_CORPORA_COUNT = SEARCH_KINDS.length;
|
|
23
23
|
/** One entry per Tower noun, in the order `cockpit --help` lists them. */
|
|
24
24
|
export const TOWER_COMMAND_HELP = [
|
|
25
|
-
[
|
|
25
|
+
[
|
|
26
|
+
"careers",
|
|
27
|
+
[
|
|
28
|
+
"Usage: cockpit careers [list|show <id>|rescreen <id>|invite <id>|decide <id> --decision <d>|takehome show <role>|takehome set <role>|settings show|settings set] [flags]",
|
|
29
|
+
"",
|
|
30
|
+
"Super-admin application review and pipeline. Lists at most 100 matches with total and has_more; use filters to narrow.",
|
|
31
|
+
"list [--role <slug>] [--min-score <n>] [--since <date>] [--stage <screened|invited|submitted|graded|booking_sent|passed_on|archived>] — the board, newest first, with the two thresholds in force.",
|
|
32
|
+
"invite <id> — sends that role's take-home from the configured mailbox, whatever the screening scored. Only a row still at `screened`.",
|
|
33
|
+
"decide <id> --decision <passed_on|archived|reopen> — your own call on a row; `reopen` puts it back to `screened`.",
|
|
34
|
+
"takehome show <role> — the subject, body, link and the reviewer's rubric for that role.",
|
|
35
|
+
"takehome set <role> [--subject <s>] [--body-file <path>] [--link <url>] [--pass-score <0-10>] — the body comes from a FILE and must keep {first_name} and {link}.",
|
|
36
|
+
"settings show — the bar, the attention threshold, whether automatic sending is on, and which mailbox it goes out from.",
|
|
37
|
+
"settings set [--enabled true|false] [--auto-invite-min-score <0-100>] [--attention-min-score <0-100>] [--account <mailbox uuid>] — each takes effect on the next screening, no deploy.",
|
|
38
|
+
],
|
|
39
|
+
],
|
|
26
40
|
[
|
|
27
41
|
"docs",
|
|
28
42
|
[
|
|
@@ -327,12 +327,13 @@ export function localSubcommandHelp(command) {
|
|
|
327
327
|
[
|
|
328
328
|
"ops",
|
|
329
329
|
[
|
|
330
|
-
"Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] [--models] | recompile --person <email|name|id> [--dry-run]] [--json]",
|
|
330
|
+
"Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] [--models] [--turns] [--tool-router] | recompile --person <email|name|id> [--dry-run]] [--json]",
|
|
331
331
|
"",
|
|
332
332
|
" cockpit ops status",
|
|
333
333
|
" One line per scheduled job: when it last produced something, and whether that",
|
|
334
334
|
" is late FOR THAT JOB. A once-daily job quiet for 18 hours reads ok; a",
|
|
335
335
|
" quarter-hourly one does not. Every line names the schedule it is judged against.",
|
|
336
|
+
" --turns adds 30-day JARVIS turn aggregates; --tool-router adds the shadow-router agreement board.",
|
|
336
337
|
" A job that is not ok also prints what its artifact does and does not prove —",
|
|
337
338
|
" several only write a row when there is something new, so quiet can mean a quiet",
|
|
338
339
|
" week rather than a broken cron.",
|
|
@@ -104,7 +104,7 @@ export function localCommandHelp(command) {
|
|
|
104
104
|
" cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
|
|
105
105
|
` cockpit search "<words>" [--kind ${SEARCH_KINDS.join(",")}] [--limit <n>] [--dashboard-url <url>] [--json]`,
|
|
106
106
|
" cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
107
|
-
" cockpit careers [list|show <id>|rescreen <id>] [--role <slug>] [--min-score <n>] [--since <date>] [--json]",
|
|
107
|
+
" cockpit careers [list|show <id>|rescreen <id>|invite <id>|decide <id> --decision <passed_on|archived|reopen>|takehome show <role>|takehome set <role> --body-file <path>|settings show|settings set --enabled <true|false>] [--role <slug>] [--min-score <n>] [--since <date>] [--stage <s>] [--json]",
|
|
108
108
|
" cockpit usage sessions [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--since <window>] [--json]",
|
|
109
109
|
" cockpit usage sessions --session <id> [--from-window <n>] [--json]",
|
|
110
110
|
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|