@bli-cockpit/cli 0.2.288 → 0.2.290
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/books-files.js +44 -9
- package/dist/commands/books-usage.js +2 -1
- package/dist/commands/local-args-tower-notes.js +11 -5
- package/dist/commands/local-help-commands-tower-ops.js +1 -1
- package/dist/commands/local-help-notes.js +1 -0
- package/dist/commands/manager-read-reasons.js +3 -2
- package/dist/commands/manager-render-home.js +14 -3
- package/dist/commands/manager-render.js +6 -3
- package/dist/commands/manager.js +1 -1
- package/dist/commands/notes-record-capture.js +6 -2
- package/dist/commands/notes-sync-loop.js +6 -4
- package/dist/commands/public-root.js +1 -1
- package/package.json +2 -2
|
@@ -52,8 +52,10 @@ export async function exportBook(door, book, dir) {
|
|
|
52
52
|
if (!(await isEmptyOrMissing(dir)))
|
|
53
53
|
return failAgentDoor(door, TAG, "export_dir_not_empty", `${dir} is not empty. Export into a new or empty folder so no old record is mixed in.`);
|
|
54
54
|
const counts = {};
|
|
55
|
+
const pending = [];
|
|
55
56
|
for (const collection of BOOK_COLLECTIONS) {
|
|
56
57
|
counts[collection] = 0;
|
|
58
|
+
const cursors = new Set();
|
|
57
59
|
let after = null;
|
|
58
60
|
do {
|
|
59
61
|
const query = new URLSearchParams({ collection, limit: "500" });
|
|
@@ -61,17 +63,31 @@ export async function exportBook(door, book, dir) {
|
|
|
61
63
|
query.set("after", after);
|
|
62
64
|
const answer = await booksRequest(door, "GET", `/api/books/${encodeURIComponent(book)}/records?${query}`, "export");
|
|
63
65
|
if (!answer.ok)
|
|
64
|
-
return failAgentDoor(door, TAG, answer.reason, answer.detail, { collection,
|
|
66
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, { collection, read: counts });
|
|
65
67
|
const page = answer.body;
|
|
66
|
-
if (page.records.length > 0)
|
|
67
|
-
await mkdir(join(dir, collection), { recursive: true });
|
|
68
68
|
for (const record of page.records) {
|
|
69
|
-
|
|
69
|
+
pending.push({ collection, ...record });
|
|
70
70
|
counts[collection] += 1;
|
|
71
71
|
}
|
|
72
72
|
after = page.next_cursor;
|
|
73
|
+
if (after !== null && cursors.has(after))
|
|
74
|
+
return failAgentDoor(door, TAG, "invalid_cursor", "The export door repeated a cursor. No files were written.");
|
|
75
|
+
if (after !== null)
|
|
76
|
+
cursors.add(after);
|
|
73
77
|
} while (after !== null);
|
|
74
78
|
}
|
|
79
|
+
// Validate the entire manifest before creating a single file on a case-insensitive filesystem.
|
|
80
|
+
const names = new Set();
|
|
81
|
+
for (const record of pending) {
|
|
82
|
+
const name = `${record.collection}/${fileNameFor(record.doc_id)}`.toLowerCase();
|
|
83
|
+
if (names.has(name))
|
|
84
|
+
return failAgentDoor(door, TAG, "export_id_collision", "Duplicate or case-only record ids cannot be exported safely.");
|
|
85
|
+
names.add(name);
|
|
86
|
+
}
|
|
87
|
+
for (const record of pending) {
|
|
88
|
+
await mkdir(join(dir, record.collection), { recursive: true });
|
|
89
|
+
await writeFile(join(dir, record.collection, `${fileNameFor(record.doc_id)}.json`), `${JSON.stringify(sorted(record.data), null, 2)}\n`, "utf8");
|
|
90
|
+
}
|
|
75
91
|
const total = Object.values(counts).reduce((sum, n) => sum + n, 0);
|
|
76
92
|
console.error(`${TAG} exported ${JSON.stringify({ book, total })}`);
|
|
77
93
|
if (door.json)
|
|
@@ -84,15 +100,14 @@ export async function exportBook(door, book, dir) {
|
|
|
84
100
|
async function readLayout(dir) {
|
|
85
101
|
const writes = [];
|
|
86
102
|
const skipped = [];
|
|
103
|
+
const ids = new Set();
|
|
87
104
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
88
105
|
if (!entry.isDirectory() || !BOOK_COLLECTIONS.includes(entry.name)) {
|
|
89
|
-
skipped
|
|
90
|
-
continue;
|
|
106
|
+
throw new Error(`Unknown folder or skipped file: ${entry.name}. Nothing was imported.`);
|
|
91
107
|
}
|
|
92
108
|
for (const file of (await readdir(join(dir, entry.name))).sort()) {
|
|
93
109
|
if (!file.endsWith(".json")) {
|
|
94
|
-
|
|
95
|
-
continue;
|
|
110
|
+
throw new Error(`Skipped file: ${entry.name}/${file}. Nothing was imported.`);
|
|
96
111
|
}
|
|
97
112
|
const text = (await readFile(join(dir, entry.name, file), "utf8")).replace(/^/, "");
|
|
98
113
|
let data;
|
|
@@ -102,7 +117,18 @@ async function readLayout(dir) {
|
|
|
102
117
|
catch {
|
|
103
118
|
throw new Error(`${entry.name}/${file} is not valid JSON.`);
|
|
104
119
|
}
|
|
105
|
-
|
|
120
|
+
const docId = docIdFor(file.slice(0, -".json".length));
|
|
121
|
+
if (!docId || docId.length > 200 || /[/\u0000-\u001f\u007f-\u009f]/.test(docId))
|
|
122
|
+
throw new Error(`${entry.name}/${file} has an invalid record id.`);
|
|
123
|
+
const key = `${entry.name}/${docId}`.toLowerCase();
|
|
124
|
+
if (ids.has(key))
|
|
125
|
+
throw new Error(`Duplicate or case-only id collision: ${entry.name}/${docId}. Nothing was imported.`);
|
|
126
|
+
ids.add(key);
|
|
127
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
128
|
+
throw new Error(`${entry.name}/${file} must contain a JSON object.`);
|
|
129
|
+
if (Buffer.byteLength(JSON.stringify(data), "utf8") > 262_144)
|
|
130
|
+
throw new Error(`${entry.name}/${file} exceeds the 256 KiB record limit.`);
|
|
131
|
+
writes.push({ op: "put", collection: entry.name, doc_id: docId, data, merge: false, expected_version: 0 });
|
|
106
132
|
}
|
|
107
133
|
}
|
|
108
134
|
return { writes, skipped };
|
|
@@ -135,6 +161,15 @@ export async function importBook(door, book, dir) {
|
|
|
135
161
|
return failAgentDoor(door, TAG, "import_read_failed", error.message);
|
|
136
162
|
}
|
|
137
163
|
const counts = Object.fromEntries(BOOK_COLLECTIONS.map((c) => [c, 0]));
|
|
164
|
+
// Include tombstones: an import must never revive or overwrite any existing record.
|
|
165
|
+
for (const collection of BOOK_COLLECTIONS) {
|
|
166
|
+
const query = new URLSearchParams({ collection, limit: "1", include_deleted: "1" });
|
|
167
|
+
const answer = await booksRequest(door, "GET", `/api/books/${encodeURIComponent(book)}/records?${query}`, "import check");
|
|
168
|
+
if (!answer.ok)
|
|
169
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
170
|
+
if (answer.body.records.length)
|
|
171
|
+
return failAgentDoor(door, TAG, "import_book_not_empty", "This book already has records. Import requires an empty book; use versioned puts to recover a partial import.");
|
|
172
|
+
}
|
|
138
173
|
const batches = batchesOf(layout.writes);
|
|
139
174
|
for (const [index, batch] of batches.entries()) {
|
|
140
175
|
const answer = await booksRequest(door, "POST", `/api/books/${encodeURIComponent(book)}/batch`, "import", { writes: batch });
|
|
@@ -9,6 +9,7 @@ export const BOOKS_USAGE = [
|
|
|
9
9
|
" cockpit books delete <book> <collection> <doc_id> [--version <n>] [--json] (soft: the record is kept)",
|
|
10
10
|
" cockpit books batch <book> [--json] ({\"writes\": [...]} on stdin, 1 to 50 writes, all or nothing)",
|
|
11
11
|
" cockpit books export <book> <dir> [--json] (<dir>/<collection>/<doc_id>.json)",
|
|
12
|
-
" cockpit books import <book> <dir> [--json] (
|
|
12
|
+
" cockpit books import <book> <dir> [--json] (empty book only; validated export layout, through the batch door)",
|
|
13
|
+
"Import refuses existing records (including deleted), unknown folders, skipped files and duplicate or case-only ids. Partial batches require versioned puts for recovery.",
|
|
13
14
|
"Collections: meta, timeline, actions, documents, meetings, conversations, workstreams, sources, syncs.",
|
|
14
15
|
].join("\n");
|
|
@@ -111,6 +111,7 @@ const NOTES_ALLOWED_FLAGS = [
|
|
|
111
111
|
// transcript was stored cut short. A FLAG, not a verb: it is the same sync
|
|
112
112
|
// door with a different request.
|
|
113
113
|
"--repair-truncated",
|
|
114
|
+
"--repair-empty", // BLI-23914: re-read Wispr notes filed with no transcript (meeting still running).
|
|
114
115
|
// BLI-22350. `review <note-id> --submit` reads `{ raw_hash, answers }` from stdin; `--accept-all` sends the defaults.
|
|
115
116
|
"--submit",
|
|
116
117
|
"--accept-all",
|
|
@@ -153,7 +154,7 @@ export function parseNotesArgs(args) {
|
|
|
153
154
|
if (action === "rename" && !optionalNonEmpty(values.flags.get("--name")))
|
|
154
155
|
throw new Error("notes rename needs --name <name>.");
|
|
155
156
|
const { to, clearShelf } = readNotesShelfTarget(action, values);
|
|
156
|
-
const repairTruncated =
|
|
157
|
+
const { repairTruncated, repairEmpty } = readRepairFlags(action, values);
|
|
157
158
|
const review = readReviewRequest(action, values, rest);
|
|
158
159
|
const { limit, month, offset } = readNotesWindow(action, values);
|
|
159
160
|
return {
|
|
@@ -189,6 +190,7 @@ export function parseNotesArgs(args) {
|
|
|
189
190
|
...(month ? { month } : {}),
|
|
190
191
|
...(offset === undefined ? {} : { offset }),
|
|
191
192
|
...(repairTruncated ? { repairTruncated: true } : {}),
|
|
193
|
+
...(repairEmpty ? { repairEmpty: true } : {}),
|
|
192
194
|
...review,
|
|
193
195
|
yes,
|
|
194
196
|
json,
|
|
@@ -243,12 +245,16 @@ function readReviewRequest(action, values, rest) {
|
|
|
243
245
|
throw new Error(`notes review ${flag} needs a note id: cockpit notes review <note-id> ${flag}.`);
|
|
244
246
|
return { ...(noteId ? { noteId } : {}), ...(flag === "--submit" ? { submit: true } : {}), ...(flag === "--accept-all" ? { acceptAll: true } : {}) };
|
|
245
247
|
}
|
|
246
|
-
function
|
|
248
|
+
function readRepairFlags(action, values) {
|
|
247
249
|
const repairTruncated = values.booleans.has("--repair-truncated");
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
const repairEmpty = values.booleans.has("--repair-empty");
|
|
251
|
+
for (const [on, flag] of [[repairTruncated, "--repair-truncated"], [repairEmpty, "--repair-empty"]]) {
|
|
252
|
+
if (on && action !== "sync")
|
|
253
|
+
throw new Error(`${flag} belongs to \`cockpit notes sync <account-id> ${flag}\`.`);
|
|
250
254
|
}
|
|
251
|
-
|
|
255
|
+
if (repairEmpty && repairTruncated)
|
|
256
|
+
throw new Error("notes sync takes --repair-truncated or --repair-empty, not both.");
|
|
257
|
+
return { repairTruncated, repairEmpty };
|
|
252
258
|
}
|
|
253
259
|
function readNotesConfirmation(values) {
|
|
254
260
|
const json = values.booleans.has("--json");
|
|
@@ -217,7 +217,7 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
217
217
|
"Tower Manager's clients, from the terminal: each client's Home and \"Where we are\", its actions and its timeline items, the same things the manager page does.",
|
|
218
218
|
" cockpit manager clients every client, with its id",
|
|
219
219
|
" cockpit manager client <client> one client: state, last read, pages waiting; run <run-id> one run's receipt (a summary check says what it did to the card, not \"0 pages seen\"). A run id is a UUID from `queue <client> --json` (a row's changedByRunId), `show <id>` (a suggested change's runId) or `client <client> --json` (lastBriefRebuild.runId); a read's workflow id (lastRead.workflowRunId, like sched_wf_…) is not one",
|
|
220
|
-
" cockpit manager home <client> the client's Home: how fresh its data is (last read, when
|
|
220
|
+
" cockpit manager home <client> the client's Home: how fresh its data is (last read, when its pages last changed), \"Where we are\" (health, headline, up to 5 dated bullets,",
|
|
221
221
|
" next step, blocker, who wrote it and when), live counts, what needs attention by owner in two parts, \"Due this week\" first (today to six days ahead) then \"Overdue\" (rows sit under their owners, oldest first under each; a line \"Overdue longest: <title>, due <day>\" names the one overdue longest), each with its own total and, when cut, \"Showing X of N\" and the `list` command for the rest, what is coming up,",
|
|
222
222
|
" what the newest read that changed anything did (proposals first), \"In Tower, last 7 days\" (what people and agents added, closed, dropped and dismissed, and who, by name; a count of none is left out, and when all are none: \"nothing added, closed, dropped or dismissed by people or agents\"), and a What changed tally that likewise leaves out a kind with none (\"12 added, 2 proposed\"; \"nothing changed\" when none), and the review line (\"N suggested changes need a decision · M new items to check\"; `queue <client>` lists them), and how many sync changes are \"new from syncs\" (`queue <client>` lists them; `list` counts every unreviewed item as \"need review\", the first read's included).",
|
|
223
223
|
" Home also says how old the last check of the card is (\"Last checked 2 h ago\"), \"The last check failed; showing the card from Oct 5\" when it did, \"Last checked\" only once a check has looked at the card (a card a person wrote has none yet), a failed read's reason in the page\'s own sentence (\"Why the latest read failed: ...\"), and \"First read not done yet\" for a client no read has finished (\"First read failed\" when it failed: the manager page shows only that failure until a read succeeds, though writes made here are saved); a card computed from counts reads \"AI unavailable · computed from counts\".",
|
|
@@ -38,6 +38,7 @@ export const NOTES_HELP = [
|
|
|
38
38
|
"detach <account-id> — take it off. The stored key goes with it; the meetings it already filed stay.",
|
|
39
39
|
"sync <account-id> — read that notetaker now. Prints the run's own counts and reason label, and exits 1 when the pass failed.",
|
|
40
40
|
"sync <account-id> --repair-truncated — re-read Wispr Flow notes that were stored with only the first part of a long transcript (the \"truncated, N chars remaining\" line), and rewrite them whole. Same door and the same bounded passes; prints found, fixed and failed with each failure's reason, and is safe to run again (BLI-18006).",
|
|
41
|
+
"sync <account-id> --repair-empty — re-read Wispr Flow notes that were filed with no transcript because the meeting was still running when Tower read it (last 7 days only). Wispr finalizes the transcript about 20 minutes later; this writes it in and re-runs the longest-transcript pick. Same door and the same bounded passes; prints found, fixed and failed with each failure's reason, and is safe to run again (BLI-23914).",
|
|
41
42
|
"sync-status [<account-id>] — every notetaker's health, or one's: last successful sync, the last pass's own reason, how many meetings it carried, failures in the last 24h, and the lag between the newest meeting it has read and when it last ran (ticket 10730).",
|
|
42
43
|
"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
44
|
"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`.",
|
|
@@ -12,12 +12,13 @@ export const READ_REASON_SENTENCES = {
|
|
|
12
12
|
client_disabled: "This client is disabled.",
|
|
13
13
|
notion_not_configured: "Notion is not connected on this Tower deployment.",
|
|
14
14
|
notion_root_unreadable: "Tower cannot read this client's Notion page. Share it with the Tower Manager Dashboard integration again, then sync.",
|
|
15
|
+
tower_root_unreadable: "Tower cannot find this client's main page: it was deleted or is in the trash. Restore it in Tower, then sync.",
|
|
15
16
|
notion_unavailable: "Notion did not answer. The next sync will try again.",
|
|
16
17
|
model_failed: "The model did not answer. Nothing was lost; the next sync reads the same pages again.",
|
|
17
18
|
model_timeout: "The model took too long to answer. Nothing was lost; the next sync reads the same pages again, fewer at a time.",
|
|
18
19
|
model_output_rejected: "The model's answer was not usable. Nothing was lost; the next sync reads the same pages again.",
|
|
19
|
-
root_owned_by_other_client: "This client's
|
|
20
|
-
interrupted: "Interrupted when Tower restarted; will retry. The
|
|
20
|
+
root_owned_by_other_client: "This client's page sits inside another client's tree, which holds it. Nothing was read.",
|
|
21
|
+
interrupted: "Interrupted when Tower restarted; will retry. The scheduled sync carries it on, or Sync now.",
|
|
21
22
|
batch_too_dense: "Those pages held more than one answer can carry, so they are being read again in smaller batches.",
|
|
22
23
|
database_error: "Tower could not save the result. The next sync will try again.",
|
|
23
24
|
unexpected_error: "The run stopped on an unexpected error. The next sync will try again.",
|
|
@@ -122,10 +122,18 @@ export function coverageLines(client) {
|
|
|
122
122
|
return [
|
|
123
123
|
walk && !walk.complete ? `Walk incomplete (${walk.cap ? CAP_WORDS[walk.cap] : "stopped early"}): archiving skipped.` : null,
|
|
124
124
|
walk && walk.pageReadFailed > 0 ? `${plural(walk.pageReadFailed, "page")} could not be read this time; the next sync tries again.` : null,
|
|
125
|
-
walk && walk.unreadableSkipped > 0
|
|
125
|
+
walk && walk.unreadableSkipped > 0
|
|
126
|
+
? client.source === "docs"
|
|
127
|
+
? `${plural(walk.unreadableSkipped, "page")} gone from Tower since the sync began; the next sync tries again.`
|
|
128
|
+
: `${plural(walk.unreadableSkipped, "page")} not shared with the Tower integration.`
|
|
129
|
+
: null,
|
|
126
130
|
walk && walk.otherClientRoots > 0 ? `Stopped at ${walk.otherClientRoots} other ${walk.otherClientRoots === 1 ? "client's tree" : "clients' trees"}.` : null,
|
|
127
131
|
walk && walk.otherClientSkipped > 0 ? `${plural(walk.otherClientSkipped, "page")} held by another client.` : null,
|
|
128
|
-
incomplete > 0
|
|
132
|
+
incomplete > 0
|
|
133
|
+
? client.source === "docs"
|
|
134
|
+
? `${plural(incomplete, "page")} came into Tower from Notion only in part.`
|
|
135
|
+
: `Notion handed over ${plural(incomplete, "page")} only in part.`
|
|
136
|
+
: null,
|
|
129
137
|
idle && waiting > 0 ? `${plural(waiting, "page")} still waiting for the next read.` : null,
|
|
130
138
|
].filter((line) => line !== null);
|
|
131
139
|
}
|
|
@@ -137,7 +145,10 @@ function freshnessLines(home, now) {
|
|
|
137
145
|
? `failed ${when(read.finishedAt ?? read.startedAt, now)}; ${synced ? `last good read ${when(synced, now)}` : "no good read yet"}`
|
|
138
146
|
: null;
|
|
139
147
|
const lastRead = failed ?? (read ? `last read ${when(read.finishedAt ?? read.startedAt, now)}${read.finishedAt ? "" : ", still running"}` : synced ? `last synced ${when(synced, now)}` : "never read");
|
|
140
|
-
const
|
|
148
|
+
const sourceName = home.client.source === "docs" ? "Tower" : "Notion";
|
|
149
|
+
const notion = home.freshness.notionLastEditedAt
|
|
150
|
+
? `${home.client.source === "docs" ? "Tower pages" : "Notion"} last changed ${when(home.freshness.notionLastEditedAt, now)}`
|
|
151
|
+
: `no ${sourceName} page yet`;
|
|
141
152
|
// What the read could not see sits under the Data line, so "fresh" never hides it.
|
|
142
153
|
return [`Data: ${FRESHNESS[home.freshness.state] ?? home.freshness.state}. ${lastRead[0].toUpperCase()}${lastRead.slice(1)}; ${notion}.`, ...coverageLines(home.client).map((line) => ` ${line}`), ...(failed && read?.reason ? [` Why the latest read failed: ${readReasonText(read.reason)}`] : [])];
|
|
143
154
|
}
|
|
@@ -220,12 +220,15 @@ export function itemDetailLines(detail, timeline, color) {
|
|
|
220
220
|
/** One client in full: its state, its last read and how many pages wait. */
|
|
221
221
|
export function clientDetailLines(client, color) {
|
|
222
222
|
const lines = [...clientLines([client], color)];
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
const source = client.source === "docs" ? "Tower" : "Notion";
|
|
224
|
+
lines.push(` Read from: ${source} pages`);
|
|
225
|
+
const root = client.rootUrl ?? client.notionRootUrl;
|
|
226
|
+
if (root)
|
|
227
|
+
lines.push(` ${source} root: ${root}`);
|
|
225
228
|
if (client.queued)
|
|
226
229
|
lines.push(" Queued behind another run (Sync all).");
|
|
227
230
|
if (client.storedPages !== undefined)
|
|
228
|
-
lines.push(` ${client.storedPages} stored pages${client.incompletePages ? `, ${client.incompletePages} only partly handed over by Notion` : ""}`);
|
|
231
|
+
lines.push(` ${client.storedPages} stored pages${client.incompletePages ? `, ${client.incompletePages} only partly handed over${client.source === "docs" ? " when they came into Tower from Notion" : " by Notion"}` : ""}`);
|
|
229
232
|
const read = client.lastRead;
|
|
230
233
|
if (read) {
|
|
231
234
|
lines.push(` Last read: ${read.kind}, ${read.reason}, started ${managerTime(read.startedAt)}${read.finishedAt ? `, finished ${managerTime(read.finishedAt)}` : ", still running"}`);
|
package/dist/commands/manager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `cockpit manager` (BLI-5006): Tower Manager's client actions and timeline items from the terminal.
|
|
3
3
|
*
|
|
4
|
-
* Plain words: Tower reads a client's Notion
|
|
4
|
+
* Plain words: Tower reads a client's pages (from Tower, or from Notion) and keeps their actions and timeline. This lists
|
|
5
5
|
* them, adds and edits them, ticks them off, links an action to the meeting it came from, and
|
|
6
6
|
* decides the AI's suggestions, the same things the manager page does.
|
|
7
7
|
*
|
|
@@ -89,11 +89,15 @@ function fileBytes(file) {
|
|
|
89
89
|
return typeof size === 'number' ? size : 0;
|
|
90
90
|
} catch (e) { return 0; }
|
|
91
91
|
}
|
|
92
|
+
// macOS 27 no longer exposes AVCaptureDevice on \$ after importing AVFoundation; by name it carries no bridge
|
|
93
|
+
// metadata, so the status comes back as the string "3" and Number() is what makes === work (BLI-23872).
|
|
94
|
+
var CaptureDevice = $.AVCaptureDevice || $.NSClassFromString('AVCaptureDevice');
|
|
95
|
+
function micStatus() { return Number(CaptureDevice.authorizationStatusForMediaType('soun')); }
|
|
92
96
|
${RECORDER_STOP_RULE}
|
|
93
97
|
function run(argv) {
|
|
94
98
|
var dir = argv[0];
|
|
95
99
|
var startedMs = Date.now();
|
|
96
|
-
var auth =
|
|
100
|
+
var auth = micStatus();
|
|
97
101
|
if (auth === 1 || auth === 2) { writeText(dir + '/failed.txt', 'mic_denied'); $.exit(3); }
|
|
98
102
|
var settings = $.NSDictionary.dictionaryWithObjectsForKeys(
|
|
99
103
|
$([1819304813, ${RECORDING_SAMPLE_RATE}, 1, 16, 0, 0]),
|
|
@@ -105,7 +109,7 @@ function run(argv) {
|
|
|
105
109
|
while (!exists(dir + '/stop')) {
|
|
106
110
|
tick(0.2);
|
|
107
111
|
waited += 0.2;
|
|
108
|
-
var now =
|
|
112
|
+
var now = micStatus();
|
|
109
113
|
if (now === 1 || now === 2) { recorder.stop; writeText(dir + '/failed.txt', 'mic_denied'); $.exit(3); }
|
|
110
114
|
if (!announced && now === 3) { announced = true; writeText(dir + '/ready', 'ok'); }
|
|
111
115
|
if (!announced && waited > 120) { recorder.stop; writeText(dir + '/failed.txt', 'mic_permission_unanswered'); $.exit(5); }
|
|
@@ -49,7 +49,9 @@ export async function syncNotetaker(command, door) {
|
|
|
49
49
|
// BLI-18006: `--repair-truncated` asks the same door to re-read Wispr Flow notes
|
|
50
50
|
// that were stored cut short. Fixed ones count as "unchanged" in the totals
|
|
51
51
|
// (the ledger's column), failed ones as "skipped"; the repair line says which.
|
|
52
|
-
repairing: command.repairTruncated === true,
|
|
52
|
+
repairing: command.repairTruncated === true || command.repairEmpty === true,
|
|
53
|
+
// BLI-23914: `--repair-empty` re-reads notes filed with no transcript, same door.
|
|
54
|
+
repairKind: command.repairEmpty === true ? "empty_transcripts" : command.repairTruncated === true ? "truncated_transcripts" : null,
|
|
53
55
|
liveWaitMs: Number.isFinite(pollMs) && pollMs >= 0 ? pollMs : LIVE_PASS_WAIT_MS,
|
|
54
56
|
};
|
|
55
57
|
const state = {
|
|
@@ -73,9 +75,9 @@ async function runOnePass(door, settings, state) {
|
|
|
73
75
|
const answer = await ask(door, {
|
|
74
76
|
path: `/api/notes/accounts/${encodeURIComponent(settings.accountId)}/sync`,
|
|
75
77
|
method: "POST",
|
|
76
|
-
label: settings.repairing ? "notes sync (repair truncated)" : "notes sync",
|
|
78
|
+
label: settings.repairKind === "empty_transcripts" ? "notes sync (repair empty)" : settings.repairing ? "notes sync (repair truncated)" : "notes sync",
|
|
77
79
|
timeoutMs: settings.repairing ? REPAIR_DEADLINE_MS : WRITE_DEADLINE_MS,
|
|
78
|
-
...(settings.
|
|
80
|
+
...(settings.repairKind ? { body: { repair: settings.repairKind } } : {}),
|
|
79
81
|
});
|
|
80
82
|
if (!answer.ok)
|
|
81
83
|
return await retryOrStop(door, settings, state, answer);
|
|
@@ -144,7 +146,7 @@ function finishSync(door, settings, state, run) {
|
|
|
144
146
|
if (door.json)
|
|
145
147
|
return emit(door, { ok: true, passes, totals, ...(settings.repairing ? { repair: repairTotals } : {}), run });
|
|
146
148
|
writeLine(door.io.stdout, settings.repairing
|
|
147
|
-
? `Repair finished after ${passes} pass${passes === 1 ? "" : "es"}: ${repairTotals.fixed} fixed, ${repairTotals.failed} failed (${repairTotals.found} carried the marker when the first pass began).`
|
|
149
|
+
? `Repair finished after ${passes} pass${passes === 1 ? "" : "es"}: ${repairTotals.fixed} fixed, ${repairTotals.failed} failed (${repairTotals.found} ${settings.repairKind === "empty_transcripts" ? "had no transcript" : "carried the marker"} when the first pass began).`
|
|
148
150
|
: `Caught up after ${passes} pass${passes === 1 ? "" : "es"}: ${totals.stored} stored, ${totals.unchanged} unchanged, ${totals.skipped} skipped.`);
|
|
149
151
|
return 0;
|
|
150
152
|
}
|
|
@@ -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.290");
|
|
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.290",
|
|
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.89",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.176",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.95",
|
|
33
33
|
"unpdf": "1.8.1"
|
|
34
34
|
},
|