@bli-cockpit/cli 0.2.117 → 0.2.121
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/analyze.js +74 -54
- package/dist/commands/brief-rewrite.js +164 -101
- package/dist/commands/brief.js +38 -13
- package/dist/commands/correct.js +38 -21
- package/dist/commands/docs.js +13 -10
- package/dist/commands/editor.js +59 -30
- package/dist/commands/install-receipts.js +106 -91
- package/dist/commands/local-args-tower-admin.js +4 -0
- package/dist/commands/local-args-tower-cal.js +28 -3
- package/dist/commands/local-args-tower-chat.js +55 -28
- package/dist/commands/local-args-tower-docs-msg.js +39 -8
- package/dist/commands/local-args-tower-mail.js +27 -1
- package/dist/commands/local-args-tower-models.js +14 -17
- package/dist/commands/local-args-tower-pages.js +62 -5
- package/dist/commands/local-args-tower-work.js +37 -6
- package/dist/commands/local-help-commands.js +6 -3
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/mcp-stdio-probe.js +92 -73
- package/dist/commands/memory-hook-performance.js +135 -101
- package/dist/commands/memory-install-claude.js +15 -14
- package/dist/commands/memory-install-codex.js +10 -6
- package/dist/commands/memory-install-config.js +5 -4
- package/dist/commands/memory-install-contract.js +56 -10
- package/dist/commands/memory-install-report.js +16 -11
- package/dist/commands/memory-install-skills.js +11 -11
- package/dist/commands/memory-log.js +22 -5
- package/dist/commands/msg.js +11 -5
- package/dist/commands/notes-accounts.js +96 -5
- package/dist/commands/notes.js +10 -3
- package/dist/commands/onboard-setup.js +16 -1
- package/dist/commands/ops-sections.js +89 -0
- package/dist/commands/ops.js +117 -120
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/scout.js +90 -68
- package/dist/commands/session-sync-failures.js +19 -13
- package/dist/commands/session-sync-record.js +53 -52
- package/dist/commands/session-sync-upload.js +15 -11
- package/dist/commands/sessions.js +61 -51
- package/dist/commands/slack.js +90 -61
- package/dist/commands/status.js +53 -41
- package/dist/commands/workbook.js +23 -20
- package/package.json +2 -2
package/dist/commands/correct.js
CHANGED
|
@@ -37,11 +37,22 @@ export async function runCorrect(command, io) {
|
|
|
37
37
|
writeFailure(command, io, "no_correction_text", 'Say what is wrong: `cockpit correct --claim <id> --text "..."`, or pipe it in on stdin.');
|
|
38
38
|
return 1;
|
|
39
39
|
}
|
|
40
|
+
const selection = await readBriefClaim(command, io, dashboardUrl, session.device_token, log);
|
|
41
|
+
if (!selection)
|
|
42
|
+
return 1;
|
|
43
|
+
const filed = await fileCorrection(command, io, dashboardUrl, session.device_token, log, selection, text);
|
|
44
|
+
if (!filed)
|
|
45
|
+
return 1;
|
|
46
|
+
printFiledCorrection(command, io, filed);
|
|
47
|
+
logFiledCorrection(command, io, filed, selection.claim, text, startedAt);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
async function readBriefClaim(command, io, dashboardUrl, deviceToken, log) {
|
|
40
51
|
// Step one: which page, whose, and does that line exist.
|
|
41
52
|
const read = await towerJsonRequest({
|
|
42
53
|
dashboardUrl,
|
|
43
54
|
path: `/api/jarvis/brief${briefQuery(command)}`,
|
|
44
|
-
deviceToken
|
|
55
|
+
deviceToken,
|
|
45
56
|
fetch: io.fetch,
|
|
46
57
|
method: "GET",
|
|
47
58
|
label: "correct:brief",
|
|
@@ -50,62 +61,69 @@ export async function runCorrect(command, io) {
|
|
|
50
61
|
});
|
|
51
62
|
if (!read.ok) {
|
|
52
63
|
writeFailure(command, io, read.reason, read.detail);
|
|
53
|
-
return
|
|
64
|
+
return null;
|
|
54
65
|
}
|
|
55
66
|
const brief = read.body;
|
|
56
67
|
const personId = brief.page?.personId;
|
|
57
68
|
if (!brief.ok || !personId) {
|
|
58
69
|
writeFailure(command, io, brief.error ?? "no_page", brief.reply ?? "There is no page to correct.");
|
|
59
|
-
return
|
|
70
|
+
return null;
|
|
60
71
|
}
|
|
61
72
|
const claim = (brief.claims ?? []).find((candidate) => candidate.claimId === command.claimId);
|
|
62
73
|
if (!claim) {
|
|
63
74
|
writeFailure(command, io, "unknown_claim", `That page has no line called “${command.claimId}”. Run \`cockpit brief --claims\` to see the ids.`);
|
|
64
|
-
return
|
|
75
|
+
return null;
|
|
65
76
|
}
|
|
77
|
+
return { brief, personId, claim };
|
|
78
|
+
}
|
|
79
|
+
async function fileCorrection(command, io, dashboardUrl, deviceToken, log, selection, text) {
|
|
66
80
|
// Step two: the same door the panel's form posts to.
|
|
67
81
|
const written = await towerJsonRequest({
|
|
68
82
|
dashboardUrl,
|
|
69
83
|
path: "/api/jarvis/corrections",
|
|
70
|
-
deviceToken
|
|
84
|
+
deviceToken,
|
|
71
85
|
fetch: io.fetch,
|
|
72
86
|
label: "correct",
|
|
73
87
|
timeoutMs: REQUEST_DEADLINE_MS,
|
|
74
88
|
log,
|
|
75
89
|
body: {
|
|
76
|
-
personId,
|
|
77
|
-
pageId: brief.page?.pageId ?? null,
|
|
90
|
+
personId: selection.personId,
|
|
91
|
+
pageId: selection.brief.page?.pageId ?? null,
|
|
78
92
|
claimId: command.claimId,
|
|
79
93
|
// The line as it reads now, so the ledger records what was disputed and
|
|
80
94
|
// the live check has the sentence to work from.
|
|
81
|
-
quotedText: claim.text ?? null,
|
|
95
|
+
quotedText: selection.claim.text ?? null,
|
|
82
96
|
correctionText: text,
|
|
83
|
-
contextLinks: claim.links ?? [],
|
|
97
|
+
contextLinks: selection.claim.links ?? [],
|
|
84
98
|
clauseOnPage: true,
|
|
85
|
-
clauseIsObserved: Boolean(claim.observed),
|
|
99
|
+
clauseIsObserved: Boolean(selection.claim.observed),
|
|
86
100
|
...(command.supersedes ? { supersedes: command.supersedes } : {}),
|
|
87
101
|
},
|
|
88
102
|
});
|
|
89
103
|
if (!written.ok) {
|
|
90
104
|
writeFailure(command, io, written.reason, written.detail);
|
|
91
|
-
return
|
|
105
|
+
return null;
|
|
92
106
|
}
|
|
93
107
|
const filed = written.body;
|
|
94
108
|
if (!filed.correctionId) {
|
|
95
109
|
writeFailure(command, io, "not_filed", filed.error ?? "Tower did not record that correction.");
|
|
96
|
-
return
|
|
110
|
+
return null;
|
|
97
111
|
}
|
|
112
|
+
return filed;
|
|
113
|
+
}
|
|
114
|
+
function printFiledCorrection(command, io, filed) {
|
|
98
115
|
if (command.json) {
|
|
99
116
|
writeLine(io.stdout, JSON.stringify({ ok: true, ...filed }));
|
|
117
|
+
return;
|
|
100
118
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
const styled = colorEnabled(io);
|
|
120
|
+
writeLine(io.stdout, filed.reply ?? "Filed.");
|
|
121
|
+
if (filed.finding)
|
|
122
|
+
writeLine(io.stdout, dim(`The record says: ${filed.finding}`, styled));
|
|
123
|
+
if (filed.link)
|
|
124
|
+
writeLine(io.stdout, dim(filed.link, styled));
|
|
125
|
+
}
|
|
126
|
+
function logFiledCorrection(command, io, filed, claim, text, startedAt) {
|
|
109
127
|
writeLine(io.stderr, `[correct cli] filed ${JSON.stringify({
|
|
110
128
|
tier: filed.tier ?? null,
|
|
111
129
|
outcome: filed.outcome ?? null,
|
|
@@ -117,7 +135,6 @@ export async function runCorrect(command, io) {
|
|
|
117
135
|
text_length: text.length,
|
|
118
136
|
elapsed_ms: Date.now() - startedAt,
|
|
119
137
|
})}`);
|
|
120
|
-
return 0;
|
|
121
138
|
}
|
|
122
139
|
function briefQuery(command) {
|
|
123
140
|
const params = new URLSearchParams({ claims: "1" });
|
package/dist/commands/docs.js
CHANGED
|
@@ -225,8 +225,19 @@ async function updateDoc(command, door) {
|
|
|
225
225
|
&& !command.clearParent) {
|
|
226
226
|
return failAgentDoor(door, TAG, "invalid_body", "docs update needs at least one of --title, --visibility, --parent/--clear-parent, or a body on --body-stdin/--file.");
|
|
227
227
|
}
|
|
228
|
-
const answer = await
|
|
229
|
-
|
|
228
|
+
const answer = await updateDocument(command, door, resolved.id, bodyMarkdown);
|
|
229
|
+
if (!answer.ok)
|
|
230
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
231
|
+
const document = answer.body.document ?? {};
|
|
232
|
+
writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({ document_id: document["id"] ?? resolved.id })}`);
|
|
233
|
+
if (door.json)
|
|
234
|
+
return emitAgentDoor(door, { ok: true, document });
|
|
235
|
+
writeLine(door.io.stdout, `Updated ${String(document["title"] ?? "")} (${String(document["id"] ?? resolved.id)}).`);
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
async function updateDocument(command, door, documentId, bodyMarkdown) {
|
|
239
|
+
return askAgentDoor(door, {
|
|
240
|
+
path: `/api/docs/documents/${encodeURIComponent(documentId)}`,
|
|
230
241
|
method: "PATCH",
|
|
231
242
|
label: "docs update",
|
|
232
243
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
@@ -242,12 +253,4 @@ async function updateDoc(command, door) {
|
|
|
242
253
|
...(command.allowEmpty ? { allow_empty: true } : {}),
|
|
243
254
|
},
|
|
244
255
|
});
|
|
245
|
-
if (!answer.ok)
|
|
246
|
-
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
247
|
-
const document = answer.body.document ?? {};
|
|
248
|
-
writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({ document_id: document["id"] ?? resolved.id })}`);
|
|
249
|
-
if (door.json)
|
|
250
|
-
return emitAgentDoor(door, { ok: true, document });
|
|
251
|
-
writeLine(door.io.stdout, `Updated ${String(document["title"] ?? "")} (${String(document["id"] ?? resolved.id)}).`);
|
|
252
|
-
return 0;
|
|
253
256
|
}
|
package/dist/commands/editor.js
CHANGED
|
@@ -105,14 +105,32 @@ export async function editInEditor(options) {
|
|
|
105
105
|
const platform = options.platform ?? process.platform;
|
|
106
106
|
const log = options.log ?? ((line) => console.error(line));
|
|
107
107
|
const chosen = resolveEditorCommand(options.env, platform);
|
|
108
|
-
if (!chosen)
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
if (!chosen)
|
|
109
|
+
return noEditorConfigured();
|
|
110
|
+
const unavailable = invalidWindowsEditorReason(chosen, platform);
|
|
111
|
+
if (unavailable)
|
|
112
|
+
return unavailable;
|
|
113
|
+
const scratch = await writeEditorScratchFile(options);
|
|
114
|
+
if (!scratch.ok)
|
|
115
|
+
return scratch;
|
|
116
|
+
try {
|
|
117
|
+
return await runEditorSession(options, chosen, scratch.file, log);
|
|
115
118
|
}
|
|
119
|
+
finally {
|
|
120
|
+
// Whatever happened. The page is not a secret, but it is somebody's writing
|
|
121
|
+
// and it does not belong in /tmp after the command returns.
|
|
122
|
+
await removeEditorScratchDirectory(scratch.directory);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function noEditorConfigured() {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
reason: "no_editor_configured",
|
|
129
|
+
detail: "No editor is configured. Set EDITOR (or VISUAL) — for example `export EDITOR=nano` — " +
|
|
130
|
+
"or pipe the edited page in on stdin instead.",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function invalidWindowsEditorReason(chosen, platform) {
|
|
116
134
|
// A `.cmd` or `.bat` is a batch script; Node cannot execute one without
|
|
117
135
|
// handing the whole line to cmd.exe, and that is the one thing this module
|
|
118
136
|
// will not do with a person's own path in it.
|
|
@@ -125,12 +143,14 @@ export async function editInEditor(options) {
|
|
|
125
143
|
"notepad, or the full path to Code.exe.",
|
|
126
144
|
};
|
|
127
145
|
}
|
|
128
|
-
|
|
129
|
-
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
async function writeEditorScratchFile(options) {
|
|
130
149
|
try {
|
|
131
|
-
directory = await mkdtemp(join(tmpdir(), "cockpit-brief-"));
|
|
132
|
-
file = join(directory, `brief-${randomBytes(4).toString("hex")}${options.suffix ?? ".md"}`);
|
|
150
|
+
const directory = await mkdtemp(join(tmpdir(), "cockpit-brief-"));
|
|
151
|
+
const file = join(directory, `brief-${randomBytes(4).toString("hex")}${options.suffix ?? ".md"}`);
|
|
133
152
|
await writeFile(file, options.contents, "utf8");
|
|
153
|
+
return { ok: true, directory, file };
|
|
134
154
|
}
|
|
135
155
|
catch (error) {
|
|
136
156
|
return {
|
|
@@ -139,13 +159,11 @@ export async function editInEditor(options) {
|
|
|
139
159
|
detail: `The page could not be written to a scratch file (${messageOf(error)}).`,
|
|
140
160
|
};
|
|
141
161
|
}
|
|
162
|
+
}
|
|
163
|
+
async function runEditorSession(options, chosen, file, log) {
|
|
142
164
|
try {
|
|
143
165
|
const spawn = options.spawn ?? spawnSync;
|
|
144
|
-
log
|
|
145
|
-
program: basenameOf(chosen.program),
|
|
146
|
-
extra_args: chosen.args.length,
|
|
147
|
-
bytes: Buffer.byteLength(options.contents, "utf8"),
|
|
148
|
-
})}`);
|
|
166
|
+
reportEditorOpening(log, chosen, options.contents);
|
|
149
167
|
// `stdio: "inherit"` hands the terminal over: a full-screen editor needs the
|
|
150
168
|
// real tty. `shell: false` is the default and is stated to make the rule
|
|
151
169
|
// above impossible to lose in a refactor.
|
|
@@ -170,12 +188,8 @@ export async function editInEditor(options) {
|
|
|
170
188
|
"Nothing has been sent to Tower.",
|
|
171
189
|
};
|
|
172
190
|
}
|
|
173
|
-
const text =
|
|
174
|
-
log
|
|
175
|
-
program: basenameOf(chosen.program),
|
|
176
|
-
status: result.status ?? null,
|
|
177
|
-
bytes: Buffer.byteLength(text, "utf8"),
|
|
178
|
-
})}`);
|
|
191
|
+
const text = await readEditedText(file);
|
|
192
|
+
reportEditorClosed(log, chosen, result.status, text);
|
|
179
193
|
return { ok: true, text, program: basenameOf(chosen.program) };
|
|
180
194
|
}
|
|
181
195
|
catch (error) {
|
|
@@ -185,14 +199,29 @@ export async function editInEditor(options) {
|
|
|
185
199
|
detail: `The edited page could not be read back (${messageOf(error)}).`,
|
|
186
200
|
};
|
|
187
201
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
}
|
|
203
|
+
function reportEditorOpening(log, chosen, contents) {
|
|
204
|
+
log(`[brief edit] editor opening ${JSON.stringify({
|
|
205
|
+
program: basenameOf(chosen.program),
|
|
206
|
+
extra_args: chosen.args.length,
|
|
207
|
+
bytes: Buffer.byteLength(contents, "utf8"),
|
|
208
|
+
})}`);
|
|
209
|
+
}
|
|
210
|
+
async function readEditedText(file) {
|
|
211
|
+
return stripLeadingBom(await readFile(file, "utf8"));
|
|
212
|
+
}
|
|
213
|
+
function reportEditorClosed(log, chosen, status, text) {
|
|
214
|
+
log(`[brief edit] editor closed ${JSON.stringify({
|
|
215
|
+
program: basenameOf(chosen.program),
|
|
216
|
+
status,
|
|
217
|
+
bytes: Buffer.byteLength(text, "utf8"),
|
|
218
|
+
})}`);
|
|
219
|
+
}
|
|
220
|
+
async function removeEditorScratchDirectory(directory) {
|
|
221
|
+
await rm(directory, { recursive: true, force: true }).catch(() => {
|
|
222
|
+
// Deliberately silent: the temp directory is the OS's to reap, and a
|
|
223
|
+
// failure to remove it must not turn a saved edit into a failed command.
|
|
224
|
+
});
|
|
196
225
|
}
|
|
197
226
|
/** The program's own name, never the directories around it. */
|
|
198
227
|
function basenameOf(program) {
|
|
@@ -48,6 +48,17 @@ export function sanitizeInstallErrorCode(value) {
|
|
|
48
48
|
export async function reportInstallEventsBestEffort(options) {
|
|
49
49
|
if (options.events.length === 0)
|
|
50
50
|
return null;
|
|
51
|
+
if (shouldWithholdDevBuildReceipts(options))
|
|
52
|
+
return null;
|
|
53
|
+
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
54
|
+
if (!(await queueInstallEvents(options, paths)))
|
|
55
|
+
return null;
|
|
56
|
+
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
57
|
+
if (!hasValidInstallEventSession(session))
|
|
58
|
+
return null;
|
|
59
|
+
return deliverPendingInstallEvents(options, paths, session.device_token);
|
|
60
|
+
}
|
|
61
|
+
function shouldWithholdDevBuildReceipts(options) {
|
|
51
62
|
// BLI-3554. Withheld BEFORE the outbox, not before the POST: an entry queued
|
|
52
63
|
// by a checkout survives in `~/.cockpit` and the next real scheduled tick
|
|
53
64
|
// would deliver it under the workspace version, which is exactly how 47
|
|
@@ -65,32 +76,18 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
65
76
|
cli_version: LOCAL_COLLECTOR_VERSION,
|
|
66
77
|
fix: "set COCKPIT_DEV=0 to post receipts from a checkout deliberately",
|
|
67
78
|
}));
|
|
68
|
-
return
|
|
79
|
+
return true;
|
|
69
80
|
}
|
|
70
|
-
|
|
81
|
+
return suppression.suppressed;
|
|
82
|
+
}
|
|
83
|
+
async function queueInstallEvents(options, paths) {
|
|
71
84
|
try {
|
|
72
85
|
await enqueueInstallEventEntry(paths, {
|
|
73
86
|
dashboardUrl: options.dashboardUrl,
|
|
74
87
|
cliVersion: LOCAL_COLLECTOR_VERSION,
|
|
75
88
|
command: options.command,
|
|
76
89
|
osPlatform: os.platform(),
|
|
77
|
-
events: options.events.map(
|
|
78
|
-
step: event.step.trim().slice(0, 120),
|
|
79
|
-
status: event.status,
|
|
80
|
-
...(event.error_code
|
|
81
|
-
? { error_code: sanitizeInstallErrorCode(event.error_code) }
|
|
82
|
-
: {}),
|
|
83
|
-
// Already redacted and capped at the point it was produced; bounded
|
|
84
|
-
// again here because this mapping is what the server contract sees.
|
|
85
|
-
...(event.error_detail
|
|
86
|
-
? {
|
|
87
|
-
error_detail: event.error_detail
|
|
88
|
-
.trim()
|
|
89
|
-
.slice(0, SYNC_ERROR_DETAIL_MAX_CHARS),
|
|
90
|
-
}
|
|
91
|
-
: {}),
|
|
92
|
-
...(event.at ? { at: event.at } : {}),
|
|
93
|
-
})),
|
|
90
|
+
events: options.events.map(normalizeInstallEvent),
|
|
94
91
|
});
|
|
95
92
|
}
|
|
96
93
|
catch (error) {
|
|
@@ -107,90 +104,44 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
107
104
|
if (options.json) {
|
|
108
105
|
writeLine(options.io.stderr, "Install event outbox unavailable: local_write_failed");
|
|
109
106
|
}
|
|
110
|
-
return
|
|
107
|
+
return false;
|
|
111
108
|
}
|
|
112
|
-
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
function normalizeInstallEvent(event) {
|
|
112
|
+
// Already redacted and capped at the point it was produced; bounded
|
|
113
|
+
// again here because this mapping is what the server contract sees.
|
|
114
|
+
const detail = event.error_detail?.trim().slice(0, SYNC_ERROR_DETAIL_MAX_CHARS);
|
|
115
|
+
const code = event.error_code ? sanitizeInstallErrorCode(event.error_code) : undefined;
|
|
116
|
+
return {
|
|
117
|
+
step: event.step.trim().slice(0, 120),
|
|
118
|
+
status: event.status,
|
|
119
|
+
...(code ? { error_code: code } : {}),
|
|
120
|
+
...(detail ? { error_detail: detail } : {}),
|
|
121
|
+
...(event.at ? { at: event.at } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function hasValidInstallEventSession(session) {
|
|
113
125
|
if (!session ||
|
|
114
126
|
session.session_state !== "valid" ||
|
|
115
127
|
typeof session.device_token !== "string" ||
|
|
116
128
|
!session.device_token) {
|
|
117
|
-
return
|
|
129
|
+
return false;
|
|
118
130
|
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
async function deliverPendingInstallEvents(options, paths, deviceToken) {
|
|
119
134
|
const pending = (await readPendingInstallEventEntries(paths)).slice(0, 20);
|
|
120
135
|
const failures = [];
|
|
121
136
|
let delivered = 0;
|
|
122
137
|
let observedMinCliVersion = null;
|
|
123
138
|
for (let offset = 0; offset < pending.length; offset += 5) {
|
|
124
|
-
await Promise.all(pending.slice(offset, offset + 5).map(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const response = await options.io.fetch(`${entry.dashboard_url}/api/ambient/install-events`, {
|
|
129
|
-
method: "POST",
|
|
130
|
-
headers: {
|
|
131
|
-
"Content-Type": "application/json",
|
|
132
|
-
Authorization: `Bearer ${session.device_token}`,
|
|
133
|
-
},
|
|
134
|
-
body: JSON.stringify({
|
|
135
|
-
cli_version: entry.cli_version,
|
|
136
|
-
command: entry.command,
|
|
137
|
-
os_platform: entry.os_platform,
|
|
138
|
-
events: entry.events,
|
|
139
|
-
}),
|
|
140
|
-
signal: controller.signal,
|
|
141
|
-
});
|
|
142
|
-
if (!response.ok) {
|
|
143
|
-
throw new Error(`http_${response.status}`);
|
|
144
|
-
}
|
|
145
|
-
const receipt = (await response.json().catch((error) => {
|
|
146
|
-
// This reply carries the server-published `min_cli_version` floor
|
|
147
|
-
// (BLI-2678). A body that will not parse means the floor is not
|
|
148
|
-
// observed on this tick and the forced-update path silently does
|
|
149
|
-
// nothing — while the 2xx above says the receipt landed fine.
|
|
150
|
-
console.error("[install-receipts] receipt body unreadable; no min_cli_version observed", JSON.stringify({
|
|
151
|
-
reason: "receipt_body_unreadable",
|
|
152
|
-
http_status: response.status,
|
|
153
|
-
...describeError(error),
|
|
154
|
-
}));
|
|
155
|
-
return null;
|
|
156
|
-
}));
|
|
157
|
-
if (typeof receipt?.min_cli_version === "string" &&
|
|
158
|
-
receipt.min_cli_version.trim()) {
|
|
159
|
-
observedMinCliVersion = receipt.min_cli_version.trim();
|
|
160
|
-
}
|
|
161
|
-
await removeInstallEventEntry(paths, entry.outbox_id);
|
|
139
|
+
await Promise.all(pending.slice(offset, offset + 5).map((entry) => deliverInstallEventEntry(options.io, paths, entry, deviceToken, failures).then((floor) => {
|
|
140
|
+
if (floor != null)
|
|
141
|
+
observedMinCliVersion = floor;
|
|
142
|
+
if (floor !== undefined)
|
|
162
143
|
delivered += 1;
|
|
163
|
-
|
|
164
|
-
catch (error) {
|
|
165
|
-
const failureReason = classifyInstallTelemetryError(error);
|
|
166
|
-
failures.push(failureReason);
|
|
167
|
-
// The classified reason is the coarse bucket the outbox row keeps;
|
|
168
|
-
// beside it, what actually happened. `network_error` covers DNS,
|
|
169
|
-
// TLS, timeout and abort, and only one of those is worth waking up
|
|
170
|
-
// for (BLI-3238).
|
|
171
|
-
console.error("[install-receipts] install event delivery failed, entry kept for retry", JSON.stringify({
|
|
172
|
-
reason: failureReason,
|
|
173
|
-
outbox_id: entry.outbox_id,
|
|
174
|
-
...describeError(error),
|
|
175
|
-
}));
|
|
176
|
-
await recordInstallEventAttemptFailure(paths, entry, {
|
|
177
|
-
attemptedAt: new Date().toISOString(),
|
|
178
|
-
failureReason,
|
|
179
|
-
}).catch((writeError) => {
|
|
180
|
-
// Double failure: delivery failed AND the retry bookkeeping did.
|
|
181
|
-
// The entry stays queued, so nothing is lost, but the attempt
|
|
182
|
-
// count stops advancing and the outbox looks stuck for no reason.
|
|
183
|
-
console.error("[install-receipts] could not record the delivery failure against the entry", JSON.stringify({
|
|
184
|
-
reason: "attempt_bookkeeping_failed",
|
|
185
|
-
outbox_id: entry.outbox_id,
|
|
186
|
-
...describeError(writeError),
|
|
187
|
-
}));
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
finally {
|
|
191
|
-
clearTimeout(timeout);
|
|
192
|
-
}
|
|
193
|
-
}));
|
|
144
|
+
})));
|
|
194
145
|
}
|
|
195
146
|
if (delivered > 0) {
|
|
196
147
|
// The success branch says so too (BLI-3554 / the logging contract): a log
|
|
@@ -209,6 +160,70 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
209
160
|
}
|
|
210
161
|
return observedMinCliVersion;
|
|
211
162
|
}
|
|
163
|
+
async function deliverInstallEventEntry(io, paths, entry, deviceToken, failures) {
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
166
|
+
try {
|
|
167
|
+
const response = await postInstallEventEntry(io, entry, deviceToken, controller.signal);
|
|
168
|
+
const floor = await readObservedMinCliVersion(response);
|
|
169
|
+
await removeInstallEventEntry(paths, entry.outbox_id);
|
|
170
|
+
return floor;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
await keepFailedInstallEventEntry(paths, entry, error, failures);
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
clearTimeout(timeout);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function postInstallEventEntry(io, entry, deviceToken, signal) {
|
|
181
|
+
const response = await io.fetch(`${entry.dashboard_url}/api/ambient/install-events`, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${deviceToken}` },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
cli_version: entry.cli_version,
|
|
186
|
+
command: entry.command,
|
|
187
|
+
os_platform: entry.os_platform,
|
|
188
|
+
events: entry.events,
|
|
189
|
+
}),
|
|
190
|
+
signal,
|
|
191
|
+
});
|
|
192
|
+
if (!response.ok)
|
|
193
|
+
throw new Error(`http_${response.status}`);
|
|
194
|
+
return response;
|
|
195
|
+
}
|
|
196
|
+
async function readObservedMinCliVersion(response) {
|
|
197
|
+
const receipt = (await response.json().catch((error) => {
|
|
198
|
+
// This reply carries the server-published `min_cli_version` floor
|
|
199
|
+
// (BLI-2678). A body that will not parse means the floor is not
|
|
200
|
+
// observed on this tick and the forced-update path silently does
|
|
201
|
+
// nothing — while the 2xx above says the receipt landed fine.
|
|
202
|
+
console.error("[install-receipts] receipt body unreadable; no min_cli_version observed", JSON.stringify({ reason: "receipt_body_unreadable", http_status: response.status, ...describeError(error) }));
|
|
203
|
+
return null;
|
|
204
|
+
}));
|
|
205
|
+
return typeof receipt?.min_cli_version === "string" && receipt.min_cli_version.trim()
|
|
206
|
+
? receipt.min_cli_version.trim()
|
|
207
|
+
: null;
|
|
208
|
+
}
|
|
209
|
+
async function keepFailedInstallEventEntry(paths, entry, error, failures) {
|
|
210
|
+
const failureReason = classifyInstallTelemetryError(error);
|
|
211
|
+
failures.push(failureReason);
|
|
212
|
+
// The classified reason is the coarse bucket the outbox row keeps;
|
|
213
|
+
// beside it, what actually happened. `network_error` covers DNS,
|
|
214
|
+
// TLS, timeout and abort, and only one of those is worth waking up
|
|
215
|
+
// for (BLI-3238).
|
|
216
|
+
console.error("[install-receipts] install event delivery failed, entry kept for retry", JSON.stringify({ reason: failureReason, outbox_id: entry.outbox_id, ...describeError(error) }));
|
|
217
|
+
await recordInstallEventAttemptFailure(paths, entry, {
|
|
218
|
+
attemptedAt: new Date().toISOString(),
|
|
219
|
+
failureReason,
|
|
220
|
+
}).catch((writeError) => {
|
|
221
|
+
// Double failure: delivery failed AND the retry bookkeeping did.
|
|
222
|
+
// The entry stays queued, so nothing is lost, but the attempt
|
|
223
|
+
// count stops advancing and the outbox looks stuck for no reason.
|
|
224
|
+
console.error("[install-receipts] could not record the delivery failure against the entry", JSON.stringify({ reason: "attempt_bookkeeping_failed", outbox_id: entry.outbox_id, ...describeError(writeError) }));
|
|
225
|
+
});
|
|
226
|
+
}
|
|
212
227
|
function classifyInstallTelemetryError(error) {
|
|
213
228
|
if (error instanceof Error && error.name === "AbortError") {
|
|
214
229
|
return "timeout";
|
|
@@ -89,6 +89,8 @@ export function parseOpsArgs(args) {
|
|
|
89
89
|
// BLI-3912: one line per model — latency, tokens/s, today's spend, last
|
|
90
90
|
// probe verdict. A SECOND door like --memory, asked for only when named.
|
|
91
91
|
"--models",
|
|
92
|
+
"--turns",
|
|
93
|
+
"--tool-router",
|
|
92
94
|
"--person",
|
|
93
95
|
"--dry-run",
|
|
94
96
|
"--json",
|
|
@@ -130,6 +132,8 @@ export function parseOpsArgs(args) {
|
|
|
130
132
|
skips: values.booleans.has("--skips"),
|
|
131
133
|
memory: values.booleans.has("--memory") || memoryDays !== undefined,
|
|
132
134
|
models: values.booleans.has("--models"),
|
|
135
|
+
turns: values.booleans.has("--turns"),
|
|
136
|
+
toolRouter: values.booleans.has("--tool-router"),
|
|
133
137
|
...(memoryDays === undefined ? {} : { memoryDays: Number(memoryDays) }),
|
|
134
138
|
...base,
|
|
135
139
|
};
|
|
@@ -77,12 +77,25 @@ export function parseCalArgs(args) {
|
|
|
77
77
|
"--attendee",
|
|
78
78
|
],
|
|
79
79
|
});
|
|
80
|
+
const { action, rest } = readCalAction(values);
|
|
81
|
+
const { subject, query } = readCalSubjectOrQuery(action, rest);
|
|
82
|
+
const { title, startsAt, endsAt } = readCalCreateDetails(action, values);
|
|
83
|
+
validateCalSharing(action, values);
|
|
84
|
+
// Read in the order the flags have always been checked, so a caller who got
|
|
85
|
+
// two of them wrong is told about --limit first, as before.
|
|
86
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
87
|
+
const offset = readCalOffset(values);
|
|
88
|
+
return buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit);
|
|
89
|
+
}
|
|
90
|
+
function readCalAction(values) {
|
|
80
91
|
const first = values.positionals[0];
|
|
81
92
|
const action = (first === undefined ? "today" : first);
|
|
82
93
|
if (!CAL_ACTIONS.has(action)) {
|
|
83
94
|
throw new Error(`Unknown cal command: ${first}. Try today, week, next, find, calendars, add-ical, create, share, detach, or sync.`);
|
|
84
95
|
}
|
|
85
|
-
|
|
96
|
+
return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
|
|
97
|
+
}
|
|
98
|
+
function readCalSubjectOrQuery(action, rest) {
|
|
86
99
|
let subject;
|
|
87
100
|
let query;
|
|
88
101
|
if (CAL_ACTIONS_NEEDING_A_SUBJECT.has(action)) {
|
|
@@ -100,6 +113,9 @@ export function parseCalArgs(args) {
|
|
|
100
113
|
else if (rest.length > 0) {
|
|
101
114
|
throw new Error(`cal ${action} does not take "${rest[0]}".`);
|
|
102
115
|
}
|
|
116
|
+
return { subject, query };
|
|
117
|
+
}
|
|
118
|
+
function readCalCreateDetails(action, values) {
|
|
103
119
|
const title = optionalNonEmpty(values.flags.get("--title"));
|
|
104
120
|
const startsAt = optionalNonEmpty(values.flags.get("--at"));
|
|
105
121
|
const endsAt = optionalNonEmpty(values.flags.get("--until"));
|
|
@@ -114,15 +130,24 @@ export function parseCalArgs(args) {
|
|
|
114
130
|
if (!endsAt)
|
|
115
131
|
throw new Error("cal create needs --until <when it ends>.");
|
|
116
132
|
}
|
|
117
|
-
|
|
133
|
+
return { title, startsAt, endsAt };
|
|
134
|
+
}
|
|
135
|
+
function validateCalSharing(action, values) {
|
|
136
|
+
if (action === "share" &&
|
|
137
|
+
!values.booleans.has("--org-visible") &&
|
|
138
|
+
!values.booleans.has("--private")) {
|
|
118
139
|
throw new Error("cal share needs --org-visible (every member may read it) or --private (only you). Sharing is a decision, not a default.");
|
|
119
140
|
}
|
|
120
|
-
|
|
141
|
+
}
|
|
142
|
+
function readCalOffset(values) {
|
|
121
143
|
const offsetRaw = optionalNonEmpty(values.flags.get("--offset"));
|
|
122
144
|
const offset = offsetRaw === undefined ? undefined : Number.parseInt(offsetRaw, 10);
|
|
123
145
|
if (offset !== undefined && !Number.isFinite(offset)) {
|
|
124
146
|
throw new Error("--offset takes a whole number of days (today/next) or weeks (week).");
|
|
125
147
|
}
|
|
148
|
+
return offset;
|
|
149
|
+
}
|
|
150
|
+
function buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit) {
|
|
126
151
|
return {
|
|
127
152
|
kind: "cal",
|
|
128
153
|
action,
|