@bli-cockpit/cli 0.2.212 → 0.2.217
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/agent-door.js +5 -3
- package/dist/commands/local-args-tower-admin.js +8 -1
- package/dist/commands/local-args-tower-manager.js +371 -0
- package/dist/commands/local-args-tower.js +1 -0
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-command-shapes-manager.js +9 -0
- package/dist/commands/local-help-commands-tower-ops.js +41 -0
- package/dist/commands/local-help-commands-tower.js +1 -1
- package/dist/commands/local-help-commands.js +3 -0
- package/dist/commands/local-help.js +2 -0
- package/dist/commands/local.js +8 -12
- package/dist/commands/manager-render.js +168 -0
- package/dist/commands/manager-requests.js +100 -0
- package/dist/commands/manager.js +211 -0
- package/dist/commands/notes-accounts.js +48 -60
- package/dist/commands/notes-sync-loop.js +129 -0
- package/dist/commands/notes-wisprflow-login.js +156 -187
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/team.js +32 -0
- package/package.json +2 -2
|
@@ -52,6 +52,7 @@ export async function askAgentDoor(door, options) {
|
|
|
52
52
|
httpStatus: status,
|
|
53
53
|
reason: doorReason(body) ?? `http_${status}`,
|
|
54
54
|
detail: doorMessage(body) ?? `Tower answered ${status} and said nothing about why.`,
|
|
55
|
+
body,
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
return { ok: true, httpStatus: result.response.status, body };
|
|
@@ -76,10 +77,11 @@ export function emitAgentDoor(door, body, exitCode = 0) {
|
|
|
76
77
|
writeLine(door.io.stdout, JSON.stringify(body));
|
|
77
78
|
return exitCode;
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
/** `extra` is for a command that has more to say than the reason (`refusal`); a caller that passes none prints exactly what it always did. */
|
|
81
|
+
export function failAgentDoor(door, tag, reason, detail, extra = {}) {
|
|
82
|
+
writeLine(door.io.stderr, `${tag} refused ${JSON.stringify({ reason, ...extra })}`);
|
|
81
83
|
if (door.json) {
|
|
82
|
-
writeLine(door.io.stdout, JSON.stringify({ ok: false, error: reason, detail }));
|
|
84
|
+
writeLine(door.io.stdout, JSON.stringify({ ok: false, error: reason, detail, ...extra }));
|
|
83
85
|
}
|
|
84
86
|
else {
|
|
85
87
|
writeLine(door.io.stderr, detail);
|
|
@@ -347,6 +347,13 @@ export function parseTeamArgs(args) {
|
|
|
347
347
|
*/
|
|
348
348
|
function parseTeamDeviceArgs(rest, flags, common) {
|
|
349
349
|
const [deviceVerb, deviceRef, ...extra] = rest;
|
|
350
|
+
if (deviceVerb === "scope") {
|
|
351
|
+
const [deviceScope, ...surplus] = extra;
|
|
352
|
+
if (!deviceRef || (deviceScope !== "notetaker_bot" && deviceScope !== "none") || surplus.length > 0) {
|
|
353
|
+
throw new Error("team device scope takes <id|name> notetaker_bot|none.");
|
|
354
|
+
}
|
|
355
|
+
return { kind: "team", action: "device-scope", deviceRef, deviceScope, ...common };
|
|
356
|
+
}
|
|
350
357
|
if (extra.length > 0)
|
|
351
358
|
throw new Error("team device does not accept that many arguments.");
|
|
352
359
|
if (!deviceVerb || deviceVerb === "list") {
|
|
@@ -378,7 +385,7 @@ function parseTeamDeviceArgs(rest, flags, common) {
|
|
|
378
385
|
...common,
|
|
379
386
|
};
|
|
380
387
|
}
|
|
381
|
-
throw new Error("team device takes one verb: list, doctor <id|name>, or revoke <id|name>.");
|
|
388
|
+
throw new Error("team device takes one verb: list, doctor <id|name>, scope <id|name> notetaker_bot|none, or revoke <id|name>.");
|
|
382
389
|
}
|
|
383
390
|
/** `cockpit model [show|set <key>]` — the alias surface for the personal chat model. */
|
|
384
391
|
export function parseModelArgs(args) {
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit manager` argument parsing (BLI-5006): client actions and timeline items from the terminal.
|
|
3
|
+
*
|
|
4
|
+
* cockpit manager clients
|
|
5
|
+
* cockpit manager client <client>
|
|
6
|
+
* cockpit manager run <run-id>
|
|
7
|
+
* cockpit manager list <client> [--timeline] [--needs-review] [--status open|done|dropped|closed|all] [--owner <name> | --no-owner] [--overdue] [--kind <kind>] [--search <text>] [--archived] [--review dismissed|all]
|
|
8
|
+
* cockpit manager queue <client>
|
|
9
|
+
* cockpit manager show <id> [--timeline]
|
|
10
|
+
* cockpit manager add <client> --title <t> [--owners "A, B"] [--due YYYY-MM-DD] [--meeting <timeline-id>] [--detail <text>] [--source-url <url>] [--source-note <text>] [--request-id <uuid>]
|
|
11
|
+
* cockpit manager add <client> --timeline --kind <kind> --title <t> [--date YYYY-MM-DD] [--owner <one name>] [--detail <text>] [--source-url <url>] [--source-note <text>] [--request-id <uuid>]
|
|
12
|
+
* cockpit manager edit <id> --version <v> [--timeline] [--title] [--detail] [--due <date>|none] [--owners "A, B"|none]
|
|
13
|
+
* (timeline: --kind, --date <date>|none, --owner <one name>|none)
|
|
14
|
+
* cockpit manager done|reopen <action> --version <v>
|
|
15
|
+
* cockpit manager link <action> --version <v> --meeting <timeline-id> | --none
|
|
16
|
+
* cockpit manager approve <id> --version <v> [--timeline] [--restore] (--restore brings a dismissed item back)
|
|
17
|
+
* cockpit manager dismiss|archive|unarchive <id> --version <v> [--timeline]
|
|
18
|
+
* cockpit manager proposal <id> <proposal-id> accept|dismiss|add-new --version <v> [--timeline]
|
|
19
|
+
*
|
|
20
|
+
* Every verb is one `/api/manager` door on this machine's device token. A bare `cockpit manager`,
|
|
21
|
+
* `help` and `--help` are the help, never a list and never another command.
|
|
22
|
+
*/
|
|
23
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
24
|
+
export const MANAGER_VERBS = [
|
|
25
|
+
"clients", "client", "run", "list", "queue", "show", "add", "edit", "done", "reopen", "link",
|
|
26
|
+
"approve", "dismiss", "proposal", "archive", "unarchive",
|
|
27
|
+
];
|
|
28
|
+
export const MANAGER_ACTION_STATUSES = ["open", "done", "dropped", "closed", "all"];
|
|
29
|
+
export const MANAGER_TIMELINE_KINDS = ["meeting", "document", "milestone", "internal"];
|
|
30
|
+
const ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
31
|
+
const VERSION = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
32
|
+
const ONE_VERB = `manager takes one verb: ${MANAGER_VERBS.join(", ")}. Example: \`cockpit manager clients\` or \`cockpit manager list <client>\`.`;
|
|
33
|
+
const OWN_FLAGS = [
|
|
34
|
+
"--timeline", "--needs-review", "--status", "--owner", "--owners", "--no-owner", "--kind", "--search", "--archived", "--review",
|
|
35
|
+
"--title", "--detail", "--due", "--date", "--meeting", "--none", "--version", "--request-id", "--restore", "--overdue", "--source-url", "--source-note",
|
|
36
|
+
];
|
|
37
|
+
const COMMON_FLAGS = ["--home", "--dashboard-url", "--json", "--help"];
|
|
38
|
+
const VALUE_FLAGS = [
|
|
39
|
+
"--status", "--owner", "--owners", "--kind", "--search", "--review", "--title", "--detail", "--due", "--date", "--meeting",
|
|
40
|
+
"--version", "--request-id", "--source-url", "--source-note", "--home", "--dashboard-url",
|
|
41
|
+
];
|
|
42
|
+
/** Which flags each verb owns, so a flag on the wrong verb is refused by name rather than ignored. */
|
|
43
|
+
export const FLAGS_BY_VERB = {
|
|
44
|
+
clients: [],
|
|
45
|
+
client: [],
|
|
46
|
+
run: [],
|
|
47
|
+
list: ["--timeline", "--needs-review", "--status", "--owner", "--no-owner", "--overdue", "--kind", "--search", "--archived", "--review"],
|
|
48
|
+
queue: [],
|
|
49
|
+
show: ["--timeline"],
|
|
50
|
+
add: ["--timeline", "--title", "--detail", "--owners", "--owner", "--due", "--meeting", "--kind", "--date", "--source-url", "--source-note", "--request-id"],
|
|
51
|
+
edit: ["--timeline", "--version", "--title", "--detail", "--owners", "--owner", "--due", "--kind", "--date"],
|
|
52
|
+
done: ["--version"],
|
|
53
|
+
reopen: ["--version"],
|
|
54
|
+
link: ["--version", "--meeting", "--none"],
|
|
55
|
+
approve: ["--timeline", "--version", "--restore"],
|
|
56
|
+
dismiss: ["--timeline", "--version"],
|
|
57
|
+
proposal: ["--timeline", "--version"],
|
|
58
|
+
archive: ["--timeline", "--version"],
|
|
59
|
+
unarchive: ["--timeline", "--version"],
|
|
60
|
+
};
|
|
61
|
+
/** Positional words each verb takes, named for the refusal. */
|
|
62
|
+
const POSITIONALS = {
|
|
63
|
+
clients: [],
|
|
64
|
+
client: ["client"],
|
|
65
|
+
run: ["run id"],
|
|
66
|
+
list: ["client"],
|
|
67
|
+
queue: ["client"],
|
|
68
|
+
show: ["id"],
|
|
69
|
+
add: ["client"],
|
|
70
|
+
edit: ["id"],
|
|
71
|
+
done: ["action id"],
|
|
72
|
+
reopen: ["action id"],
|
|
73
|
+
link: ["action id"],
|
|
74
|
+
approve: ["id"],
|
|
75
|
+
dismiss: ["id"],
|
|
76
|
+
proposal: ["id", "proposal id", "accept, dismiss or add-new"],
|
|
77
|
+
archive: ["id"],
|
|
78
|
+
unarchive: ["id"],
|
|
79
|
+
};
|
|
80
|
+
/** A value flag given twice would silently keep only the last; say so instead. */
|
|
81
|
+
function refuseRepeatedFlags(args) {
|
|
82
|
+
for (const flag of VALUE_FLAGS) {
|
|
83
|
+
const times = args.filter((arg) => arg === flag || arg.startsWith(`${flag}=`)).length;
|
|
84
|
+
if (times > 1) {
|
|
85
|
+
throw new Error(flag === "--owners"
|
|
86
|
+
? '--owners was given more than once. Put everyone in one list: --owners "Pip Moss, Rowan Ash".'
|
|
87
|
+
: `${flag} was given more than once. Give it once.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function parseManagerArgs(args) {
|
|
92
|
+
refuseRepeatedFlags(args);
|
|
93
|
+
const values = parseNamedArgs(args, { allowedFlags: [...OWN_FLAGS, ...COMMON_FLAGS], valueFlags: VALUE_FLAGS });
|
|
94
|
+
const base = {
|
|
95
|
+
kind: "manager",
|
|
96
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
97
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
98
|
+
json: values.booleans.has("--json"),
|
|
99
|
+
};
|
|
100
|
+
const first = values.positionals[0];
|
|
101
|
+
if (values.booleans.has("--help") || first === undefined || first === "help" || first === "-h") {
|
|
102
|
+
return { ...base, action: "clients", help: true, timeline: false };
|
|
103
|
+
}
|
|
104
|
+
if (!MANAGER_VERBS.includes(first))
|
|
105
|
+
throw new Error(ONE_VERB);
|
|
106
|
+
const verb = first;
|
|
107
|
+
const owned = FLAGS_BY_VERB[verb];
|
|
108
|
+
const stray = [...values.flags.keys(), ...values.booleans].find((flag) => OWN_FLAGS.includes(flag) && !owned.includes(flag));
|
|
109
|
+
if (stray)
|
|
110
|
+
throw new Error(`${stray} does not belong to \`cockpit manager ${verb}\`.`);
|
|
111
|
+
const rest = values.positionals.slice(1);
|
|
112
|
+
const wanted = POSITIONALS[verb];
|
|
113
|
+
if (rest.length !== wanted.length) {
|
|
114
|
+
throw new Error(wanted.length === 0
|
|
115
|
+
? `\`cockpit manager ${verb}\` takes no positional arguments.`
|
|
116
|
+
: `\`cockpit manager ${verb}\` takes exactly: ${wanted.map((word) => `<${word}>`).join(" ")}${verb === "list" || verb === "queue" || verb === "add" || verb === "client" ? " (quote a client's name that has spaces)" : ""}.`);
|
|
117
|
+
}
|
|
118
|
+
const timeline = values.booleans.has("--timeline");
|
|
119
|
+
const shape = { ...base, action: verb, timeline };
|
|
120
|
+
const flag = (name) => values.flags.get(name);
|
|
121
|
+
if (verb === "list" || verb === "queue" || verb === "add" || verb === "client")
|
|
122
|
+
shape.client = rest[0];
|
|
123
|
+
else if (verb === "run")
|
|
124
|
+
shape.runId = readId(rest[0], "A run");
|
|
125
|
+
else if (verb !== "clients")
|
|
126
|
+
shape.id = readId(rest[0], verb === "done" || verb === "reopen" || verb === "link" ? "An action" : "An item");
|
|
127
|
+
if (verb === "proposal") {
|
|
128
|
+
shape.proposalId = rest[1];
|
|
129
|
+
shape.decision = readDecision(rest[2]);
|
|
130
|
+
}
|
|
131
|
+
const needsVersion = !["clients", "client", "run", "list", "queue", "show", "add"].includes(verb);
|
|
132
|
+
if (needsVersion)
|
|
133
|
+
shape.version = readVersion(flag("--version"), verb);
|
|
134
|
+
if (verb === "list")
|
|
135
|
+
readListFilters(values, shape);
|
|
136
|
+
if (verb === "add")
|
|
137
|
+
readAddFields(values, shape);
|
|
138
|
+
if (verb === "edit")
|
|
139
|
+
readEditFields(values, shape);
|
|
140
|
+
if (verb === "link")
|
|
141
|
+
readLink(values, shape);
|
|
142
|
+
if (verb === "approve" && values.booleans.has("--restore"))
|
|
143
|
+
shape.restore = true;
|
|
144
|
+
return shape;
|
|
145
|
+
}
|
|
146
|
+
function readId(value, what) {
|
|
147
|
+
if (!ID.test(value))
|
|
148
|
+
throw new Error(`${what} id looks like 8f14e45f-ceea-467a-9575-1d3a4e7b2c10 (\`cockpit manager list <client>\` prints them).`);
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
function readVersion(raw, verb) {
|
|
152
|
+
if (raw === undefined) {
|
|
153
|
+
throw new Error(`\`cockpit manager ${verb}\` needs --version <version>: the item's version exactly as \`cockpit manager show <id>\` prints it. It keeps you from writing over a change someone else made.`);
|
|
154
|
+
}
|
|
155
|
+
if (!VERSION.test(raw))
|
|
156
|
+
throw new Error("--version is the item's version exactly as `cockpit manager show <id>` prints it, like 2026-10-09T17:03:11.123456+00:00.");
|
|
157
|
+
return raw;
|
|
158
|
+
}
|
|
159
|
+
function readDecision(word) {
|
|
160
|
+
if (word === "accept" || word === "dismiss")
|
|
161
|
+
return word;
|
|
162
|
+
if (word === "add-new" || word === "add_as_new")
|
|
163
|
+
return "add_as_new";
|
|
164
|
+
throw new Error("The last word of `cockpit manager proposal` is accept, dismiss or add-new.");
|
|
165
|
+
}
|
|
166
|
+
/** One calendar date, `YYYY-MM-DD`, that exists (`2026-02-30` does not). */
|
|
167
|
+
export function isCalendarDate(value) {
|
|
168
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
169
|
+
if (!match)
|
|
170
|
+
return false;
|
|
171
|
+
const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
172
|
+
const date = new Date(Date.UTC(2000, 0, 1));
|
|
173
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
174
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
175
|
+
}
|
|
176
|
+
function readDate(raw, flag) {
|
|
177
|
+
if (!isCalendarDate(raw))
|
|
178
|
+
throw new Error(`${flag} wants a real date written YYYY-MM-DD, like 2026-10-31.`);
|
|
179
|
+
return raw;
|
|
180
|
+
}
|
|
181
|
+
/** "A, B" is two people: split on commas, trim, drop blanks. */
|
|
182
|
+
export function splitOwners(raw) {
|
|
183
|
+
return raw.split(",").map((name) => name.trim()).filter((name) => name !== "");
|
|
184
|
+
}
|
|
185
|
+
function readKind(raw, flag) {
|
|
186
|
+
if (!MANAGER_TIMELINE_KINDS.includes(raw))
|
|
187
|
+
throw new Error(`${flag} wants one of ${MANAGER_TIMELINE_KINDS.join(", ")}.`);
|
|
188
|
+
return raw;
|
|
189
|
+
}
|
|
190
|
+
function readListFilters(values, shape) {
|
|
191
|
+
const status = values.flags.get("--status");
|
|
192
|
+
const owner = values.flags.get("--owner");
|
|
193
|
+
const noOwner = values.booleans.has("--no-owner");
|
|
194
|
+
const kind = values.flags.get("--kind");
|
|
195
|
+
const review = values.flags.get("--review");
|
|
196
|
+
const overdue = values.booleans.has("--overdue");
|
|
197
|
+
if (shape.timeline && (status || owner || noOwner || overdue))
|
|
198
|
+
throw new Error(`${status ? "--status" : owner ? "--owner" : noOwner ? "--no-owner" : "--overdue"} filters actions, and a timeline list does not take it. Drop --timeline, or filter the timeline with --kind.`);
|
|
199
|
+
if (!shape.timeline && kind)
|
|
200
|
+
throw new Error("--kind filters the timeline. Add --timeline.");
|
|
201
|
+
if (owner && noOwner)
|
|
202
|
+
throw new Error("--owner and --no-owner say opposite things. Give one.");
|
|
203
|
+
if (status && !MANAGER_ACTION_STATUSES.includes(status))
|
|
204
|
+
throw new Error(`--status wants one of ${MANAGER_ACTION_STATUSES.join(", ")}.`);
|
|
205
|
+
if (review !== undefined && review !== "dismissed" && review !== "all")
|
|
206
|
+
throw new Error("--review wants dismissed or all (dismissed items are hidden unless you ask).");
|
|
207
|
+
if (kind)
|
|
208
|
+
shape.kindFilter = readKind(kind, "--kind");
|
|
209
|
+
if (status)
|
|
210
|
+
shape.status = status;
|
|
211
|
+
if (owner)
|
|
212
|
+
shape.ownerFilter = owner.trim();
|
|
213
|
+
if (noOwner)
|
|
214
|
+
shape.noOwner = true;
|
|
215
|
+
if (overdue)
|
|
216
|
+
shape.overdue = true;
|
|
217
|
+
if (review)
|
|
218
|
+
shape.review = review === "dismissed" ? "rejected" : "all";
|
|
219
|
+
if (values.booleans.has("--archived"))
|
|
220
|
+
shape.archived = true;
|
|
221
|
+
const search = optionalNonEmpty(values.flags.get("--search"));
|
|
222
|
+
if (search)
|
|
223
|
+
shape.search = search;
|
|
224
|
+
if (values.booleans.has("--needs-review"))
|
|
225
|
+
shape.needsReview = true;
|
|
226
|
+
}
|
|
227
|
+
/** The flags only an action has, and the flags only a timeline item has, refused on the wrong one in plain words. */
|
|
228
|
+
function refuseWrongItemFlags(values, shape) {
|
|
229
|
+
const given = (name) => values.flags.has(name) || values.booleans.has(name);
|
|
230
|
+
if (shape.timeline) {
|
|
231
|
+
if (given("--owners"))
|
|
232
|
+
throw new Error("A timeline item has one owner: use --owner <name> (or --owner none). --owners is for an action.");
|
|
233
|
+
const action = ["--due", "--meeting"].find(given);
|
|
234
|
+
if (action)
|
|
235
|
+
throw new Error(`${action} belongs to an action; a timeline item has a --date. Drop --timeline for an action.`);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
if (given("--owner"))
|
|
239
|
+
throw new Error("An action has several owners: use --owners \"A, B\" (or --owners none). --owner is for a timeline item.");
|
|
240
|
+
const item = ["--kind", "--date"].find(given);
|
|
241
|
+
if (item)
|
|
242
|
+
throw new Error(`${item} belongs to a timeline item. Add --timeline, or use --due for an action's date.`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const isNone = (value) => value.trim().toLowerCase() === "none";
|
|
246
|
+
/** `--owners "A, B"` replaces the whole list; `none` is the only way to clear it; a list of blanks is neither. */
|
|
247
|
+
function readOwnersList(raw) {
|
|
248
|
+
if (isNone(raw))
|
|
249
|
+
return [];
|
|
250
|
+
// Commas separate people (the web form's rule), so a name can hold neither a comma nor the quotes around one.
|
|
251
|
+
if (raw.includes('"'))
|
|
252
|
+
throw new Error('--owners splits people on commas, so a name cannot hold a comma or a double quote. Write the names without them: --owners "Pip Moss, Rowan Ash".');
|
|
253
|
+
const owners = splitOwners(raw);
|
|
254
|
+
if (owners.length === 0)
|
|
255
|
+
throw new Error("--owners names nobody. Give one or more people separated by commas, or none to clear them.");
|
|
256
|
+
return owners;
|
|
257
|
+
}
|
|
258
|
+
function readOneOwner(raw) {
|
|
259
|
+
if (isNone(raw))
|
|
260
|
+
return null;
|
|
261
|
+
const name = raw.trim();
|
|
262
|
+
if (name === "")
|
|
263
|
+
throw new Error("--owner needs a name, or none to clear it.");
|
|
264
|
+
return name;
|
|
265
|
+
}
|
|
266
|
+
function readAddFields(values, shape) {
|
|
267
|
+
refuseWrongItemFlags(values, shape);
|
|
268
|
+
const title = optionalNonEmpty(values.flags.get("--title"));
|
|
269
|
+
if (!title)
|
|
270
|
+
throw new Error("`cockpit manager add` needs --title <text>.");
|
|
271
|
+
shape.title = title;
|
|
272
|
+
const detail = values.flags.get("--detail");
|
|
273
|
+
if (detail !== undefined)
|
|
274
|
+
shape.detail = detail;
|
|
275
|
+
const requestId = values.flags.get("--request-id");
|
|
276
|
+
if (requestId !== undefined)
|
|
277
|
+
shape.requestId = readId(requestId, "A request");
|
|
278
|
+
readSource(values, shape);
|
|
279
|
+
if (shape.timeline) {
|
|
280
|
+
const kind = values.flags.get("--kind");
|
|
281
|
+
if (!kind)
|
|
282
|
+
throw new Error(`\`cockpit manager add --timeline\` needs --kind ${MANAGER_TIMELINE_KINDS.join("|")}.`);
|
|
283
|
+
shape.itemKind = readKind(kind, "--kind");
|
|
284
|
+
const date = values.flags.get("--date");
|
|
285
|
+
if (date && !isNone(date))
|
|
286
|
+
shape.occurredAt = readDate(date, "--date");
|
|
287
|
+
const owner = values.flags.get("--owner");
|
|
288
|
+
const name = owner === undefined ? null : readOneOwner(owner);
|
|
289
|
+
if (name)
|
|
290
|
+
shape.ownerName = name;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const due = values.flags.get("--due");
|
|
294
|
+
if (due && !isNone(due))
|
|
295
|
+
shape.dueAt = readDate(due, "--due");
|
|
296
|
+
const meeting = values.flags.get("--meeting");
|
|
297
|
+
if (meeting)
|
|
298
|
+
shape.timelineItemId = readId(meeting, "A timeline item");
|
|
299
|
+
const owners = values.flags.get("--owners");
|
|
300
|
+
const list = owners === undefined ? [] : readOwnersList(owners);
|
|
301
|
+
if (list.length > 0)
|
|
302
|
+
shape.owners = list;
|
|
303
|
+
}
|
|
304
|
+
/** Where an item a person adds came from (the web form's Source field): a link, a note, or both. The server checks them again. */
|
|
305
|
+
function readSource(values, shape) {
|
|
306
|
+
const url = optionalNonEmpty(values.flags.get("--source-url"));
|
|
307
|
+
if (url !== undefined) {
|
|
308
|
+
if (!/^https?:\/\/\S+$/i.test(url.trim()))
|
|
309
|
+
throw new Error("--source-url wants a link starting with http:// or https://. Put anything else in --source-note.");
|
|
310
|
+
shape.sourceUrl = url.trim();
|
|
311
|
+
}
|
|
312
|
+
const note = optionalNonEmpty(values.flags.get("--source-note"));
|
|
313
|
+
if (note !== undefined) {
|
|
314
|
+
if (note.trim().length > 1000)
|
|
315
|
+
throw new Error("--source-note is at most 1,000 characters. A link can be longer: use --source-url.");
|
|
316
|
+
shape.sourceNote = note.trim();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function readEditFields(values, shape) {
|
|
320
|
+
refuseWrongItemFlags(values, shape);
|
|
321
|
+
let changes = 0;
|
|
322
|
+
const title = values.flags.get("--title");
|
|
323
|
+
if (title !== undefined) {
|
|
324
|
+
if (title.trim() === "")
|
|
325
|
+
throw new Error("--title cannot be blank.");
|
|
326
|
+
shape.title = title.trim();
|
|
327
|
+
changes += 1;
|
|
328
|
+
}
|
|
329
|
+
const detail = values.flags.get("--detail");
|
|
330
|
+
if (detail !== undefined) {
|
|
331
|
+
shape.detail = detail;
|
|
332
|
+
changes += 1;
|
|
333
|
+
}
|
|
334
|
+
const owners = values.flags.get("--owners");
|
|
335
|
+
if (owners !== undefined) {
|
|
336
|
+
shape.owners = readOwnersList(owners);
|
|
337
|
+
changes += 1;
|
|
338
|
+
}
|
|
339
|
+
const owner = values.flags.get("--owner");
|
|
340
|
+
if (owner !== undefined) {
|
|
341
|
+
shape.ownerName = readOneOwner(owner);
|
|
342
|
+
changes += 1;
|
|
343
|
+
}
|
|
344
|
+
const due = values.flags.get("--due");
|
|
345
|
+
if (due !== undefined) {
|
|
346
|
+
shape.dueAt = isNone(due) ? null : readDate(due, "--due");
|
|
347
|
+
changes += 1;
|
|
348
|
+
}
|
|
349
|
+
const date = values.flags.get("--date");
|
|
350
|
+
if (date !== undefined) {
|
|
351
|
+
shape.occurredAt = isNone(date) ? null : readDate(date, "--date");
|
|
352
|
+
changes += 1;
|
|
353
|
+
}
|
|
354
|
+
const kind = values.flags.get("--kind");
|
|
355
|
+
if (kind !== undefined) {
|
|
356
|
+
shape.itemKind = readKind(kind, "--kind");
|
|
357
|
+
changes += 1;
|
|
358
|
+
}
|
|
359
|
+
if (changes === 0) {
|
|
360
|
+
throw new Error("`cockpit manager edit` has nothing to change. Give at least one of --title, --detail, --owners, --due (--kind, --date, --owner on a timeline item). A flag you leave out keeps its value; `none` clears owners, a due date or a date.");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function readLink(values, shape) {
|
|
364
|
+
const meeting = values.flags.get("--meeting");
|
|
365
|
+
const none = values.booleans.has("--none");
|
|
366
|
+
if (meeting && none)
|
|
367
|
+
throw new Error("--meeting and --none say opposite things. Give one.");
|
|
368
|
+
if (!meeting && !none)
|
|
369
|
+
throw new Error("`cockpit manager link` needs --meeting <timeline-id> to link, or --none to unlink.");
|
|
370
|
+
shape.timelineItemId = meeting ? readId(meeting, "A timeline item") : null;
|
|
371
|
+
}
|
|
@@ -43,6 +43,7 @@ export { parseUsageArgs } from "./local-args-tower-usage.js";
|
|
|
43
43
|
export { parseLogsArgs } from "./local-args-tower-logs.js";
|
|
44
44
|
export { parseBrainArgs } from "./local-args-tower-brain.js";
|
|
45
45
|
export { parseApprovalsArgs } from "./local-args-tower-approvals.js";
|
|
46
|
+
export { parseManagerArgs } from "./local-args-tower-manager.js";
|
|
46
47
|
export { parseImageArgs } from "./local-args-tower-image.js";
|
|
47
48
|
export { parseCareersArgs } from './local-args-tower-careers.js';
|
|
48
49
|
export { parseRenameArgs } from './local-args-tower-rename.js';
|
|
@@ -19,7 +19,7 @@ import { parseGlossaryArgs } from "./glossary.js";
|
|
|
19
19
|
import { parseRootsArgs } from "./roots.js";
|
|
20
20
|
import { SURFACE_ALIASES } from "./local-help.js";
|
|
21
21
|
import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseSourcesArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
|
|
22
|
-
import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseGithubArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseSetupArgs, parseSpeedArgs, parseLogsArgs, parseBrainArgs, parseApprovalsArgs, parseImageArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
|
|
22
|
+
import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseGithubArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseSetupArgs, parseSpeedArgs, parseLogsArgs, parseBrainArgs, parseApprovalsArgs, parseManagerArgs, parseImageArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
|
|
23
23
|
// `normalizeUrl` has always been part of this module's surface — `local.ts` and
|
|
24
24
|
// `local-auth.ts` import it from here — so it stays exported from this address
|
|
25
25
|
// even though it now lives next door. The same goes for the four names the
|
|
@@ -146,6 +146,8 @@ export function parseLocalArgs(argv) {
|
|
|
146
146
|
return parseBrainArgs(argv.slice(1));
|
|
147
147
|
case "approvals":
|
|
148
148
|
return parseApprovalsArgs(argv.slice(1));
|
|
149
|
+
case "manager":
|
|
150
|
+
return parseManagerArgs(argv.slice(1));
|
|
149
151
|
case "image":
|
|
150
152
|
return parseImageArgs(argv.slice(1));
|
|
151
153
|
default:
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit manager` command shape (BLI-5006), beside `local-command-shapes-approvals.ts` for the
|
|
3
|
+
* same reason: `local-command-shapes.ts` sits at the readability ceiling.
|
|
4
|
+
*
|
|
5
|
+
* The parser (`local-args-tower-manager.ts`) turns the typed words into the fields a request
|
|
6
|
+
* carries, so `manager.ts` only builds paths and bodies. A field that is ABSENT is "leave it
|
|
7
|
+
* alone"; an explicit clear is `owners: []`, `ownerName: null`, `dueAt: null`, `occurredAt: null`.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -89,6 +89,47 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
89
89
|
"approvals_edit, approvals_reject, approvals_rules, approvals_rules_create, approvals_rules_revoke.",
|
|
90
90
|
],
|
|
91
91
|
],
|
|
92
|
+
[
|
|
93
|
+
"manager",
|
|
94
|
+
[
|
|
95
|
+
"Usage: cockpit manager <clients|client|run|list|queue|show|add|edit|done|reopen|link|approve|dismiss|proposal|archive|unarchive> ... [--dashboard-url <url>] [--json]",
|
|
96
|
+
"",
|
|
97
|
+
"Tower Manager's client actions and timeline items, from the terminal: the same things the manager page does.",
|
|
98
|
+
" cockpit manager clients every client, with its id",
|
|
99
|
+
" cockpit manager client <client> one client: state, last read, pages waiting; run <run-id> one run's receipt",
|
|
100
|
+
" cockpit manager list <client> [--timeline] [--needs-review] [--status open|done|dropped|closed|all] [--owner <name>|--no-owner] [--overdue] [--kind <kind>] [--search <text>] [--archived] [--review dismissed|all]",
|
|
101
|
+
" actions (default), or with --timeline the timeline; --status, --owner, --no-owner and --overdue (open actions due before today) are for actions, --kind for the timeline.",
|
|
102
|
+
" Dismissed and archived items are hidden: --review dismissed (or all) and --archived find them. A list holds at most 500 rows and says when it is cut.",
|
|
103
|
+
" cockpit manager queue <client> changes from syncs after onboarding that nobody has looked at yet (the onboarding's own items are not in it)",
|
|
104
|
+
" an empty queue does not mean everything is reviewed: list <client> --needs-review --status all, and list <client> --timeline --needs-review",
|
|
105
|
+
" cockpit manager show <id> [--timeline] one item in full: detail, sources, suggested changes with their ids, what a sync changed",
|
|
106
|
+
" cockpit manager add <client> --title <text> [--owners \"A, B\"] [--due YYYY-MM-DD] [--meeting <timeline-id>] [--detail <text>] [--source-url <url>] [--source-note <text>] [--request-id <uuid>]",
|
|
107
|
+
" cockpit manager add <client> --timeline --kind meeting|document|milestone|internal --title <text> [--date YYYY-MM-DD] [--owner <one name>] [--detail <text>] [--source-url <url>] [--source-note <text>] [--request-id <uuid>]",
|
|
108
|
+
" no --date makes an undated timeline item, which lands in \"Needs a date\"",
|
|
109
|
+
" --source-url (an http(s) link) and --source-note say where it came from, as the web form's Source does; show prints them",
|
|
110
|
+
" cockpit manager edit <id> --version <v> [--timeline] [--title <text>] [--detail <text>] [--due <date>|none] [--owners \"A, B\"|none]",
|
|
111
|
+
" on a timeline item: [--kind <kind>] [--date <date>|none] [--owner <one name>|none]",
|
|
112
|
+
" cockpit manager done <action> --version <v> tick an action off (an AI action you tick off counts as reviewed by you, and suggested changes it makes moot close); reopen <action> --version <v> open it again",
|
|
113
|
+
" cockpit manager link <action> --version <v> --meeting <timeline-id> link an action to the meeting it came from; --none unlinks it",
|
|
114
|
+
" cockpit manager approve <id> --version <v> [--timeline] [--restore] the AI's item is right: mark it reviewed; --restore brings a DISMISSED item back into the lists, marked reviewed by you (without it, a dismissed item is refused)",
|
|
115
|
+
" cockpit manager dismiss <id> --version <v> [--timeline] the AI's item is wrong: dismiss it (kept, hidden from the lists)",
|
|
116
|
+
" cockpit manager proposal <id> <proposal-id> accept|dismiss|add-new --version <v> [--timeline] decide one suggested change; add-new makes it an item of its own",
|
|
117
|
+
" cockpit manager archive <id> --version <v> [--timeline] delete an item: it moves to the Deleted list (list --archived), kept; unarchive <id> --version <v> brings it back",
|
|
118
|
+
"approve, dismiss and proposal decide the AI's suggestion as YOU: your name is recorded as the reviewer, and the item leaves the review queue unless a suggested change still waits on it.",
|
|
119
|
+
"<client> is the client's id, or its exact name (any case; `clients` lists them); a name that matches no client, or several, is refused with the names. Ids and versions are printed by list, queue and show. Items you add are reviewed already.",
|
|
120
|
+
"--version is the item's version exactly as list or show prints it, copied as it is. Every change to an existing item names it: if someone or a sync changed the item",
|
|
121
|
+
"since, the write is refused with a sentence saying so and nothing is written; run show again for the new version and retry.",
|
|
122
|
+
"--owners on an action takes, as the web form says, \"One or more people, separated by commas\" (\"Rowan Ash, Juniper Twig\"), and it REPLACES the whole list. Leave it out and the owners stay;",
|
|
123
|
+
"--owners none clears them. A timeline item has ONE owner: --owner <name> or --owner none. The same pattern clears a date: --due none, --date none.",
|
|
124
|
+
"Owner names are kept as written, not matched to accounts: use the spelling `list` shows, so one person stays one name. A flag you leave out is never sent, and a flag given twice is refused. A date is YYYY-MM-DD. `cockpit manager`, `manager help` and `manager --help` print this and change nothing.",
|
|
125
|
+
"add always sends a request id (--request-id <uuid>, else a fresh one) and prints it. If Tower does not answer, nothing was confirmed: rerun the same command with",
|
|
126
|
+
"--request-id <id> and it retries without making a duplicate.",
|
|
127
|
+
"--json writes the door's body to stdout, verbatim, and nothing else (an add's body also carries requestId); a refusal is { ok: false, error, detail, refusal? } and exits 1.",
|
|
128
|
+
"Progress lines on stderr start with [tag]; stdout is the answer (2>/dev/null hides them). Creating a client, syncing, Rebuild brief and merging owners are on the manager page only. Teaching: docs/architecture/tower-manager.md.",
|
|
129
|
+
"The bli-tower MCP server has the twins: manager_clients, manager_client, manager_run, manager_list, manager_queue, manager_show, manager_add, manager_edit,",
|
|
130
|
+
"manager_done, manager_reopen, manager_link, manager_approve, manager_dismiss, manager_proposal, manager_archive, manager_unarchive.",
|
|
131
|
+
],
|
|
132
|
+
],
|
|
92
133
|
[
|
|
93
134
|
"image",
|
|
94
135
|
[
|
|
@@ -391,7 +391,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
391
391
|
"recording <recording-id> [--json] — one recording, text and all, shown or alternate.",
|
|
392
392
|
"accounts --show-webhook <account-id> — that account's webhook URL, where to paste it at the vendor, and whether a signing secret is stored (BLI-4394). Until one is, every request to that URL is refused, which is the right way to be off.",
|
|
393
393
|
"webhook <account-id> --secret-stdin — store the VENDOR's webhook signing secret. THE SECRET IS READ FROM STDIN, never a flag: printf \"%s\" \"<signing secret>\" | cockpit notes webhook <account-id> --secret-stdin",
|
|
394
|
-
"connect --provider wisprflow
|
|
394
|
+
"connect --provider wisprflow — Wispr Flow has no API key: this asks Tower to start the sign-in (the same door the Settings button, the MCP tool notes_connect_wisprflow and JARVIS use), prints and opens Wispr's approval page (Google, Apple, Microsoft or company single sign-on), and waits up to 10 minutes for Tower to report it connected, then Tower reads your Notetaker meetings from then on. Needs Notetaker and MCP access on your Wispr account. Exits 1 with a reason if nothing connects (BLI-12926, BLI-17973).",
|
|
395
395
|
"connect --provider fellow|circleback [--workspace <subdomain>] --key-stdin [--name <n>] — connect one. Fellow needs --workspace, its API is per workspace. THE API KEY IS READ FROM STDIN, never a flag: printf \"%s\" \"<key>\" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin",
|
|
396
396
|
"detach <account-id> — take it off. The stored key goes with it; the meetings it already filed stay.",
|
|
397
397
|
"sync <account-id> — read that notetaker now. Prints the run's own counts and reason label, and exits 1 when the pass failed.",
|
|
@@ -204,6 +204,9 @@ export function localSubcommandHelp(command) {
|
|
|
204
204
|
" Every paired collector device: name, id, trust state, owner, and why",
|
|
205
205
|
" (revoked reason, or when it was approved, or that it's still waiting).",
|
|
206
206
|
" Admin only.",
|
|
207
|
+
" cockpit team device scope <id|name> notetaker_bot|none [--json]",
|
|
208
|
+
" Super admin only. Gives the meeting bot's device the scope Tower's claim door requires,",
|
|
209
|
+
" or takes it away. Without it the bot is refused every meeting (bot_scope_required).",
|
|
207
210
|
" cockpit team device revoke <id|name> --reason <label> [--note \"<text>\"] [--yes]",
|
|
208
211
|
" Admin only. Names the device, asks first unless --yes, then revokes it —",
|
|
209
212
|
" this cannot be undone. --reason must be one of: stale_test_pairing,",
|
|
@@ -87,6 +87,7 @@ export const rootCommandNames = new Set([
|
|
|
87
87
|
"logs",
|
|
88
88
|
"brain",
|
|
89
89
|
"approvals",
|
|
90
|
+
"manager",
|
|
90
91
|
"image",
|
|
91
92
|
...SURFACE_ALIASES.keys(),
|
|
92
93
|
]);
|
|
@@ -148,6 +149,7 @@ export function localCommandHelp(command) {
|
|
|
148
149
|
" cockpit logs <collector|dashboard|railway|railway:<service>|-> [--since 15m|1h|2d|<iso>] [--raw <template-id> [--limit <n>]] [--all-levels] [--table <file>] [--json]",
|
|
149
150
|
" cockpit brain status [--project <id>] [--show open|running|needs_human|fixed|not_actionable|cleared] [--dashboard-url <url>] [--json]",
|
|
150
151
|
" cockpit approvals [list|show <id>|create|approve <id> [--edit]|edit <id> [--approve]|reject <id>|rules [--active true|false]|rules create <card-id> [--any-audience] [--label <text>]|rules revoke <rule-id> [--reason <text>]] [--status <s>] [--limit <n>] [--before <cursor>] [--dashboard-url <url>] [--json]",
|
|
152
|
+
" cockpit manager <clients|client <client>|run <run-id>|list <client>|queue <client>|show <id>|add <client> --title <t>|edit <id> --version <v>|done|reopen|link|approve|dismiss|proposal|archive|unarchive> [--timeline] [--dashboard-url <url>] [--json]",
|
|
151
153
|
" cockpit image <describe <file> [--session <id>]|show <sha256-or-prefix>> [--dashboard-url <url>] [--json]",
|
|
152
154
|
" cockpit setup [--since <7d|30d|date>] [--details] [--person <name|email>] [--compare <name,name>] [--json]",
|
|
153
155
|
" cockpit usage sessions [--person <exact-name>] [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--tool claude|codex] [--since <window>] [--until <iso>] [--cursor <cursor>] [--limit <n>] [--json]",
|
package/dist/commands/local.js
CHANGED
|
@@ -55,6 +55,7 @@ import { runUsage } from "./usage.js";
|
|
|
55
55
|
import { runSpeed } from "./speed.js";
|
|
56
56
|
import { runLogs } from "./logs.js";
|
|
57
57
|
import { runApprovals } from "./approvals.js";
|
|
58
|
+
import { runManager } from "./manager.js";
|
|
58
59
|
import { runImage } from "./image.js";
|
|
59
60
|
import { runBrain } from "./brain.js";
|
|
60
61
|
import { parseLocalArgs } from "./local-args.js";
|
|
@@ -228,21 +229,15 @@ export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorA
|
|
|
228
229
|
return await runSourcesCheck(command, io);
|
|
229
230
|
case "glossary":
|
|
230
231
|
return await runGlossary(command, io);
|
|
231
|
-
case "docs":
|
|
232
|
-
|
|
233
|
-
case "
|
|
234
|
-
return await runMsg(command, io);
|
|
235
|
-
case "issue":
|
|
236
|
-
return await runIssue(command, io);
|
|
232
|
+
case "docs": return await runDocs(command, io);
|
|
233
|
+
case "msg": return await runMsg(command, io);
|
|
234
|
+
case "issue": return await runIssue(command, io);
|
|
237
235
|
case "cal": return runCal(command, io);
|
|
238
|
-
case "mail":
|
|
239
|
-
return await runMail(command, io);
|
|
236
|
+
case "mail": return await runMail(command, io);
|
|
240
237
|
case "project": return await runProject(command, io);
|
|
241
238
|
case "search": return await runSearch(command, io);
|
|
242
|
-
case "github":
|
|
243
|
-
|
|
244
|
-
case "release":
|
|
245
|
-
return await runRelease(command, io);
|
|
239
|
+
case "github": return await runGithub(command, io);
|
|
240
|
+
case "release": return await runRelease(command, io);
|
|
246
241
|
case "careers": return await runCareers(command, io);
|
|
247
242
|
case "rename": return await runRename(command, io);
|
|
248
243
|
case "setup": return await runSetup(command, io);
|
|
@@ -251,6 +246,7 @@ export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorA
|
|
|
251
246
|
case "logs": return await runLogs(command, io);
|
|
252
247
|
case "brain": return await runBrain(command, io);
|
|
253
248
|
case "approvals": return await runApprovals(command, io);
|
|
249
|
+
case "manager": return await runManager(command, io);
|
|
254
250
|
case "image": return await runImage(command, io);
|
|
255
251
|
}
|
|
256
252
|
}
|