@bli-cockpit/cli 0.2.236 → 0.2.238
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/adapters/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-overlay.js +106 -0
- package/dist/commands/cal.js +40 -23
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/doctor-usage-telemetry.js +2 -1
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +68 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +54 -48
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +56 -188
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-msg.js +149 -0
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-work.js +52 -33
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +11 -4
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/mail.js +6 -4
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/msg.js +51 -9
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +91 -67
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +2 -2
package/dist/commands/msg.js
CHANGED
|
@@ -25,6 +25,8 @@ const CONTENT_MAX_CHARS = 8_000;
|
|
|
25
25
|
export async function runMsg(command, io) {
|
|
26
26
|
const door = await openAgentDoor("msg", command, io);
|
|
27
27
|
switch (command.action) {
|
|
28
|
+
case "show":
|
|
29
|
+
return showMessage(command, door);
|
|
28
30
|
case "channels":
|
|
29
31
|
return listChannels(door);
|
|
30
32
|
case "read":
|
|
@@ -59,9 +61,20 @@ async function fetchChannels(door) {
|
|
|
59
61
|
});
|
|
60
62
|
if (!answer.ok)
|
|
61
63
|
return { ok: false, reason: answer.reason, detail: answer.detail };
|
|
64
|
+
const channels = answer.body.channels ?? [];
|
|
65
|
+
let page = answer.body;
|
|
66
|
+
for (let count = 0; page.hasMore; count++) {
|
|
67
|
+
if (count >= 200 || !page.nextCursor)
|
|
68
|
+
return { ok: false, reason: "read_failed", detail: "Channel listing is incomplete. Retry online or use an ID." };
|
|
69
|
+
const next = await askAgentDoor(door, { path: `/api/msg/channels?before=${encodeURIComponent(page.nextCursor)}`, method: "GET", label: "msg channels page", timeoutMs: READ_DEADLINE_MS });
|
|
70
|
+
if (!next.ok)
|
|
71
|
+
return { ok: false, reason: next.reason, detail: next.detail };
|
|
72
|
+
channels.push(...(next.body.channels ?? []));
|
|
73
|
+
page = next.body;
|
|
74
|
+
}
|
|
62
75
|
return {
|
|
63
76
|
ok: true,
|
|
64
|
-
channels
|
|
77
|
+
channels,
|
|
65
78
|
fromCache: answer.fromCache,
|
|
66
79
|
ageMs: answer.ageMs,
|
|
67
80
|
coverage: answer.coverage,
|
|
@@ -89,6 +102,12 @@ async function listChannels(door) {
|
|
|
89
102
|
}
|
|
90
103
|
/** Exact id, or exact name with or without a leading `#`. Never fuzzy. */
|
|
91
104
|
async function resolveChannelId(door, ref) {
|
|
105
|
+
if (/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(ref)) {
|
|
106
|
+
const answer = await askAgentDoor(door, { path: `/api/msg/channels?channel_id=${encodeURIComponent(ref)}`, method: "GET", label: "msg channel", timeoutMs: READ_DEADLINE_MS });
|
|
107
|
+
if (!answer.ok)
|
|
108
|
+
return answer.reason === "channel_not_found_or_unreadable" ? { status: "not_found" } : { status: "list_failed", reason: answer.reason, detail: answer.detail };
|
|
109
|
+
return { status: "ok", id: ref };
|
|
110
|
+
}
|
|
92
111
|
const fetched = await fetchChannels(door);
|
|
93
112
|
if (!fetched.ok)
|
|
94
113
|
return { status: "list_failed", reason: fetched.reason, detail: fetched.detail };
|
|
@@ -112,6 +131,8 @@ async function readChannel(command, door) {
|
|
|
112
131
|
if (resolved.status === "not_found")
|
|
113
132
|
return channelNotFound(door, ref);
|
|
114
133
|
const query = new URLSearchParams();
|
|
134
|
+
if (command.before)
|
|
135
|
+
query.set("before", command.before);
|
|
115
136
|
if (command.limit !== undefined)
|
|
116
137
|
query.set("limit", String(command.limit));
|
|
117
138
|
if (command.threadId)
|
|
@@ -124,7 +145,7 @@ async function readChannel(command, door) {
|
|
|
124
145
|
timeoutMs: READ_DEADLINE_MS,
|
|
125
146
|
});
|
|
126
147
|
if (!answer.ok)
|
|
127
|
-
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
148
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, answer.body?.coverage === "failed_partial" ? answer.body : {});
|
|
128
149
|
const body = answer.body;
|
|
129
150
|
const messages = body.messages ?? [];
|
|
130
151
|
// BLI-10449: whether this channel is Tower's own or a Slack snapshot, and
|
|
@@ -132,12 +153,14 @@ async function readChannel(command, door) {
|
|
|
132
153
|
const provenance = body.provenance ?? null;
|
|
133
154
|
const limitApplied = body.limitApplied ?? null;
|
|
134
155
|
if (door.json)
|
|
135
|
-
return emitAgentDoor(door, { ok: true, channelId: resolved.id, provenance, limitApplied, messages });
|
|
156
|
+
return emitAgentDoor(door, { ok: true, ...body, channelId: resolved.id, provenance, limitApplied, messages, nextCursor: body.nextCursor, hasMore: body.hasMore, coverage: body.coverage });
|
|
136
157
|
if (provenance?.sentence)
|
|
137
158
|
writeLine(door.io.stdout, provenance.sentence);
|
|
138
159
|
if (limitApplied !== null) {
|
|
139
160
|
writeLine(door.io.stdout, `Showing the newest ${limitApplied}: one read returns at most ${limitApplied} messages.`);
|
|
140
161
|
}
|
|
162
|
+
if (body.nextCursor)
|
|
163
|
+
writeLine(door.io.stdout, `More history: --before ${body.nextCursor}`);
|
|
141
164
|
if (messages.length === 0) {
|
|
142
165
|
writeLine(door.io.stdout, "No messages.");
|
|
143
166
|
return 0;
|
|
@@ -145,7 +168,7 @@ async function readChannel(command, door) {
|
|
|
145
168
|
for (const message of [...messages].reverse()) {
|
|
146
169
|
// An imported Slack message has no Tower user; its Slack name is the label.
|
|
147
170
|
const who = message.agent_label ?? message.user_id ?? message.slack_author_label ?? "someone";
|
|
148
|
-
writeLine(door.io.stdout, `${message.created_at} ${who} ${message.content ?? ""}`);
|
|
171
|
+
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${message.content ?? ""}`);
|
|
149
172
|
}
|
|
150
173
|
return 0;
|
|
151
174
|
}
|
|
@@ -159,23 +182,26 @@ async function readThread(command, door) {
|
|
|
159
182
|
if (resolved.status === "not_found")
|
|
160
183
|
return channelNotFound(door, command.channelRef);
|
|
161
184
|
const answer = await askAgentDoor(door, {
|
|
162
|
-
path: `/api/msg/channels/${encodeURIComponent(resolved.id)}/messages?thread_parent_id=${encodeURIComponent(command.threadId ?? "")}`,
|
|
185
|
+
path: `/api/msg/channels/${encodeURIComponent(resolved.id)}/messages?thread_parent_id=${encodeURIComponent(command.threadId ?? "")}${command.before ? `&before=${encodeURIComponent(command.before)}` : ""}${command.limit ? `&limit=${command.limit}` : ""}`,
|
|
163
186
|
method: "GET",
|
|
164
187
|
label: "msg thread",
|
|
165
188
|
timeoutMs: READ_DEADLINE_MS,
|
|
166
189
|
});
|
|
167
190
|
if (!answer.ok)
|
|
168
|
-
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
169
|
-
const
|
|
191
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, answer.body?.coverage === "failed_partial" ? answer.body : {});
|
|
192
|
+
const body = answer.body;
|
|
193
|
+
const messages = body.messages ?? [];
|
|
170
194
|
if (door.json)
|
|
171
|
-
return emitAgentDoor(door, { ok: true, channelId: resolved.id, threadParentId: command.threadId, messages });
|
|
195
|
+
return emitAgentDoor(door, { ok: true, ...body, channelId: resolved.id, threadParentId: command.threadId, messages, nextCursor: body.nextCursor, hasMore: body.hasMore, coverage: body.coverage });
|
|
196
|
+
if (body.nextCursor)
|
|
197
|
+
writeLine(door.io.stdout, `More history: --before ${body.nextCursor}`);
|
|
172
198
|
if (messages.length === 0) {
|
|
173
199
|
writeLine(door.io.stdout, "No replies.");
|
|
174
200
|
return 0;
|
|
175
201
|
}
|
|
176
202
|
for (const message of [...messages].reverse()) {
|
|
177
203
|
const who = message.agent_label ?? message.user_id;
|
|
178
|
-
writeLine(door.io.stdout, `${message.created_at} ${who} ${message.content ?? ""}`);
|
|
204
|
+
writeLine(door.io.stdout, `${message.id} ${message.created_at} ${who} ${message.content ?? ""}`);
|
|
179
205
|
}
|
|
180
206
|
return 0;
|
|
181
207
|
}
|
|
@@ -307,4 +333,20 @@ async function openDm(command, door) {
|
|
|
307
333
|
// true either way rather than claiming a creation that may not have happened.
|
|
308
334
|
writeLine(door.io.stdout, `Direct message with ${email}: ${channel?.id ?? "?"}`);
|
|
309
335
|
return 0;
|
|
336
|
+
}
|
|
337
|
+
async function showMessage(command, door) {
|
|
338
|
+
const answer = await askAgentDoor(door, { path: `/api/msg/messages/${encodeURIComponent(command.threadId ?? "")}`, method: "GET", label: "msg show", timeoutMs: READ_DEADLINE_MS });
|
|
339
|
+
if (!answer.ok)
|
|
340
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
341
|
+
if (door.json)
|
|
342
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
343
|
+
const body = answer.body;
|
|
344
|
+
writeLine(door.io.stdout, body.href ?? "Message context");
|
|
345
|
+
for (const page of [body.channelPage, body.threadPage]) {
|
|
346
|
+
for (const row of [...(page?.messages ?? [])].reverse())
|
|
347
|
+
writeLine(door.io.stdout, `${row.id} ${row.created_at} ${row.content ?? ""}`);
|
|
348
|
+
if (page?.nextCursor)
|
|
349
|
+
writeLine(door.io.stdout, `More history: --before ${page.nextCursor}`);
|
|
350
|
+
}
|
|
351
|
+
return 0;
|
|
310
352
|
}
|
|
@@ -80,6 +80,25 @@ function writeSharedFeeds(door, feeds) {
|
|
|
80
80
|
}
|
|
81
81
|
writeLine(door.io.stdout, " Its runs in full: `cockpit notes granola-status`.");
|
|
82
82
|
}
|
|
83
|
+
function writeHowToConnect(door) {
|
|
84
|
+
writeLine(door.io.stdout, "");
|
|
85
|
+
writeLine(door.io.stdout, 'Connect one: printf "%s" "<your fellow api key>" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin');
|
|
86
|
+
writeLine(door.io.stdout, ' or: printf "%s" "<your circleback api key>" | cockpit notes connect --provider circleback --key-stdin');
|
|
87
|
+
writeLine(door.io.stdout, " or: cockpit notes connect --provider wisprflow (opens Wispr's sign-in in your browser and waits; no key)");
|
|
88
|
+
}
|
|
89
|
+
function writeAccountRows(door, accounts) {
|
|
90
|
+
for (const account of accounts) {
|
|
91
|
+
const health = account.status === "active"
|
|
92
|
+
? "active"
|
|
93
|
+
: `${account.status}: ${account.status_reason ?? "no reason recorded"}`;
|
|
94
|
+
// The workspace rides beside the provider because a Fellow account that
|
|
95
|
+
// points at the wrong subdomain fails with a 401 and nothing else says so.
|
|
96
|
+
const where = account.workspace ? `${account.provider}/${account.workspace}` : account.provider;
|
|
97
|
+
writeLine(door.io.stdout, `${(account.label ?? account.provider).padEnd(20)} ${where.padEnd(20)} ${health.padEnd(24)} ${account.last_synced_at ?? "never read"}`);
|
|
98
|
+
}
|
|
99
|
+
writeLine(door.io.stdout, "");
|
|
100
|
+
writeLine(door.io.stdout, `${accounts.length} notetaker(s). ids: ${accounts.map((account) => account.id).join(", ")}`);
|
|
101
|
+
}
|
|
83
102
|
export async function listNotesAccounts(command, door) {
|
|
84
103
|
const answer = await ask(door, {
|
|
85
104
|
path: "/api/notes/accounts",
|
|
@@ -101,25 +120,12 @@ export async function listNotesAccounts(command, door) {
|
|
|
101
120
|
if (accounts.length === 0) {
|
|
102
121
|
writeLine(door.io.stdout, "You have not connected a notetaker of your own.");
|
|
103
122
|
writeSharedFeeds(door, sharedFeeds);
|
|
104
|
-
|
|
105
|
-
writeLine(door.io.stdout, 'Connect one: printf "%s" "<your fellow api key>" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin');
|
|
106
|
-
writeLine(door.io.stdout, ' or: printf "%s" "<your circleback api key>" | cockpit notes connect --provider circleback --key-stdin');
|
|
107
|
-
writeLine(door.io.stdout, " or: cockpit notes connect --provider wisprflow (opens Wispr's sign-in in your browser and waits; no key)");
|
|
123
|
+
writeHowToConnect(door);
|
|
108
124
|
if (coverage)
|
|
109
125
|
printCoverage(door, coverage);
|
|
110
126
|
return 0;
|
|
111
127
|
}
|
|
112
|
-
|
|
113
|
-
const health = account.status === "active"
|
|
114
|
-
? "active"
|
|
115
|
-
: `${account.status}: ${account.status_reason ?? "no reason recorded"}`;
|
|
116
|
-
// The workspace rides beside the provider because a Fellow account that
|
|
117
|
-
// points at the wrong subdomain fails with a 401 and nothing else says so.
|
|
118
|
-
const where = account.workspace ? `${account.provider}/${account.workspace}` : account.provider;
|
|
119
|
-
writeLine(door.io.stdout, `${(account.label ?? account.provider).padEnd(20)} ${where.padEnd(20)} ${health.padEnd(24)} ${account.last_synced_at ?? "never read"}`);
|
|
120
|
-
}
|
|
121
|
-
writeLine(door.io.stdout, "");
|
|
122
|
-
writeLine(door.io.stdout, `${accounts.length} notetaker(s). ids: ${accounts.map((account) => account.id).join(", ")}`);
|
|
128
|
+
writeAccountRows(door, accounts);
|
|
123
129
|
writeSharedFeeds(door, sharedFeeds);
|
|
124
130
|
if (coverage)
|
|
125
131
|
printCoverage(door, coverage);
|
|
@@ -159,21 +165,10 @@ export async function connectNotetaker(command, door, wisprflow = {}) {
|
|
|
159
165
|
if (isInteractiveStdin(door.io)) {
|
|
160
166
|
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');
|
|
161
167
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// which can be far longer than any API key.
|
|
167
|
-
maxChars: command.provider === "wisprflow" ? SIGN_IN_MAX_CHARS : KEY_MAX_CHARS,
|
|
168
|
-
overflowMessage: "That is longer than any notetaker API key; nothing was sent.",
|
|
169
|
-
})).trim();
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
return fail(door, "key_unreadable", error instanceof Error ? error.message : String(error));
|
|
173
|
-
}
|
|
174
|
-
if (apiKey === "") {
|
|
175
|
-
return fail(door, "key_required_on_stdin", "Nothing arrived on stdin, so nothing was connected.");
|
|
176
|
-
}
|
|
168
|
+
const key = await readConnectKey(command, door);
|
|
169
|
+
if ("exitCode" in key)
|
|
170
|
+
return key.exitCode;
|
|
171
|
+
const apiKey = key.apiKey;
|
|
177
172
|
const answer = await ask(door, {
|
|
178
173
|
path: "/api/notes/accounts",
|
|
179
174
|
method: "POST",
|
|
@@ -194,10 +189,32 @@ export async function connectNotetaker(command, door, wisprflow = {}) {
|
|
|
194
189
|
const account = answer.body.account;
|
|
195
190
|
if (door.json)
|
|
196
191
|
return emit(door, { ok: true, account });
|
|
197
|
-
|
|
192
|
+
writeConnected(door, account, command.provider ?? "fellow");
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
/** The key as piped on stdin, or the exit code of the refusal already written. */
|
|
196
|
+
async function readConnectKey(command, door) {
|
|
197
|
+
let apiKey;
|
|
198
|
+
try {
|
|
199
|
+
apiKey = (await readPipedText(door.io.stdin, {
|
|
200
|
+
// A pre-built Wispr Flow sign-in bundle carries an OAuth refresh token,
|
|
201
|
+
// which can be far longer than any API key.
|
|
202
|
+
maxChars: command.provider === "wisprflow" ? SIGN_IN_MAX_CHARS : KEY_MAX_CHARS,
|
|
203
|
+
overflowMessage: "That is longer than any notetaker API key; nothing was sent.",
|
|
204
|
+
})).trim();
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return { exitCode: fail(door, "key_unreadable", error instanceof Error ? error.message : String(error)) };
|
|
208
|
+
}
|
|
209
|
+
if (apiKey === "") {
|
|
210
|
+
return { exitCode: fail(door, "key_required_on_stdin", "Nothing arrived on stdin, so nothing was connected.") };
|
|
211
|
+
}
|
|
212
|
+
return { apiKey };
|
|
213
|
+
}
|
|
214
|
+
function writeConnected(door, account, requestedProvider) {
|
|
215
|
+
const named = account?.provider ?? requestedProvider;
|
|
198
216
|
writeLine(door.io.stdout, `Connected ${account?.workspace ? `${named}/${account.workspace}` : named} (${account?.id ?? "no id returned"}).`);
|
|
199
217
|
writeLine(door.io.stdout, "Nothing is read until a sync runs: `cockpit notes sync <id>`, or wait for the half-hourly cron.");
|
|
200
|
-
return 0;
|
|
201
218
|
}
|
|
202
219
|
/**
|
|
203
220
|
* `cockpit notes accounts --show-webhook <id>` and
|
|
@@ -44,96 +44,122 @@ export async function syncNotetaker(command, door) {
|
|
|
44
44
|
// and the MCP tool call; nothing here reads a notetaker itself.
|
|
45
45
|
const env = door.io.env ?? {};
|
|
46
46
|
const pollMs = Number.parseInt(env["COCKPIT_NOTES_SYNC_POLL_MS"] ?? "", 10);
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// A timeout or a 5xx mid-history is the call's, not the account's: the
|
|
69
|
-
// cursor was saved after every meeting, so asking again resumes. Three in
|
|
70
|
-
// a row is a real outage and is reported with its reason.
|
|
71
|
-
const transient = /^(timeout|network|unreachable|http_5\d\d|BLI-E5\d\d|read_failed)/i.test(answer.reason);
|
|
72
|
-
if (transient && transientFailures < MAX_TRANSIENT_FAILURES) {
|
|
73
|
-
transientFailures += 1;
|
|
74
|
-
writeLine(door.io.stderr, `${TAG} sync call failed, retrying ${JSON.stringify({ reason: answer.reason, attempt: transientFailures })}`);
|
|
75
|
-
await sleepMs(liveWaitMs);
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
return failRun(door, answer.reason, answer.detail, last, passes, totals);
|
|
79
|
-
}
|
|
80
|
-
transientFailures = 0;
|
|
81
|
-
const run = answer.body.run;
|
|
82
|
-
if (!run) {
|
|
83
|
-
writeLine(door.io.stdout, "Tower answered without a run.");
|
|
84
|
-
return failRun(door, "no_run_in_answer", "Tower answered the sync without a run.", last, passes, totals);
|
|
85
|
-
}
|
|
86
|
-
last = run;
|
|
87
|
-
if (run.reason === "running") {
|
|
88
|
-
// Another pass (the cron, a second terminal) is live on this account. It
|
|
89
|
-
// writes the same cursor; wait for it rather than racing it.
|
|
90
|
-
liveWaits += 1;
|
|
91
|
-
writeLine(door.io.stderr, `${TAG} another pass is running ${JSON.stringify({ run_id: run.runId ?? null, waits: liveWaits })}`);
|
|
92
|
-
if (liveWaits > MAX_LIVE_WAITS) {
|
|
93
|
-
return failRun(door, "pass_still_running", "Another pass kept running on this notetaker. Check `cockpit notes sync-status`, then try again.", last, passes, totals);
|
|
94
|
-
}
|
|
95
|
-
await sleepMs(liveWaitMs);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
passes += 1;
|
|
99
|
-
totals.stored += run.notesAdded;
|
|
100
|
-
totals.unchanged += run.notesUpdated;
|
|
101
|
-
totals.skipped += run.notesSkipped;
|
|
102
|
-
const waiting = typeof run.remaining === "number" ? `; ${run.remaining} waiting` : "";
|
|
103
|
-
if (run.repair) {
|
|
104
|
-
repairTotals.fixed += run.repair.fixed;
|
|
105
|
-
repairTotals.failed += run.repair.failed;
|
|
106
|
-
repairTotals.found = Math.max(repairTotals.found, run.repair.found);
|
|
107
|
-
}
|
|
108
|
-
const line = run.repair
|
|
109
|
-
? `pass ${passes}: ${run.reason}: repair: ${repairLine(run.repair)}${run.more ? ", continuing" : ""}.`
|
|
110
|
-
: `pass ${passes}: ${run.reason}: ${run.notesAdded} stored, ${run.notesUpdated} unchanged, ${run.notesSkipped} skipped${run.more ? `${waiting}, continuing` : ""}.`;
|
|
111
|
-
// Progress is commentary in --json (stdout is the one answer), and the
|
|
112
|
-
// answer in a terminal.
|
|
113
|
-
writeLine(door.json ? door.io.stderr : door.io.stdout, line);
|
|
114
|
-
if (run.detail && !run.ok)
|
|
115
|
-
writeLine(door.json ? door.io.stderr : door.io.stdout, run.detail);
|
|
116
|
-
if (!run.ok) {
|
|
117
|
-
return failRun(door, run.reason, run.detail || `The pass ended ${run.reason}.`, run, passes, totals);
|
|
118
|
-
}
|
|
119
|
-
if (!run.more) {
|
|
120
|
-
if (door.json)
|
|
121
|
-
return emit(door, { ok: true, passes, totals, ...(repairing ? { repair: repairTotals } : {}), run });
|
|
122
|
-
writeLine(door.io.stdout, repairing
|
|
123
|
-
? `Repair finished after ${passes} pass${passes === 1 ? "" : "es"}: ${repairTotals.fixed} fixed, ${repairTotals.failed} failed (${repairTotals.found} carried the marker when the first pass began).`
|
|
124
|
-
: `Caught up after ${passes} pass${passes === 1 ? "" : "es"}: ${totals.stored} stored, ${totals.unchanged} unchanged, ${totals.skipped} skipped.`);
|
|
125
|
-
return 0;
|
|
126
|
-
}
|
|
127
|
-
// A pass that handled nothing and still says "more" would loop forever.
|
|
128
|
-
if (run.notesAdded + run.notesUpdated + run.notesSkipped === 0)
|
|
129
|
-
stalled += 1;
|
|
130
|
-
else
|
|
131
|
-
stalled = 0;
|
|
132
|
-
if (stalled >= MAX_STALLED_PASSES) {
|
|
133
|
-
return failRun(door, "no_progress", `Three passes in a row handled no meeting (last: ${run.reason}). The cursor is saved; try again later or check \`cockpit notes sync-status\`.`, run, passes, totals);
|
|
134
|
-
}
|
|
47
|
+
const settings = {
|
|
48
|
+
accountId,
|
|
49
|
+
// BLI-18006: `--repair-truncated` asks the same door to re-read Wispr Flow notes
|
|
50
|
+
// that were stored cut short. Fixed ones count as "unchanged" in the totals
|
|
51
|
+
// (the ledger's column), failed ones as "skipped"; the repair line says which.
|
|
52
|
+
repairing: command.repairTruncated === true,
|
|
53
|
+
liveWaitMs: Number.isFinite(pollMs) && pollMs >= 0 ? pollMs : LIVE_PASS_WAIT_MS,
|
|
54
|
+
};
|
|
55
|
+
const state = {
|
|
56
|
+
passes: 0,
|
|
57
|
+
stalled: 0,
|
|
58
|
+
liveWaits: 0,
|
|
59
|
+
transientFailures: 0,
|
|
60
|
+
last: undefined,
|
|
61
|
+
totals: { stored: 0, unchanged: 0, skipped: 0 },
|
|
62
|
+
repairTotals: { fixed: 0, failed: 0, found: 0 },
|
|
63
|
+
};
|
|
64
|
+
while (state.passes < MAX_SYNC_PASSES) {
|
|
65
|
+
const outcome = await runOnePass(door, settings, state);
|
|
66
|
+
if (outcome !== "again")
|
|
67
|
+
return outcome;
|
|
135
68
|
}
|
|
136
|
-
return
|
|
69
|
+
return stopSync(door, state, "too_many_passes", `Stopped after ${MAX_SYNC_PASSES} passes with meetings still waiting. Run \`cockpit notes sync ${accountId}\` again to continue from the saved cursor.`);
|
|
70
|
+
}
|
|
71
|
+
/** One door call and what follows it: an exit code when the sync is over, "again" when it is not. */
|
|
72
|
+
async function runOnePass(door, settings, state) {
|
|
73
|
+
const answer = await ask(door, {
|
|
74
|
+
path: `/api/notes/accounts/${encodeURIComponent(settings.accountId)}/sync`,
|
|
75
|
+
method: "POST",
|
|
76
|
+
label: settings.repairing ? "notes sync (repair truncated)" : "notes sync",
|
|
77
|
+
timeoutMs: settings.repairing ? REPAIR_DEADLINE_MS : WRITE_DEADLINE_MS,
|
|
78
|
+
...(settings.repairing ? { body: { repair: "truncated_transcripts" } } : {}),
|
|
79
|
+
});
|
|
80
|
+
if (!answer.ok)
|
|
81
|
+
return await retryOrStop(door, settings, state, answer);
|
|
82
|
+
state.transientFailures = 0;
|
|
83
|
+
const run = answer.body.run;
|
|
84
|
+
if (!run) {
|
|
85
|
+
writeLine(door.io.stdout, "Tower answered without a run.");
|
|
86
|
+
return stopSync(door, state, "no_run_in_answer", "Tower answered the sync without a run.");
|
|
87
|
+
}
|
|
88
|
+
state.last = run;
|
|
89
|
+
if (run.reason === "running")
|
|
90
|
+
return await waitForLivePass(door, settings, state, run);
|
|
91
|
+
recordPass(door, state, run);
|
|
92
|
+
if (!run.ok) {
|
|
93
|
+
return stopSync(door, state, run.reason, run.detail || `The pass ended ${run.reason}.`);
|
|
94
|
+
}
|
|
95
|
+
if (!run.more)
|
|
96
|
+
return finishSync(door, settings, state, run);
|
|
97
|
+
return checkProgress(door, state, run);
|
|
98
|
+
}
|
|
99
|
+
/** A timeout or a 5xx mid-history is the call's, not the account's: the cursor was saved after every meeting. */
|
|
100
|
+
async function retryOrStop(door, settings, state, answer) {
|
|
101
|
+
// Asking again resumes. Three in a row is a real outage and is reported with its reason.
|
|
102
|
+
const transient = /^(timeout|network|unreachable|http_5\d\d|BLI-E5\d\d|read_failed)/i.test(answer.reason);
|
|
103
|
+
if (!transient || state.transientFailures >= MAX_TRANSIENT_FAILURES) {
|
|
104
|
+
return stopSync(door, state, answer.reason, answer.detail);
|
|
105
|
+
}
|
|
106
|
+
state.transientFailures += 1;
|
|
107
|
+
writeLine(door.io.stderr, `${TAG} sync call failed, retrying ${JSON.stringify({ reason: answer.reason, attempt: state.transientFailures })}`);
|
|
108
|
+
await sleepMs(settings.liveWaitMs);
|
|
109
|
+
return "again";
|
|
110
|
+
}
|
|
111
|
+
/** Another pass (the cron, a second terminal) is live on this account. It writes the same cursor; wait for it rather than racing it. */
|
|
112
|
+
async function waitForLivePass(door, settings, state, run) {
|
|
113
|
+
state.liveWaits += 1;
|
|
114
|
+
writeLine(door.io.stderr, `${TAG} another pass is running ${JSON.stringify({ run_id: run.runId ?? null, waits: state.liveWaits })}`);
|
|
115
|
+
if (state.liveWaits > MAX_LIVE_WAITS) {
|
|
116
|
+
return stopSync(door, state, "pass_still_running", "Another pass kept running on this notetaker. Check `cockpit notes sync-status`, then try again.");
|
|
117
|
+
}
|
|
118
|
+
await sleepMs(settings.liveWaitMs);
|
|
119
|
+
return "again";
|
|
120
|
+
}
|
|
121
|
+
/** Add the pass to the totals and print its line. */
|
|
122
|
+
function recordPass(door, state, run) {
|
|
123
|
+
state.passes += 1;
|
|
124
|
+
state.totals.stored += run.notesAdded;
|
|
125
|
+
state.totals.unchanged += run.notesUpdated;
|
|
126
|
+
state.totals.skipped += run.notesSkipped;
|
|
127
|
+
const waiting = typeof run.remaining === "number" ? `; ${run.remaining} waiting` : "";
|
|
128
|
+
if (run.repair) {
|
|
129
|
+
state.repairTotals.fixed += run.repair.fixed;
|
|
130
|
+
state.repairTotals.failed += run.repair.failed;
|
|
131
|
+
state.repairTotals.found = Math.max(state.repairTotals.found, run.repair.found);
|
|
132
|
+
}
|
|
133
|
+
const line = run.repair
|
|
134
|
+
? `pass ${state.passes}: ${run.reason}: repair: ${repairLine(run.repair)}${run.more ? ", continuing" : ""}.`
|
|
135
|
+
: `pass ${state.passes}: ${run.reason}: ${run.notesAdded} stored, ${run.notesUpdated} unchanged, ${run.notesSkipped} skipped${run.more ? `${waiting}, continuing` : ""}.`;
|
|
136
|
+
// Progress is commentary in --json (stdout is the one answer), and the
|
|
137
|
+
// answer in a terminal.
|
|
138
|
+
writeLine(door.json ? door.io.stderr : door.io.stdout, line);
|
|
139
|
+
if (run.detail && !run.ok)
|
|
140
|
+
writeLine(door.json ? door.io.stderr : door.io.stdout, run.detail);
|
|
141
|
+
}
|
|
142
|
+
function finishSync(door, settings, state, run) {
|
|
143
|
+
const { passes, totals, repairTotals } = state;
|
|
144
|
+
if (door.json)
|
|
145
|
+
return emit(door, { ok: true, passes, totals, ...(settings.repairing ? { repair: repairTotals } : {}), run });
|
|
146
|
+
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).`
|
|
148
|
+
: `Caught up after ${passes} pass${passes === 1 ? "" : "es"}: ${totals.stored} stored, ${totals.unchanged} unchanged, ${totals.skipped} skipped.`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
/** A pass that handled nothing and still says "more" would loop forever. */
|
|
152
|
+
function checkProgress(door, state, run) {
|
|
153
|
+
if (run.notesAdded + run.notesUpdated + run.notesSkipped === 0)
|
|
154
|
+
state.stalled += 1;
|
|
155
|
+
else
|
|
156
|
+
state.stalled = 0;
|
|
157
|
+
if (state.stalled < MAX_STALLED_PASSES)
|
|
158
|
+
return "again";
|
|
159
|
+
return stopSync(door, state, "no_progress", `Three passes in a row handled no meeting (last: ${run.reason}). The cursor is saved; try again later or check \`cockpit notes sync-status\`.`);
|
|
160
|
+
}
|
|
161
|
+
function stopSync(door, state, reason, detail) {
|
|
162
|
+
return failRun(door, reason, detail, state.last, state.passes, state.totals);
|
|
137
163
|
}
|
|
138
164
|
/** A sync that did not finish: the reason on stderr, the structured answer on stdout in --json. */
|
|
139
165
|
function failRun(door, reason, detail, run, passes, totals) {
|
|
@@ -57,8 +57,14 @@ export async function connectWisprflowInBrowser(door, options = {}) {
|
|
|
57
57
|
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
58
58
|
const now = options.now ?? Date.now;
|
|
59
59
|
const open = options.open ?? ((url) => openInBrowser(url));
|
|
60
|
-
const
|
|
61
|
-
|
|
60
|
+
const started = await startSignIn(door);
|
|
61
|
+
if ("failure" in started)
|
|
62
|
+
return started.failure;
|
|
63
|
+
announceSignIn(door, started.signIn.authorizeUrl, open);
|
|
64
|
+
return await waitForSignIn(door, started.signIn.attemptId, sleep, now);
|
|
65
|
+
}
|
|
66
|
+
/** 1. Ask the one start door for a sign-in. */
|
|
67
|
+
async function startSignIn(door) {
|
|
62
68
|
const started = await ask(door, {
|
|
63
69
|
path: WISPRFLOW_START_PATH,
|
|
64
70
|
method: "POST",
|
|
@@ -69,8 +75,8 @@ export async function connectWisprflowInBrowser(door, options = {}) {
|
|
|
69
75
|
if (!started.ok) {
|
|
70
76
|
log(door, "start refused", { reason: started.reason });
|
|
71
77
|
if (started.reason === "account_already_connected")
|
|
72
|
-
return alreadyConnected(door);
|
|
73
|
-
return { ok: false, reason: started.reason, detail: started.detail };
|
|
78
|
+
return { failure: await alreadyConnected(door) };
|
|
79
|
+
return { failure: { ok: false, reason: started.reason, detail: started.detail } };
|
|
74
80
|
}
|
|
75
81
|
const body = started.body;
|
|
76
82
|
const authorizeUrl = typeof body.authorize_url === "string" ? body.authorize_url : "";
|
|
@@ -78,13 +84,19 @@ export async function connectWisprflowInBrowser(door, options = {}) {
|
|
|
78
84
|
if (authorizeUrl === "" || attemptId === "") {
|
|
79
85
|
log(door, "start answer unusable", { reason: "start_answer_unusable" });
|
|
80
86
|
return {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
failure: {
|
|
88
|
+
ok: false,
|
|
89
|
+
reason: "start_answer_unusable",
|
|
90
|
+
detail: "Tower started a sign-in but did not say where to send you or how to watch it, so nothing was connected.",
|
|
91
|
+
},
|
|
84
92
|
};
|
|
85
93
|
}
|
|
86
94
|
log(door, "started", { attempt_id: attemptId, expires_at: typeof body.expires_at === "string" ? body.expires_at : null });
|
|
87
|
-
|
|
95
|
+
return { signIn: { attemptId, authorizeUrl } };
|
|
96
|
+
}
|
|
97
|
+
/** 2. Print the address first, then open it. */
|
|
98
|
+
function announceSignIn(door, authorizeUrl, open) {
|
|
99
|
+
const stderr = door.io.stderr;
|
|
88
100
|
writeLine(stderr, "Connect Wispr Flow to Tower: approve on Wispr's page in your browser.");
|
|
89
101
|
writeLine(stderr, ` ${authorizeUrl}`);
|
|
90
102
|
writeLine(stderr, " Use the Wispr account that holds your notes, signed in with Google, Apple, Microsoft or company single sign-on.");
|
|
@@ -93,7 +105,9 @@ export async function connectWisprflowInBrowser(door, options = {}) {
|
|
|
93
105
|
if (!opened.ok)
|
|
94
106
|
writeLine(stderr, `(${opened.detail} The address above still works.)`);
|
|
95
107
|
writeLine(stderr, `Waiting up to ${MAX_WAIT_MS / 60_000} minutes for the sign-in to finish...`);
|
|
96
|
-
|
|
108
|
+
}
|
|
109
|
+
/** 3. Poll the one status door. */
|
|
110
|
+
async function waitForSignIn(door, attemptId, sleep, now) {
|
|
97
111
|
const deadline = now() + MAX_WAIT_MS;
|
|
98
112
|
let polls = 0;
|
|
99
113
|
let failures = 0;
|
|
@@ -115,20 +129,9 @@ export async function connectWisprflowInBrowser(door, options = {}) {
|
|
|
115
129
|
}
|
|
116
130
|
else {
|
|
117
131
|
failures = 0;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
log(door, "poll", { attempt_id: attemptId, polls, status: state, reason });
|
|
122
|
-
if (state === "connected") {
|
|
123
|
-
const accountId = typeof status.account_id === "string" ? status.account_id : "";
|
|
124
|
-
log(door, "connected", { attempt_id: attemptId, account_id: accountId || null, polls });
|
|
125
|
-
return { ok: true, account: await readAccount(door, accountId), already: false };
|
|
126
|
-
}
|
|
127
|
-
if (state === "failed" || state === "expired") {
|
|
128
|
-
const why = reason ?? (state === "expired" ? "state_expired" : "unknown");
|
|
129
|
-
log(door, "ended without connecting", { attempt_id: attemptId, status: state, reason: why });
|
|
130
|
-
return { ok: false, reason: why, detail: failureDetail(why) };
|
|
131
|
-
}
|
|
132
|
+
const ended = await readEndOfSignIn(door, attemptId, polls, polled.body);
|
|
133
|
+
if (ended)
|
|
134
|
+
return ended;
|
|
132
135
|
}
|
|
133
136
|
await sleep(POLL_INTERVAL_MS);
|
|
134
137
|
}
|
|
@@ -140,6 +143,24 @@ export async function connectWisprflowInBrowser(door, options = {}) {
|
|
|
140
143
|
+ " Run this again for a fresh link, or check `cockpit notes accounts`.",
|
|
141
144
|
};
|
|
142
145
|
}
|
|
146
|
+
/** The sign-in's answer once it has one; undefined while it is still pending. */
|
|
147
|
+
async function readEndOfSignIn(door, attemptId, polls, pollBody) {
|
|
148
|
+
const status = pollBody;
|
|
149
|
+
const state = typeof status.status === "string" ? status.status : "unknown";
|
|
150
|
+
const reason = typeof status.reason === "string" ? status.reason : null;
|
|
151
|
+
log(door, "poll", { attempt_id: attemptId, polls, status: state, reason });
|
|
152
|
+
if (state === "connected") {
|
|
153
|
+
const accountId = typeof status.account_id === "string" ? status.account_id : "";
|
|
154
|
+
log(door, "connected", { attempt_id: attemptId, account_id: accountId || null, polls });
|
|
155
|
+
return { ok: true, account: await readAccount(door, accountId), already: false };
|
|
156
|
+
}
|
|
157
|
+
if (state === "failed" || state === "expired") {
|
|
158
|
+
const why = reason ?? (state === "expired" ? "state_expired" : "unknown");
|
|
159
|
+
log(door, "ended without connecting", { attempt_id: attemptId, status: state, reason: why });
|
|
160
|
+
return { ok: false, reason: why, detail: failureDetail(why) };
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
143
164
|
/** The start door said Wispr Flow is already connected: read which account and report it honestly. */
|
|
144
165
|
async function alreadyConnected(door) {
|
|
145
166
|
const accounts = await readAccounts(door);
|