@bli-cockpit/cli 0.2.116 → 0.2.119
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/local-args-tower-pages.js +104 -2
- package/dist/commands/local-help-commands.js +8 -0
- package/dist/commands/local-help.js +1 -0
- package/dist/commands/notes-accounts.js +230 -0
- package/dist/commands/notes.js +20 -0
- package/dist/commands/public-root.js +1 -1
- package/package.json +2 -2
|
@@ -152,6 +152,18 @@ export function parseBriefArgs(args) {
|
|
|
152
152
|
json: values.booleans.has("--json"),
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* The notetakers `cockpit notes connect` will actually seal a key for
|
|
157
|
+
* (BLI-4391). It mirrors `CONNECTABLE_NOTES_PROVIDERS` in the dashboard, and
|
|
158
|
+
* the door is still the authority — this list only saves a round trip and a
|
|
159
|
+
* sealed key for a provider with no reader.
|
|
160
|
+
*
|
|
161
|
+
* `otter` and `granola` are deliberately absent: Otter's public API is
|
|
162
|
+
* Enterprise-only and Granola still runs on its own server-side cron.
|
|
163
|
+
*/
|
|
164
|
+
const CONNECTABLE_PROVIDERS = ["fellow", "circleback"];
|
|
165
|
+
/** The provider whose API is per workspace, so its account needs a subdomain. */
|
|
166
|
+
const PROVIDERS_NEEDING_A_WORKSPACE = ["fellow"];
|
|
155
167
|
const NOTES_ACTIONS = new Set([
|
|
156
168
|
"folders", "mkdir", "rmdir", "rename",
|
|
157
169
|
"list",
|
|
@@ -164,6 +176,13 @@ const NOTES_ACTIONS = new Set([
|
|
|
164
176
|
"unshare",
|
|
165
177
|
"move",
|
|
166
178
|
"place",
|
|
179
|
+
// BLI-4383: the notetaker a person connects, and the meetings it records.
|
|
180
|
+
"accounts",
|
|
181
|
+
"connect",
|
|
182
|
+
"detach",
|
|
183
|
+
"sync",
|
|
184
|
+
// BLI-4394: arm that notetaker's webhook with the vendor's signing secret.
|
|
185
|
+
"webhook",
|
|
167
186
|
]);
|
|
168
187
|
/** Actions whose first positional is the note it acts on. */
|
|
169
188
|
const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
|
|
@@ -173,6 +192,12 @@ const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
|
|
|
173
192
|
"move",
|
|
174
193
|
"place",
|
|
175
194
|
]);
|
|
195
|
+
/**
|
|
196
|
+
* BLI-4383. Actions whose first positional is a NOTETAKER ACCOUNT id, not a
|
|
197
|
+
* note id. `sync` takes one too, and is handled beside these rather than in
|
|
198
|
+
* this set because its id is checked at the door instead of here.
|
|
199
|
+
*/
|
|
200
|
+
const NOTES_ACTIONS_NEEDING_AN_ACCOUNT = new Set(["detach", "webhook"]);
|
|
176
201
|
export function parseNotesArgs(args) {
|
|
177
202
|
const values = parseNamedArgs(args, {
|
|
178
203
|
allowedFlags: [
|
|
@@ -194,6 +219,17 @@ export function parseNotesArgs(args) {
|
|
|
194
219
|
"--yes",
|
|
195
220
|
"--json",
|
|
196
221
|
"--tree",
|
|
222
|
+
// BLI-4383. `--key-stdin` is a FLAG and not a value: the key itself is
|
|
223
|
+
// read from stdin and never from argv.
|
|
224
|
+
"--provider",
|
|
225
|
+
"--key-stdin",
|
|
226
|
+
// BLI-4391. Fellow's API is per WORKSPACE, and a subdomain is a label,
|
|
227
|
+
// not a secret, so this one carries a value while the key never does.
|
|
228
|
+
"--workspace",
|
|
229
|
+
// BLI-4394. `--secret-stdin` is a FLAG and not a value, for the same
|
|
230
|
+
// reason `--key-stdin` is: the signing secret never touches argv.
|
|
231
|
+
"--secret-stdin",
|
|
232
|
+
"--show-webhook",
|
|
197
233
|
],
|
|
198
234
|
valueFlags: [
|
|
199
235
|
"--home",
|
|
@@ -208,6 +244,9 @@ export function parseNotesArgs(args) {
|
|
|
208
244
|
"--since",
|
|
209
245
|
"--until",
|
|
210
246
|
"--limit",
|
|
247
|
+
"--provider",
|
|
248
|
+
"--workspace",
|
|
249
|
+
"--show-webhook",
|
|
211
250
|
],
|
|
212
251
|
});
|
|
213
252
|
// Bare `cockpit notes` is the list, which is the thing a person typing it
|
|
@@ -215,7 +254,7 @@ export function parseNotesArgs(args) {
|
|
|
215
254
|
const first = values.positionals[0];
|
|
216
255
|
const action = (first === undefined ? "list" : first);
|
|
217
256
|
if (!NOTES_ACTIONS.has(action)) {
|
|
218
|
-
throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, move, or
|
|
257
|
+
throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, move, place, accounts, connect, detach, sync, or webhook.`);
|
|
219
258
|
}
|
|
220
259
|
const rest = values.positionals.slice(first === undefined ? 0 : 1);
|
|
221
260
|
const json = values.booleans.has("--json");
|
|
@@ -232,6 +271,60 @@ export function parseNotesArgs(args) {
|
|
|
232
271
|
throw new Error(`notes ${action} takes one note id, not ${rest.length}.`);
|
|
233
272
|
}
|
|
234
273
|
}
|
|
274
|
+
let accountId;
|
|
275
|
+
if (NOTES_ACTIONS_NEEDING_AN_ACCOUNT.has(action) || action === "sync") {
|
|
276
|
+
accountId = optionalNonEmpty(rest[0]);
|
|
277
|
+
if (!accountId && NOTES_ACTIONS_NEEDING_AN_ACCOUNT.has(action)) {
|
|
278
|
+
throw new Error(`notes ${action} needs a notetaker account id.`);
|
|
279
|
+
}
|
|
280
|
+
if (rest.length > 1) {
|
|
281
|
+
throw new Error(`notes ${action} takes one account id, not ${rest.length}.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// BLI-4394. `--show-webhook <id>` is a flag on `accounts` rather than a verb
|
|
285
|
+
// of its own, because "what is this one's webhook URL?" is the question a
|
|
286
|
+
// person asks WHILE looking at the list.
|
|
287
|
+
const showWebhook = optionalNonEmpty(values.flags.get("--show-webhook"));
|
|
288
|
+
if (showWebhook !== undefined) {
|
|
289
|
+
if (action !== "accounts") {
|
|
290
|
+
throw new Error("--show-webhook belongs to `cockpit notes accounts --show-webhook <account-id>`.");
|
|
291
|
+
}
|
|
292
|
+
accountId = showWebhook;
|
|
293
|
+
}
|
|
294
|
+
if (action === "webhook") {
|
|
295
|
+
if (!values.booleans.has("--secret-stdin")) {
|
|
296
|
+
// Naming the flag is the point: it says out loud that the signing secret
|
|
297
|
+
// is not an argument, so nobody goes looking for a `--secret` that will
|
|
298
|
+
// never exist.
|
|
299
|
+
throw new Error('notes webhook reads the signing secret from stdin. Say so: printf "%s" "<secret>" | cockpit notes webhook <account-id> --secret-stdin');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else if (values.booleans.has("--secret-stdin")) {
|
|
303
|
+
throw new Error("--secret-stdin belongs to `cockpit notes webhook <account-id> --secret-stdin`.");
|
|
304
|
+
}
|
|
305
|
+
const provider = optionalNonEmpty(values.flags.get("--provider"));
|
|
306
|
+
const workspace = optionalNonEmpty(values.flags.get("--workspace"));
|
|
307
|
+
if (action === "connect") {
|
|
308
|
+
if (!provider) {
|
|
309
|
+
throw new Error(`notes connect needs --provider ${CONNECTABLE_PROVIDERS.join("|")}.`);
|
|
310
|
+
}
|
|
311
|
+
if (!CONNECTABLE_PROVIDERS.includes(provider)) {
|
|
312
|
+
throw new Error(`notes connect does not know the provider "${provider}". Tower reads ${CONNECTABLE_PROVIDERS.join(" and ")}.`);
|
|
313
|
+
}
|
|
314
|
+
if (PROVIDERS_NEEDING_A_WORKSPACE.includes(provider) && !workspace) {
|
|
315
|
+
throw new Error(`notes connect --provider ${provider} needs --workspace <subdomain>: its API lives at https://<subdomain>.fellow.app, so a key alone cannot address anything.`);
|
|
316
|
+
}
|
|
317
|
+
if (workspace !== undefined && !/^[a-z0-9-]{1,63}$/.test(workspace)) {
|
|
318
|
+
throw new Error("--workspace is a workspace subdomain: lowercase letters, digits and hyphens, 1 to 63 characters.");
|
|
319
|
+
}
|
|
320
|
+
if (!values.booleans.has("--key-stdin")) {
|
|
321
|
+
// Naming the flag is the point: it says out loud that the key is not an
|
|
322
|
+
// argument, so nobody goes looking for a `--key` that will never exist.
|
|
323
|
+
throw new Error('notes connect reads the key from stdin. Say so: printf "%s" "<key>" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin');
|
|
324
|
+
}
|
|
325
|
+
if (rest.length > 0)
|
|
326
|
+
throw new Error(`notes connect does not take "${rest[0]}".`);
|
|
327
|
+
}
|
|
235
328
|
let paths;
|
|
236
329
|
if (action === "upload") {
|
|
237
330
|
// Explicit paths only. No globbing happens here — a shell that expands
|
|
@@ -245,7 +338,12 @@ export function parseNotesArgs(args) {
|
|
|
245
338
|
if (rest.length !== 1 || !rest[0]?.trim())
|
|
246
339
|
throw new Error(`notes ${action} needs one folder path.`);
|
|
247
340
|
}
|
|
248
|
-
else if (action !== "paste"
|
|
341
|
+
else if (action !== "paste"
|
|
342
|
+
&& action !== "connect"
|
|
343
|
+
&& action !== "sync"
|
|
344
|
+
&& !NOTES_ACTIONS_NEEDING_A_NOTE.has(action)
|
|
345
|
+
&& !NOTES_ACTIONS_NEEDING_AN_ACCOUNT.has(action)
|
|
346
|
+
&& rest.length > 0) {
|
|
249
347
|
throw new Error(`notes ${action} does not take "${rest[0]}".`);
|
|
250
348
|
}
|
|
251
349
|
if (action === "rename" && !optionalNonEmpty(values.flags.get("--name")))
|
|
@@ -271,6 +369,10 @@ export function parseNotesArgs(args) {
|
|
|
271
369
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
272
370
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
273
371
|
...(noteId ? { noteId } : {}),
|
|
372
|
+
...(accountId ? { accountId } : {}),
|
|
373
|
+
...(provider ? { provider } : {}),
|
|
374
|
+
...(workspace ? { workspace } : {}),
|
|
375
|
+
...(showWebhook ? { showWebhook: true } : {}),
|
|
274
376
|
...(paths ? { paths } : {}),
|
|
275
377
|
filePath: optionalNonEmpty(values.flags.get("--file")),
|
|
276
378
|
name: optionalNonEmpty(values.flags.get("--name")),
|
|
@@ -471,6 +471,14 @@ export function localSubcommandHelp(command) {
|
|
|
471
471
|
"unshare <id> — take it back. Never asks: it only ever narrows who can read.",
|
|
472
472
|
"move <id> (--to \"<shelf>\"|--clear-shelf) — put it on a different shelf, or take the shelf off.",
|
|
473
473
|
"place <id> [--apply] — say which shelf this note belongs on and why, from the meeting it is part of, who was in the room and its own name. Moves nothing without --apply, and never overrules a shelf somebody typed.",
|
|
474
|
+
"",
|
|
475
|
+
"Your own notetaker (BLI-4383). Fellow and Circleback today; Otter needs an Enterprise plan and Granola still runs on its own cron.",
|
|
476
|
+
"accounts [--json] — the notetakers you have connected, with health and when each was last read.",
|
|
477
|
+
"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.",
|
|
478
|
+
"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",
|
|
479
|
+
"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",
|
|
480
|
+
"detach <account-id> — take it off. The stored key goes with it; the meetings it already filed stay.",
|
|
481
|
+
"sync <account-id> — read that notetaker now. Prints the run's own counts and reason label, and exits 1 when the pass failed.",
|
|
474
482
|
"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.",
|
|
475
483
|
"--json writes one machine-readable object to stdout; every reason, receipt and progress line stays on stderr.",
|
|
476
484
|
"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.",
|
|
@@ -87,6 +87,7 @@ export function localCommandHelp(command) {
|
|
|
87
87
|
" cockpit brief status [--who <person>] [--render] [--dashboard-url <url>] [--json]",
|
|
88
88
|
" cockpit correct --claim <claimId> --text \"<what is wrong>\" [--for <person>] [--version <pageId>] [--supersedes <id>] [--dashboard-url <url>] [--json]",
|
|
89
89
|
" cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|move <id>|place <id>] [--folder <path>] [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--file <path>] [--name <n>] [--exclude \"<sentence>\"] [--to \"<shelf>\"|--clear-shelf] [--apply] [--yes] [--dashboard-url <url>] [--json]",
|
|
90
|
+
" cockpit notes [accounts [--show-webhook <account-id>]|connect --provider fellow --workspace <sub> --key-stdin [--name <n>]|webhook <account-id> --secret-stdin|detach <account-id>|sync <account-id>] [--json] (the key and the signing secret are read from stdin, never a flag)",
|
|
90
91
|
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--yes] [--workspace <path>] [--json]",
|
|
91
92
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
92
93
|
" cockpit sessions [--source codex|claude] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit notes accounts | connect | detach | sync` — the notetaker a person
|
|
3
|
+
* connects, at a terminal (BLI-4383). Sibling of `notes.ts`, named in its
|
|
4
|
+
* header.
|
|
5
|
+
*
|
|
6
|
+
* Four verbs over the same `/api/notes/accounts` doors the Settings section
|
|
7
|
+
* calls, so nothing a browser can do here is unreachable from a terminal.
|
|
8
|
+
*
|
|
9
|
+
* **THE KEY NEVER TOUCHES ARGV.** `connect` reads it from stdin, exactly as
|
|
10
|
+
* `cockpit mail add-imap` reads a Google app password, and for the same
|
|
11
|
+
* reason: an argument list is in the process table, in the shell history and
|
|
12
|
+
* in every log that records a command line. There is deliberately no
|
|
13
|
+
* `--key <value>` flag, and there is deliberately no MCP twin for this one
|
|
14
|
+
* verb (`verb-census.ts`'s `TERMINAL_ONLY` says so in full).
|
|
15
|
+
*
|
|
16
|
+
* stdout is the answer, stderr is the commentary — the same split the rest of
|
|
17
|
+
* `cockpit notes` keeps.
|
|
18
|
+
*/
|
|
19
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
20
|
+
import { ask, emit, fail, READ_DEADLINE_MS } from "./notes-door.js";
|
|
21
|
+
/** A pass can take a while; a read never should. */
|
|
22
|
+
const WRITE_DEADLINE_MS = 120_000;
|
|
23
|
+
/** An API key is long; anything past this is not one, and is refused by size. */
|
|
24
|
+
const KEY_MAX_CHARS = 500;
|
|
25
|
+
export async function listNotesAccounts(command, door) {
|
|
26
|
+
const answer = await ask(door, {
|
|
27
|
+
path: "/api/notes/accounts",
|
|
28
|
+
method: "GET",
|
|
29
|
+
label: "notes accounts",
|
|
30
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
31
|
+
});
|
|
32
|
+
if (!answer.ok)
|
|
33
|
+
return fail(door, answer.reason, answer.detail);
|
|
34
|
+
const accounts = answer.body.accounts ?? [];
|
|
35
|
+
if (door.json)
|
|
36
|
+
return emit(door, { ok: true, accounts });
|
|
37
|
+
if (accounts.length === 0) {
|
|
38
|
+
writeLine(door.io.stdout, "No notetaker is connected yet.");
|
|
39
|
+
writeLine(door.io.stdout, "");
|
|
40
|
+
writeLine(door.io.stdout, 'Connect one: printf "%s" "<your fellow api key>" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin');
|
|
41
|
+
writeLine(door.io.stdout, ' or: printf "%s" "<your circleback api key>" | cockpit notes connect --provider circleback --key-stdin');
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
for (const account of accounts) {
|
|
45
|
+
const health = account.status === "active"
|
|
46
|
+
? "active"
|
|
47
|
+
: `${account.status}: ${account.status_reason ?? "no reason recorded"}`;
|
|
48
|
+
// The workspace rides beside the provider because a Fellow account that
|
|
49
|
+
// points at the wrong subdomain fails with a 401 and nothing else says so.
|
|
50
|
+
const where = account.workspace ? `${account.provider}/${account.workspace}` : account.provider;
|
|
51
|
+
writeLine(door.io.stdout, `${(account.label ?? account.provider).padEnd(20)} ${where.padEnd(20)} ${health.padEnd(24)} ${account.last_synced_at ?? "never read"}`);
|
|
52
|
+
}
|
|
53
|
+
writeLine(door.io.stdout, "");
|
|
54
|
+
writeLine(door.io.stdout, `${accounts.length} notetaker(s). ids: ${accounts.map((account) => account.id).join(", ")}`);
|
|
55
|
+
// `command` is unused beyond the door it already carries; naming it keeps
|
|
56
|
+
// every verb in this file one shape.
|
|
57
|
+
void command;
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
export async function connectNotetaker(command, door) {
|
|
61
|
+
// The key comes from stdin and nowhere else. An interactive terminal with
|
|
62
|
+
// nothing piped in is told how, rather than left waiting on a prompt that
|
|
63
|
+
// would echo the secret into the scrollback.
|
|
64
|
+
if (isInteractiveStdin(door.io)) {
|
|
65
|
+
return fail(door, "key_required_on_stdin", 'The API key is read from stdin, never from a flag. Try: printf "%s" "<your key>" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin');
|
|
66
|
+
}
|
|
67
|
+
let apiKey;
|
|
68
|
+
try {
|
|
69
|
+
apiKey = (await readPipedText(door.io.stdin, {
|
|
70
|
+
maxChars: KEY_MAX_CHARS,
|
|
71
|
+
overflowMessage: "That is longer than any notetaker API key; nothing was sent.",
|
|
72
|
+
})).trim();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return fail(door, "key_unreadable", error instanceof Error ? error.message : String(error));
|
|
76
|
+
}
|
|
77
|
+
if (apiKey === "") {
|
|
78
|
+
return fail(door, "key_required_on_stdin", "Nothing arrived on stdin, so nothing was connected.");
|
|
79
|
+
}
|
|
80
|
+
const answer = await ask(door, {
|
|
81
|
+
path: "/api/notes/accounts",
|
|
82
|
+
method: "POST",
|
|
83
|
+
label: "notes connect",
|
|
84
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
85
|
+
body: {
|
|
86
|
+
provider: command.provider ?? "fellow",
|
|
87
|
+
label: command.name ?? null,
|
|
88
|
+
// Absent, not null, when nobody named one: the door treats a missing
|
|
89
|
+
// workspace and an explicit null the same, and a `--workspace` a person
|
|
90
|
+
// did type is never silently dropped.
|
|
91
|
+
...(command.workspace ? { workspace: command.workspace } : {}),
|
|
92
|
+
api_key: apiKey,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
if (!answer.ok)
|
|
96
|
+
return fail(door, answer.reason, answer.detail);
|
|
97
|
+
const account = answer.body.account;
|
|
98
|
+
if (door.json)
|
|
99
|
+
return emit(door, { ok: true, account });
|
|
100
|
+
const named = account?.provider ?? command.provider ?? "fellow";
|
|
101
|
+
writeLine(door.io.stdout, `Connected ${account?.workspace ? `${named}/${account.workspace}` : named} (${account?.id ?? "no id returned"}).`);
|
|
102
|
+
writeLine(door.io.stdout, "Nothing is read until a sync runs: `cockpit notes sync <id>`, or wait for the half-hourly cron.");
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `cockpit notes accounts --show-webhook <id>` and
|
|
107
|
+
* `cockpit notes webhook <id> --secret-stdin` (BLI-4394).
|
|
108
|
+
*
|
|
109
|
+
* The URL is SHOWN, because a person cannot register a webhook they cannot
|
|
110
|
+
* read. The signing secret is READ FROM STDIN and never from a flag, for the
|
|
111
|
+
* same reason `connect` reads the API key that way: an argument list is in the
|
|
112
|
+
* process table, in the shell history and in every log that records a command
|
|
113
|
+
* line. There is deliberately no `--secret <value>`, and deliberately no MCP
|
|
114
|
+
* twin for this verb.
|
|
115
|
+
*/
|
|
116
|
+
export async function showNotesWebhook(command, door) {
|
|
117
|
+
const accountId = command.accountId ?? "";
|
|
118
|
+
if (accountId === "") {
|
|
119
|
+
return fail(door, "account_required", "Name the notetaker: `cockpit notes accounts --show-webhook <account-id>`. `cockpit notes accounts` lists the ids.");
|
|
120
|
+
}
|
|
121
|
+
const answer = await ask(door, {
|
|
122
|
+
path: `/api/notes/accounts/${encodeURIComponent(accountId)}/webhook`,
|
|
123
|
+
method: "GET",
|
|
124
|
+
label: "notes webhook",
|
|
125
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
126
|
+
});
|
|
127
|
+
if (!answer.ok)
|
|
128
|
+
return fail(door, answer.reason, answer.detail);
|
|
129
|
+
const webhook = answer.body.webhook;
|
|
130
|
+
if (door.json)
|
|
131
|
+
return emit(door, { ok: true, webhook });
|
|
132
|
+
if (!webhook?.url) {
|
|
133
|
+
writeLine(door.io.stdout, "This notetaker has no webhook URL.");
|
|
134
|
+
writeLine(door.io.stdout, "Its key was sealed before webhooks shipped. Detach it and connect it again to mint one.");
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
writeLine(door.io.stdout, webhook.url);
|
|
138
|
+
writeLine(door.io.stdout, "");
|
|
139
|
+
writeLine(door.io.stdout, webhook.armed
|
|
140
|
+
? "A signing secret is stored, so requests to that URL are verified."
|
|
141
|
+
: "NO SIGNING SECRET IS STORED YET, so every request to that URL is refused. That is the right way to be off.");
|
|
142
|
+
if (webhook.where) {
|
|
143
|
+
writeLine(door.io.stdout, "");
|
|
144
|
+
writeLine(door.io.stdout, webhook.where);
|
|
145
|
+
}
|
|
146
|
+
writeLine(door.io.stdout, "");
|
|
147
|
+
writeLine(door.io.stdout, `Store the secret: printf "%s" "<signing secret>" | cockpit notes webhook ${accountId} --secret-stdin`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
export async function armNotesWebhook(command, door) {
|
|
151
|
+
const accountId = command.accountId ?? "";
|
|
152
|
+
if (accountId === "") {
|
|
153
|
+
return fail(door, "account_required", "Name the notetaker: `cockpit notes webhook <account-id> --secret-stdin`.");
|
|
154
|
+
}
|
|
155
|
+
// The secret comes from stdin and nowhere else, exactly as the API key does.
|
|
156
|
+
if (isInteractiveStdin(door.io)) {
|
|
157
|
+
return fail(door, "secret_required_on_stdin", `The signing secret is read from stdin, never from a flag. Try: printf "%s" "<signing secret>" | cockpit notes webhook ${accountId} --secret-stdin`);
|
|
158
|
+
}
|
|
159
|
+
let secret;
|
|
160
|
+
try {
|
|
161
|
+
secret = (await readPipedText(door.io.stdin, {
|
|
162
|
+
maxChars: KEY_MAX_CHARS,
|
|
163
|
+
overflowMessage: "That is longer than any webhook signing secret; nothing was sent.",
|
|
164
|
+
})).trim();
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return fail(door, "secret_unreadable", error instanceof Error ? error.message : String(error));
|
|
168
|
+
}
|
|
169
|
+
if (secret === "") {
|
|
170
|
+
return fail(door, "secret_required_on_stdin", "Nothing arrived on stdin, so nothing was stored.");
|
|
171
|
+
}
|
|
172
|
+
const answer = await ask(door, {
|
|
173
|
+
path: `/api/notes/accounts/${encodeURIComponent(accountId)}/webhook`,
|
|
174
|
+
method: "PUT",
|
|
175
|
+
label: "notes webhook arm",
|
|
176
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
177
|
+
body: { signing_secret: secret },
|
|
178
|
+
});
|
|
179
|
+
if (!answer.ok)
|
|
180
|
+
return fail(door, answer.reason, answer.detail);
|
|
181
|
+
if (door.json)
|
|
182
|
+
return emit(door, { ok: true, armed: true, account_id: accountId });
|
|
183
|
+
writeLine(door.io.stdout, `Stored. Requests to ${accountId}'s webhook URL are now verified.`);
|
|
184
|
+
writeLine(door.io.stdout, "Until a meeting is written up nothing changes; the half-hourly sweep still reads this account either way.");
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
export async function detachNotetaker(command, door) {
|
|
188
|
+
const accountId = command.accountId ?? "";
|
|
189
|
+
const answer = await ask(door, {
|
|
190
|
+
path: `/api/notes/accounts?account_id=${encodeURIComponent(accountId)}`,
|
|
191
|
+
method: "DELETE",
|
|
192
|
+
label: "notes detach",
|
|
193
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
194
|
+
});
|
|
195
|
+
if (!answer.ok)
|
|
196
|
+
return fail(door, answer.reason, answer.detail);
|
|
197
|
+
if (door.json)
|
|
198
|
+
return emit(door, { ok: true, detached: accountId });
|
|
199
|
+
writeLine(door.io.stdout, `Detached ${accountId}. Its stored key went with it.`);
|
|
200
|
+
writeLine(door.io.stdout, "The meetings it already filed stay where they are.");
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
export async function syncNotetaker(command, door) {
|
|
204
|
+
const accountId = command.accountId ?? "";
|
|
205
|
+
if (accountId === "") {
|
|
206
|
+
// One account per call on purpose: the sweep over ALL of them is the cron,
|
|
207
|
+
// and a terminal asking for "everything" would hide which one failed.
|
|
208
|
+
return fail(door, "account_required", "Name the notetaker to sync: `cockpit notes sync <account-id>`. `cockpit notes accounts` lists the ids.");
|
|
209
|
+
}
|
|
210
|
+
const answer = await ask(door, {
|
|
211
|
+
path: `/api/notes/accounts/${encodeURIComponent(accountId)}/sync`,
|
|
212
|
+
method: "POST",
|
|
213
|
+
label: "notes sync",
|
|
214
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
215
|
+
});
|
|
216
|
+
if (!answer.ok)
|
|
217
|
+
return fail(door, answer.reason, answer.detail);
|
|
218
|
+
const run = answer.body.run;
|
|
219
|
+
if (door.json)
|
|
220
|
+
return emit(door, { ok: true, run });
|
|
221
|
+
if (!run) {
|
|
222
|
+
writeLine(door.io.stdout, "Tower answered without a run.");
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
// The ledger row's own words, relayed rather than rephrased.
|
|
226
|
+
writeLine(door.io.stdout, `${run.reason}: ${run.notesAdded} stored, ${run.notesUpdated} unchanged, ${run.notesSkipped} skipped.`);
|
|
227
|
+
if (run.detail)
|
|
228
|
+
writeLine(door.io.stdout, run.detail);
|
|
229
|
+
return run.ok ? 0 : 1;
|
|
230
|
+
}
|
package/dist/commands/notes.js
CHANGED
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
* - `notes-writes.ts` — upload, paste, share/unshare, move, place.
|
|
31
31
|
* - `notes-door.ts` — the one HTTP request, and the shared answer / refusal /
|
|
32
32
|
* emit / say-* rendering both halves above call.
|
|
33
|
+
* - `notes-accounts.ts` — BLI-4383: the notetaker a person connects, and the
|
|
34
|
+
* meetings it records. `connect` reads the API key from STDIN and there is
|
|
35
|
+
* no flag that would take one.
|
|
33
36
|
*
|
|
34
37
|
* Every public name is still importable from this file.
|
|
35
38
|
*/
|
|
@@ -37,8 +40,10 @@ import { loadPairedSession } from "../tower-client.js";
|
|
|
37
40
|
import { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
38
41
|
import { uploadNotes, pasteNote, shareNote, moveNote, placeNote } from "./notes-writes.js";
|
|
39
42
|
import { runFolderCommand } from "./notes-folders.js";
|
|
43
|
+
import { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, } from "./notes-accounts.js";
|
|
40
44
|
export { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
41
45
|
export { uploadNotes, pasteNote, shareNote, moveNote, placeNote } from "./notes-writes.js";
|
|
46
|
+
export { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, } from "./notes-accounts.js";
|
|
42
47
|
export { ask, emit, fail, sayUpload, sayScope, errorText, TAG, } from "./notes-door.js";
|
|
43
48
|
export async function runNotes(command, io) {
|
|
44
49
|
const session = await loadPairedSession("notes", command.homeDir);
|
|
@@ -73,5 +78,20 @@ export async function runNotes(command, io) {
|
|
|
73
78
|
return moveNote(command, door);
|
|
74
79
|
case "place":
|
|
75
80
|
return placeNote(command, door);
|
|
81
|
+
case "accounts":
|
|
82
|
+
// BLI-4394. `--show-webhook <id>` turns the list into one account's
|
|
83
|
+
// webhook URL, because that is the question a person asks WHILE looking
|
|
84
|
+
// at the list rather than a verb of its own.
|
|
85
|
+
return command.showWebhook
|
|
86
|
+
? showNotesWebhook(command, door)
|
|
87
|
+
: listNotesAccounts(command, door);
|
|
88
|
+
case "connect":
|
|
89
|
+
return connectNotetaker(command, door);
|
|
90
|
+
case "detach":
|
|
91
|
+
return detachNotetaker(command, door);
|
|
92
|
+
case "sync":
|
|
93
|
+
return syncNotetaker(command, door);
|
|
94
|
+
case "webhook":
|
|
95
|
+
return armNotesWebhook(command, door);
|
|
76
96
|
}
|
|
77
97
|
}
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.119");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.119",
|
|
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.32",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.47",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.48"
|
|
33
33
|
}
|
|
34
34
|
}
|