@bli-cockpit/cli 0.2.282 → 0.2.283
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.
|
@@ -55,6 +55,7 @@ const NOTES_ACTIONS = new Set([
|
|
|
55
55
|
"bot-upcoming",
|
|
56
56
|
"record",
|
|
57
57
|
"app",
|
|
58
|
+
"review",
|
|
58
59
|
]);
|
|
59
60
|
/** Actions whose first positional is the note it acts on. */
|
|
60
61
|
const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
|
|
@@ -110,6 +111,9 @@ const NOTES_ALLOWED_FLAGS = [
|
|
|
110
111
|
// transcript was stored cut short. A FLAG, not a verb: it is the same sync
|
|
111
112
|
// door with a different request.
|
|
112
113
|
"--repair-truncated",
|
|
114
|
+
// BLI-22350. `review <note-id> --submit` reads `{ raw_hash, answers }` from stdin; `--accept-all` sends the defaults.
|
|
115
|
+
"--submit",
|
|
116
|
+
"--accept-all",
|
|
113
117
|
];
|
|
114
118
|
const NOTES_VALUE_FLAGS = [
|
|
115
119
|
"--home",
|
|
@@ -150,6 +154,7 @@ export function parseNotesArgs(args) {
|
|
|
150
154
|
throw new Error("notes rename needs --name <name>.");
|
|
151
155
|
const { to, clearShelf } = readNotesShelfTarget(action, values);
|
|
152
156
|
const repairTruncated = readRepairTruncated(action, values);
|
|
157
|
+
const review = readReviewRequest(action, values, rest);
|
|
153
158
|
const { limit, month, offset } = readNotesWindow(action, values);
|
|
154
159
|
return {
|
|
155
160
|
kind: "notes",
|
|
@@ -184,6 +189,7 @@ export function parseNotesArgs(args) {
|
|
|
184
189
|
...(month ? { month } : {}),
|
|
185
190
|
...(offset === undefined ? {} : { offset }),
|
|
186
191
|
...(repairTruncated ? { repairTruncated: true } : {}),
|
|
192
|
+
...review,
|
|
187
193
|
yes,
|
|
188
194
|
json,
|
|
189
195
|
};
|
|
@@ -193,7 +199,7 @@ function readNotesAction(first) {
|
|
|
193
199
|
// almost always wants — same reasoning as bare `cockpit` running convergence.
|
|
194
200
|
const action = (first === undefined ? "list" : first);
|
|
195
201
|
if (!NOTES_ACTIONS.has(action)) {
|
|
196
|
-
throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, summarize, move, place, accounts, connect, detach, sync, sync-status, recordings, recording, webhook, granola-status,
|
|
202
|
+
throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, summarize, move, place, accounts, connect, detach, sync, sync-status, recordings, recording, webhook, granola-status, transcription-stats, or review (and record start|stop|status|cancel, app install|uninstall|status).`);
|
|
197
203
|
}
|
|
198
204
|
return action;
|
|
199
205
|
}
|
|
@@ -220,6 +226,23 @@ function readAppRequest(rest) {
|
|
|
220
226
|
throw new Error(`notes app ${verb} does not take "${rest[1]}".`);
|
|
221
227
|
return { appAction: verb };
|
|
222
228
|
}
|
|
229
|
+
/** BLI-22350: `review [<note-id>] [--submit|--accept-all]`. Bare lists mine; an id shows one; a flag answers it. */
|
|
230
|
+
function readReviewRequest(action, values, rest) {
|
|
231
|
+
const flag = values.booleans.has("--submit") ? "--submit" : values.booleans.has("--accept-all") ? "--accept-all" : null;
|
|
232
|
+
if (action !== "review") {
|
|
233
|
+
if (flag)
|
|
234
|
+
throw new Error(`${flag} belongs to \`cockpit notes review <note-id> ${flag}\`.`);
|
|
235
|
+
return {};
|
|
236
|
+
}
|
|
237
|
+
if (rest.length > 1)
|
|
238
|
+
throw new Error(`notes review takes one note id, not ${rest.length}.`);
|
|
239
|
+
const noteId = optionalNonEmpty(rest[0]);
|
|
240
|
+
if (values.booleans.has("--submit") && values.booleans.has("--accept-all"))
|
|
241
|
+
throw new Error("notes review takes --submit or --accept-all, not both.");
|
|
242
|
+
if (flag && !noteId)
|
|
243
|
+
throw new Error(`notes review ${flag} needs a note id: cockpit notes review <note-id> ${flag}.`);
|
|
244
|
+
return { ...(noteId ? { noteId } : {}), ...(flag === "--submit" ? { submit: true } : {}), ...(flag === "--accept-all" ? { acceptAll: true } : {}) };
|
|
245
|
+
}
|
|
223
246
|
function readRepairTruncated(action, values) {
|
|
224
247
|
const repairTruncated = values.booleans.has("--repair-truncated");
|
|
225
248
|
if (repairTruncated && action !== "sync") {
|
|
@@ -328,6 +351,7 @@ function readNotesPaths(action, rest) {
|
|
|
328
351
|
const takesNoPositional = action !== "paste"
|
|
329
352
|
&& action !== "record"
|
|
330
353
|
&& action !== "app"
|
|
354
|
+
&& action !== "review"
|
|
331
355
|
&& action !== "connect"
|
|
332
356
|
&& action !== "sync"
|
|
333
357
|
&& action !== "sync-status"
|
|
@@ -3,7 +3,7 @@ export const NOTES_HELP = [
|
|
|
3
3
|
[
|
|
4
4
|
"notes",
|
|
5
5
|
[
|
|
6
|
-
"Usage: cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|summarize <id>|move <id>|accounts|connect|detach <id>|sync <id>|sync-status [<id>]|recordings <note-id>|recording <id>|record start|stop|status|cancel|app install|uninstall|status] [flags]",
|
|
6
|
+
"Usage: cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|summarize <id>|move <id>|accounts|connect|detach <id>|sync <id>|sync-status [<id>]|recordings <note-id>|recording <id>|record start|stop|status|cancel|app install|uninstall|status|review [<note-id>] [--submit|--accept-all]] [flags]",
|
|
7
7
|
"Audio uploads: MP3, M4A, WAV, WebM and OGG, up to 1 GB. Over 4 MB (a whole meeting) the file goes straight to storage and is transcribed in the background; --wait then polls for up to 4 hours and prints progress, otherwise for at most 330 seconds.",
|
|
8
8
|
"",
|
|
9
9
|
"record start|stop [<session-id>]|status|cancel — record a meeting in a room from this machine's microphone (Apple Silicon macOS and Windows; Linux says it is not supported). On Windows the microphone needs Settings > Privacy & security > Microphone on, including Let desktop apps access your microphone. Nothing leaves the machine while recording; the recorder stops by itself after 8 hours (under the 1 GB upload limit). `stop` uploads the whole original recording, silence included, and Tower writes a note with Speaker 1, Speaker 2... lines, or says no speech was detected (add --wait to watch the transcription). `cancel` stops and KEEPS the file; `status` lists what is recording and every session waiting for upload (`needs_upload`); `stop <session-id>` uploads a cancelled, crashed or auto-stopped session. The BLI Notetaker app uploads waiting sessions by itself. The recording also stays on this machine.",
|
|
@@ -42,6 +42,7 @@ export const NOTES_HELP = [
|
|
|
42
42
|
"bot-upcoming [--json] — your own Google Meet meetings today that the bot will join, with claim state. Decline or mark private to opt out.",
|
|
43
43
|
"granola-status [--limit <n>] [--json] — the Granola cron's own last several runs (ticket 10718): reason, counts, model, spend and which lookback it used. Granola runs on a shared server-side key, not a connected account, so this is its own read rather than `accounts`.",
|
|
44
44
|
"transcription-stats [--month YYYY-MM] [--json] — what meeting transcription cost and did (BLI-18003): minutes, dollars (list and real), requests, failures and fallbacks per provider for the UTC month (default: this one), the spend against the monthly cap and how close the 80% message and the hold are, how many meetings are waiting for budget, which provider keys the deploy holds (by name), and how old the numbers are. Switch providers or move the cap with `cockpit settings switches set transcription.provider|transcription.fallback_chain|transcription.monthly_cap_usd <value>`.",
|
|
45
|
+
"review [<note-id>] [--submit|--accept-all] [--json] — the meeting reviews waiting for you (BLI-22350). A recorded meeting stays with its attendees until one of them clears one review screen: who's who, lines the raw transcript may have put under the wrong speaker, unclear words, spots where the clean-up may have changed the meaning, sensitive moments. Bare `review` lists yours (waiting, being applied, cleared in the last 7 days). `review <note-id>` shows one in that order with every default answer; --json prints the raw read (raw_hash, items, defaults). `--accept-all` submits every default (names as guessed, lines kept, best-guess readings, the words as said where the meaning may have changed, sensitive moments to attendees only) and prints what it accepted. `--submit` reads { \"raw_hash\": ..., \"answers\": [...] } from stdin: edit the defaults from --json and pipe them in. A refusal says to reload when the review changed (stale_raw_hash, stale_guess) or names what is wrong with the answers. Same door as the web screen (/meeting-notes/<note-id>/review) and the notes_review_* MCP tools.",
|
|
45
46
|
"A large note is read by a model on the server and can take a couple of minutes; the terminal says so before it waits.",
|
|
46
47
|
"--json writes one machine-readable object to stdout; every reason, receipt and progress line stays on stderr.",
|
|
47
48
|
"Sharing and moving need a signed-in session the database can see. If this deployment cannot mint one, they are refused as needs_signed_in_session rather than done with no permission check — and reads say when they came back narrower than the browser's.",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit notes review [<note-id>] [--submit|--accept-all] [--json]` (BLI-22350): the attendee's meeting review in
|
|
3
|
+
* the terminal. Sibling of `notes.ts`, named in its header. Owner doc: `docs/runbooks/meeting-review-screen.md`.
|
|
4
|
+
*
|
|
5
|
+
* review the reviews waiting for me (GET /api/notes/review)
|
|
6
|
+
* review <id> one review, in the screen's order (GET /api/notes/review/<id>); --json is the raw read
|
|
7
|
+
* review <id> --submit `{ raw_hash, answers }` JSON from stdin (POST /api/notes/review/<id>)
|
|
8
|
+
* review <id> --accept-all read, then submit every item's default answer, and print what it accepted
|
|
9
|
+
*
|
|
10
|
+
* The same doors the web screen and the `notes_review_*` MCP tools call; the database decides who is a reviewer. This
|
|
11
|
+
* file holds no rule about answers: the door checks them and its refusal (reason and sentence) is relayed verbatim.
|
|
12
|
+
*/
|
|
13
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
14
|
+
import { ask, emit, errorText, fail, READ_DEADLINE_MS } from "./notes-door.js";
|
|
15
|
+
const WRITE_DEADLINE_MS = 60_000;
|
|
16
|
+
/** A review's answers are a few dozen short objects; this is far above any real one. */
|
|
17
|
+
const SUBMIT_MAX_CHARS = 512_000;
|
|
18
|
+
/** The dashboard's `review-words.ts` sentence, repeated here: this package does not import the dashboard. */
|
|
19
|
+
const AS_TRANSCRIBED_LINE = "This meeting is kept as transcribed; only the speakers need confirming.";
|
|
20
|
+
/** The web screen's plain label per meaning-check issue, repeated here for the same reason. */
|
|
21
|
+
const MEANING_ISSUE_WORDS = {
|
|
22
|
+
number: "A number may have changed",
|
|
23
|
+
negation_or_pronoun: "A 'not' or a 'we/you' may have flipped",
|
|
24
|
+
hedge_or_correction: "A 'maybe' or a correction may have been dropped",
|
|
25
|
+
added_words: "Words may have been added",
|
|
26
|
+
unclear_made_sensible: "An unclear bit may have been made to sound sensible",
|
|
27
|
+
speaker_moved: "A line may have been moved to another speaker",
|
|
28
|
+
passage_left_out: "A passage may have been left out",
|
|
29
|
+
other: "The meaning may have changed",
|
|
30
|
+
};
|
|
31
|
+
const STATE_WORDS = {
|
|
32
|
+
queued: "waiting for Tower to prepare it",
|
|
33
|
+
preparing: "being prepared (transcript cleaned and checked)",
|
|
34
|
+
awaiting_attendee: "waiting for your answers",
|
|
35
|
+
applying: "answered; Tower is applying the answers",
|
|
36
|
+
cleared: "cleared",
|
|
37
|
+
failed: "stuck; an engineer has been told",
|
|
38
|
+
};
|
|
39
|
+
const reviewPath = (noteId) => `/api/notes/review/${encodeURIComponent(noteId)}`;
|
|
40
|
+
export async function runNotesReview(command, door) {
|
|
41
|
+
if (!command.noteId)
|
|
42
|
+
return listReviews(door);
|
|
43
|
+
if (command.submit)
|
|
44
|
+
return submitFromStdin(command.noteId, door);
|
|
45
|
+
if (command.acceptAll)
|
|
46
|
+
return acceptAll(command.noteId, door);
|
|
47
|
+
return showReview(command.noteId, door);
|
|
48
|
+
}
|
|
49
|
+
async function listReviews(door) {
|
|
50
|
+
const answer = await ask(door, { path: "/api/notes/review", method: "GET", label: "notes review", timeoutMs: READ_DEADLINE_MS });
|
|
51
|
+
if (!answer.ok)
|
|
52
|
+
return fail(door, answer.reason, answer.detail);
|
|
53
|
+
const body = answer.body;
|
|
54
|
+
if (door.json)
|
|
55
|
+
return emit(door, { ok: true, ...body });
|
|
56
|
+
const out = door.io.stdout;
|
|
57
|
+
if (body.reviews.length === 0) {
|
|
58
|
+
writeLine(out, "No meeting reviews for you: nothing waits, and nothing was cleared in the last 7 days.");
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
writeLine(out, `${body.waiting} meeting(s) wait for your review.`);
|
|
62
|
+
for (const row of body.reviews) {
|
|
63
|
+
writeLine(out, `${row.note_id} ${row.meeting_date} ${row.item_count} item(s) ${STATE_WORDS[row.state] ?? row.state} ${row.note_title ?? "(untitled meeting)"}`);
|
|
64
|
+
}
|
|
65
|
+
writeLine(out, `Open one: cockpit notes review <note-id>, or ${door.dashboardUrl}/meeting-notes/<note-id>/review in a browser.`);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
async function readOne(noteId, door) {
|
|
69
|
+
return ask(door, { path: reviewPath(noteId), method: "GET", label: "notes review", timeoutMs: READ_DEADLINE_MS });
|
|
70
|
+
}
|
|
71
|
+
function lines(value) {
|
|
72
|
+
if (value === null || value === undefined)
|
|
73
|
+
return [];
|
|
74
|
+
if (typeof value === "string")
|
|
75
|
+
return value.trim() ? [value.trim()] : [];
|
|
76
|
+
if (Array.isArray(value))
|
|
77
|
+
return value.flatMap((entry) => (typeof entry === "string" ? [entry] : [JSON.stringify(entry)]));
|
|
78
|
+
return [JSON.stringify(value)];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The review in the screen's order: who's who, lines under the wrong speaker, unclear words, meaning checks,
|
|
82
|
+
* sensitive moments.
|
|
83
|
+
*/
|
|
84
|
+
/** A meaning check's stretch is one or more transcript lines: the label on the first, the rest indented under it. */
|
|
85
|
+
function stretch(label, text) {
|
|
86
|
+
const [first = "", ...rest] = text.split(/\r?\n/);
|
|
87
|
+
const pad = " ".repeat(label.length + 5);
|
|
88
|
+
return [` ${label} ${first}`, ...rest.map((line) => `${pad}${line}`)];
|
|
89
|
+
}
|
|
90
|
+
export function renderReview(body, dashboardUrl) {
|
|
91
|
+
const out = [];
|
|
92
|
+
out.push(`${body.note_title ?? "Meeting"}${body.meeting_date ? `, ${body.meeting_date}` : ""}: ${STATE_WORDS[body.state] ?? body.state}.`);
|
|
93
|
+
const of = (kind) => body.items.filter((item) => item.kind === kind);
|
|
94
|
+
const speakers = of("speaker");
|
|
95
|
+
if (speakers.length)
|
|
96
|
+
out.push("", "Who's who (the name you confirm or correct):");
|
|
97
|
+
for (const item of speakers) {
|
|
98
|
+
out.push(` ${item.label}: ${item.guess || "(no guess; leave empty to keep the label)"} [${item.id}]`, ` why: ${item.reasoning}`);
|
|
99
|
+
for (const quote of item.quotes ?? [])
|
|
100
|
+
out.push(` ${quote.timestamp} "${quote.text}"`);
|
|
101
|
+
}
|
|
102
|
+
const moved = of("speaker_line");
|
|
103
|
+
if (moved.length)
|
|
104
|
+
out.push("", "Lines that may be under the wrong speaker (default: keep):");
|
|
105
|
+
for (const item of moved)
|
|
106
|
+
out.push(` ${item.timestamp} under ${item.guess || "(no speaker)"}: "${item.quote.text}" [${item.id}]`);
|
|
107
|
+
const unclear = of("unclear");
|
|
108
|
+
if (unclear.length)
|
|
109
|
+
out.push("", "Unclear words (default: the best guess):");
|
|
110
|
+
for (const item of unclear) {
|
|
111
|
+
out.push(` ${item.timestamp} "${item.words}" -> ${item.guess || "(keep marked unclear)"}${item.changesMeaning ? " CHANGES THE MEANING" : ""} [${item.id}]`, ` context: ${item.context}`);
|
|
112
|
+
}
|
|
113
|
+
const meaning = of("meaning_check");
|
|
114
|
+
if (meaning.length)
|
|
115
|
+
out.push("", "Did the clean-up change the meaning? (default: keep the words as said; answer \"raw\" or \"cleaned\"):");
|
|
116
|
+
for (const item of meaning) {
|
|
117
|
+
out.push(` ${item.timestamp} ${MEANING_ISSUE_WORDS[item.issue] ?? MEANING_ISSUE_WORDS.other} [${item.id}]`);
|
|
118
|
+
if (item.said || item.written)
|
|
119
|
+
out.push(` "${item.said}" -> "${item.written}"`);
|
|
120
|
+
out.push(...stretch("as said: ", item.raw), ...stretch("as cleaned up:", item.cleaned));
|
|
121
|
+
}
|
|
122
|
+
const sensitive = of("sensitive");
|
|
123
|
+
if (sensitive.length)
|
|
124
|
+
out.push("", "Sensitive moments (default: attendees only):");
|
|
125
|
+
for (const item of sensitive)
|
|
126
|
+
out.push(` ${item.timestamp} ${item.what} [${item.id}]`);
|
|
127
|
+
if (body.as_transcribed)
|
|
128
|
+
out.push("", AS_TRANSCRIBED_LINE);
|
|
129
|
+
else {
|
|
130
|
+
for (const [heading, value] of [["Summary", body.summary], ["Decisions", body.decisions], ["Action items", body.action_items]]) {
|
|
131
|
+
const said = lines(value);
|
|
132
|
+
if (said.length)
|
|
133
|
+
out.push("", `${heading}:`, ...said.map((line) => ` ${line}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (body.state === "cleared" && body.attendee_notes)
|
|
137
|
+
out.push("", "Kept for the attendees only:", ...body.attendee_notes.split("\n").map((line) => ` ${line}`));
|
|
138
|
+
if (body.state === "awaiting_attendee") {
|
|
139
|
+
out.push("", `Answer: cockpit notes review ${body.note_id} --accept-all (every default above), or edit the defaults from`, `cockpit notes review ${body.note_id} --json and pipe { "raw_hash", "answers" } to cockpit notes review ${body.note_id} --submit.`, `Or open ${dashboardUrl}/meeting-notes/${body.note_id}/review.`);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
async function showReview(noteId, door) {
|
|
144
|
+
const answer = await readOne(noteId, door);
|
|
145
|
+
if (!answer.ok)
|
|
146
|
+
return fail(door, answer.reason, answer.detail);
|
|
147
|
+
if (door.json)
|
|
148
|
+
return emit(door, answer.body);
|
|
149
|
+
for (const line of renderReview(answer.body, door.dashboardUrl))
|
|
150
|
+
writeLine(door.io.stdout, line);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
async function post(noteId, door, body, accepted) {
|
|
154
|
+
const answer = await ask(door, { path: reviewPath(noteId), method: "POST", label: "notes review submit", timeoutMs: WRITE_DEADLINE_MS, body });
|
|
155
|
+
if (!answer.ok)
|
|
156
|
+
return fail(door, answer.reason, answer.detail);
|
|
157
|
+
const done = answer.body;
|
|
158
|
+
if (door.json)
|
|
159
|
+
return emit(door, { ok: true, ...done, ...(accepted ? { accepted } : {}) });
|
|
160
|
+
writeLine(door.io.stdout, `Submitted ${done.answers} answer(s). Tower is applying them; the meeting is released to the team once its privacy check passes.`);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
async function submitFromStdin(noteId, door) {
|
|
164
|
+
if (isInteractiveStdin(door.io)) {
|
|
165
|
+
return fail(door, "nothing_piped", `Pipe { "raw_hash": ..., "answers": [...] } in: cockpit notes review ${noteId} --json, edit the defaults, then pipe them to cockpit notes review ${noteId} --submit.`);
|
|
166
|
+
}
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = JSON.parse(await readPipedText(door.io.stdin, { maxChars: SUBMIT_MAX_CHARS, overflowMessage: "The answers piped in are far larger than any review." }));
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return fail(door, "invalid_body", `The answers piped in are not JSON (${errorText(error)}). Send { "raw_hash": ..., "answers": [...] }.`);
|
|
173
|
+
}
|
|
174
|
+
return post(noteId, door, parsed);
|
|
175
|
+
}
|
|
176
|
+
function acceptedWords(answer) {
|
|
177
|
+
const value = answer.answer;
|
|
178
|
+
if (answer.kind === "speaker")
|
|
179
|
+
return `speaker ${answer.guess || "(no guess)"} -> ${value === "" ? "keep the label" : String(value)}`;
|
|
180
|
+
if (answer.kind === "speaker_line")
|
|
181
|
+
return "line stays where it is";
|
|
182
|
+
if (answer.kind === "unclear") {
|
|
183
|
+
const reading = value.reading;
|
|
184
|
+
return reading === null ? "unclear word stays marked" : `unclear word -> ${reading}`;
|
|
185
|
+
}
|
|
186
|
+
if (answer.kind === "meaning_check")
|
|
187
|
+
return value === "cleaned" ? "meaning check -> keep the clean-up's words" : "meaning check -> keep the words as said";
|
|
188
|
+
return `sensitive moment -> ${String(value) === "keep_in" ? "kept in" : "attendees only"}`;
|
|
189
|
+
}
|
|
190
|
+
async function acceptAll(noteId, door) {
|
|
191
|
+
const read = await readOne(noteId, door);
|
|
192
|
+
if (!read.ok)
|
|
193
|
+
return fail(door, read.reason, read.detail);
|
|
194
|
+
const review = read.body;
|
|
195
|
+
if (review.state !== "awaiting_attendee") {
|
|
196
|
+
return fail(door, "review_not_awaiting_attendee", `This review is ${STATE_WORDS[review.state] ?? review.state}, so there is nothing to answer.`);
|
|
197
|
+
}
|
|
198
|
+
if (!door.json) {
|
|
199
|
+
writeLine(door.io.stdout, `Accepting ${review.defaults.length} default answer(s):`);
|
|
200
|
+
for (const answer of review.defaults)
|
|
201
|
+
writeLine(door.io.stdout, ` ${answer.item_id}: ${acceptedWords(answer)}`);
|
|
202
|
+
}
|
|
203
|
+
return post(noteId, door, { raw_hash: review.raw_hash, answers: review.defaults }, review.defaults);
|
|
204
|
+
}
|
package/dist/commands/notes.js
CHANGED
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
* Notetaker app on this Mac or Windows PC (Mac LaunchAgent: `notes-app-plist.ts`; Windows: `notes-app-windows.ts`).
|
|
41
41
|
* - `notes-transcription-stats.ts` — BLI-18003: what meeting transcription
|
|
42
42
|
* cost and did per provider for a month, and the spend against the cap.
|
|
43
|
+
* - `notes-review.ts` — BLI-22350: the meeting reviews waiting for me, one
|
|
44
|
+
* review, and answering it (`--submit` from stdin, `--accept-all`).
|
|
43
45
|
*
|
|
44
46
|
* Every public name is still importable from this file.
|
|
45
47
|
*/
|
|
@@ -52,6 +54,7 @@ import { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts,
|
|
|
52
54
|
import { showBotUpcoming } from "./notes-bot-upcoming.js";
|
|
53
55
|
import { showGranolaStatus } from "./notes-granola-status.js";
|
|
54
56
|
import { showTranscriptionStats } from "./notes-transcription-stats.js";
|
|
57
|
+
import { runNotesReview } from "./notes-review.js";
|
|
55
58
|
import { listRecordings, showRecording } from "./notes-recordings.js";
|
|
56
59
|
import { defaultRecordDeps, runNotesRecord } from "./notes-record.js";
|
|
57
60
|
import { runNotesApp } from "./notes-app.js";
|
|
@@ -94,6 +97,7 @@ const NOTES_VERBS = {
|
|
|
94
97
|
"bot-upcoming": showBotUpcoming,
|
|
95
98
|
"granola-status": showGranolaStatus,
|
|
96
99
|
"transcription-stats": showTranscriptionStats,
|
|
100
|
+
review: runNotesReview,
|
|
97
101
|
};
|
|
98
102
|
export async function runNotes(command, io) {
|
|
99
103
|
// BLI-22979: recording is local until `stop` uploads, so only the upload needs the paired session.
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.283");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.283",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@bli-cockpit/memory-mcp": "0.1.88",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.172",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.94",
|
|
33
33
|
"unpdf": "1.8.1"
|
|
34
34
|
},
|