@bli-cockpit/cli 0.2.224 → 0.2.237
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/agent-door.js +35 -0
- package/dist/commands/approvals.js +3 -1
- package/dist/commands/brain.js +23 -0
- package/dist/commands/cal-overlay.js +63 -0
- package/dist/commands/cal.js +4 -0
- package/dist/commands/ci.js +23 -0
- package/dist/commands/docs-export.js +62 -0
- package/dist/commands/docs.js +3 -0
- package/dist/commands/doctor-usage-telemetry.js +2 -1
- package/dist/commands/drills-render.js +63 -0
- package/dist/commands/drills.js +152 -0
- package/dist/commands/hits.js +83 -0
- package/dist/commands/intentions.js +49 -0
- package/dist/commands/local-args-docs-export.js +23 -0
- package/dist/commands/local-args-tower-admin.js +8 -0
- package/dist/commands/local-args-tower-brain-ladder.js +90 -0
- package/dist/commands/local-args-tower-brain.js +11 -1
- package/dist/commands/local-args-tower-cal.js +6 -2
- package/dist/commands/local-args-tower-docs-msg.js +10 -146
- package/dist/commands/local-args-tower-drills.js +87 -0
- package/dist/commands/local-args-tower-hits.js +93 -0
- package/dist/commands/local-args-tower-intentions.js +70 -0
- package/dist/commands/local-args-tower-logs-search.js +51 -0
- package/dist/commands/local-args-tower-logs.js +3 -0
- package/dist/commands/local-args-tower-memory.js +67 -1
- package/dist/commands/local-args-tower-msg.js +149 -0
- package/dist/commands/local-args-tower-pages.js +3 -1
- package/dist/commands/local-args-tower-people.js +25 -0
- package/dist/commands/local-args-tower-usage.js +4 -4
- package/dist/commands/local-args-tower-who.js +36 -0
- package/dist/commands/local-args-tower.js +8 -1
- package/dist/commands/local-args.js +15 -1
- package/dist/commands/local-command-shapes-auth.js +1 -0
- package/dist/commands/local-command-shapes-drills.js +7 -0
- package/dist/commands/local-command-shapes-hits.js +5 -0
- package/dist/commands/local-help-commands-tower-ops.js +123 -1
- package/dist/commands/local-help-commands-tower.js +25 -10
- package/dist/commands/local-help-commands.js +3 -0
- package/dist/commands/local-help.js +26 -6
- package/dist/commands/local-run-ops.js +11 -0
- package/dist/commands/local.js +16 -8
- package/dist/commands/logs-search.js +45 -0
- package/dist/commands/logs.js +3 -0
- package/dist/commands/mail.js +6 -4
- package/dist/commands/memory-exam.js +47 -0
- package/dist/commands/memory-repeats.js +44 -0
- package/dist/commands/memory-search-print.js +95 -0
- package/dist/commands/memory-source.js +39 -0
- package/dist/commands/memory-store.js +18 -30
- package/dist/commands/memory-verb-list.js +7 -2
- package/dist/commands/msg.js +51 -9
- package/dist/commands/notes-reads.js +20 -0
- package/dist/commands/notes-writes.js +24 -0
- package/dist/commands/notes.js +4 -2
- package/dist/commands/people.js +29 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/spend.js +79 -0
- package/dist/commands/team.js +35 -0
- package/dist/commands/usage.js +9 -0
- package/dist/commands/who.js +36 -0
- package/dist/evidence-upload-direct.js +44 -0
- package/dist/evidence-upload-object.js +18 -4
- package/dist/evidence-upload-plan.js +1 -0
- package/dist/evidence-upload-transport.js +2 -0
- package/package.json +4 -4
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
const TAG = "[people cli]";
|
|
4
|
+
const READ_DEADLINE_MS = 30_000;
|
|
5
|
+
export async function runPeople(command, io) {
|
|
6
|
+
let door;
|
|
7
|
+
try {
|
|
8
|
+
door = await openAgentDoor("people", command, io);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const detail = error instanceof Error ? error.message : "this machine is not paired with Tower";
|
|
12
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "not_paired" })}`);
|
|
13
|
+
if (command.json)
|
|
14
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "not_paired", detail }));
|
|
15
|
+
else
|
|
16
|
+
writeLine(io.stderr, detail);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const path = command.person ? `/api/people?${new URLSearchParams({ person: command.person }).toString()}` : "/api/people";
|
|
20
|
+
const answer = await askAgentDoor(door, { path, method: "GET", label: "people show", timeoutMs: READ_DEADLINE_MS });
|
|
21
|
+
if (!answer.ok)
|
|
22
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
23
|
+
const body = answer.body;
|
|
24
|
+
writeLine(io.stderr, `${TAG} answered ${JSON.stringify({ view: command.person ? "person" : "list", people: body.people?.length ?? 1 })}`);
|
|
25
|
+
if (command.json)
|
|
26
|
+
return emitAgentDoor(door, body);
|
|
27
|
+
writeLine(io.stdout, body.text);
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.237");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit spend memory [--days 1|7|30] [--json]`: what each memory operation cost, held against the real OpenAI
|
|
3
|
+
* bill (BLI-19254).
|
|
4
|
+
*
|
|
5
|
+
* One read of `GET /api/ops/memory-spend`, the same door the `spend_memory` MCP tool and JARVIS call. The
|
|
6
|
+
* terminal prints the operations ranked by list dollars, top first, with real dollars, share of the ledger, the
|
|
7
|
+
* trend against the previous window, and then how much of OpenAI's own bill the ledger explains and what it does
|
|
8
|
+
* not (`untagged`, never hidden). `--json` prints the door's body untouched.
|
|
9
|
+
*
|
|
10
|
+
* A super_admin reads it; anyone else gets the door's own refusal reason, printed verbatim by `failAgentDoor`.
|
|
11
|
+
*/
|
|
12
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
13
|
+
import { writeLine } from "./cli-io.js";
|
|
14
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
15
|
+
const USAGE = "Usage: cockpit spend memory [--days 1|7|30] [--dashboard-url <url>] [--json]";
|
|
16
|
+
export function parseSpendArgs(args) {
|
|
17
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--days", "--json", "--home", "--dashboard-url"], valueFlags: ["--days", "--home", "--dashboard-url"] });
|
|
18
|
+
const [action, ...rest] = values.positionals;
|
|
19
|
+
if (action !== "memory" || rest.length > 0)
|
|
20
|
+
throw new Error(USAGE);
|
|
21
|
+
const raw = values.flags.get("--days");
|
|
22
|
+
const days = raw === undefined ? 7 : Number(raw);
|
|
23
|
+
if (days !== 1 && days !== 7 && days !== 30)
|
|
24
|
+
throw new Error(`--days must be 1, 7 or 30. ${USAGE}`);
|
|
25
|
+
return { kind: "spend", action: "memory", days, json: values.booleans.has("--json"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
26
|
+
}
|
|
27
|
+
const usd = (value) => (value === null || value === undefined ? "n/a" : value.toFixed(value >= 100 ? 0 : 2));
|
|
28
|
+
const money = (value) => (value === null || value === undefined ? "n/a" : `$${usd(value)}`);
|
|
29
|
+
const trend = (value) => (value === null ? "new" : `${value >= 0 ? "+" : ""}${value}%`);
|
|
30
|
+
const millions = (tokens) => `${(tokens / 1e6).toFixed(1)}M`;
|
|
31
|
+
export function renderMemorySpend(body) {
|
|
32
|
+
const lines = [];
|
|
33
|
+
const { window } = body;
|
|
34
|
+
lines.push(`Tower memory spend, last ${window.days} day${window.days === 1 ? "" : "s"} (${window.from} to ${window.to} UTC), as of ${body.asOf.slice(11, 16)}Z. Today is partial; trend is against the ${window.days} day${window.days === 1 ? "" : "s"} before (${window.previousFrom} to ${window.previousTo}).`);
|
|
35
|
+
if (body.operations.length === 0) {
|
|
36
|
+
lines.push("The ledger has no spend in this window yet. Live tallying starts at deploy; `backfill` rebuilds history from the older receipt tables.");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const header = ["operation", "list $", "real $", "share", "vs prev", "calls", "biggest callers"];
|
|
40
|
+
const rows = body.operations.map((op) => [
|
|
41
|
+
op.operation, usd(op.listUsd), usd(op.realUsd), `${Math.round(op.share * 100)}%`, trend(op.trendPct), String(op.calls),
|
|
42
|
+
op.topCallers.map((caller) => `${caller.caller} $${usd(caller.listUsd)}`).join(", "),
|
|
43
|
+
]);
|
|
44
|
+
rows.push(["total", usd(body.totals.listUsd), usd(body.totals.realUsd), "", trend(body.totals.trendPct), String(body.totals.calls), ""]);
|
|
45
|
+
const widths = header.map((cell, column) => Math.max(cell.length, ...rows.map((row) => row[column].length)));
|
|
46
|
+
const render = (cells) => cells.map((cell, column) => (column === 0 || column === 6 ? cell.padEnd(widths[column]) : cell.padStart(widths[column]))).join(" ").trimEnd();
|
|
47
|
+
lines.push("", render(header), ...rows.map(render));
|
|
48
|
+
}
|
|
49
|
+
const bill = body.reconcile;
|
|
50
|
+
lines.push("");
|
|
51
|
+
if (!bill.hasBill) {
|
|
52
|
+
lines.push(`OpenAI bill: not reconciled. ${bill.reason ?? "no reason recorded"}.`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
lines.push(`OpenAI bill for Tower's keys over ${bill.billDays} complete day${bill.billDays === 1 ? "" : "s"} (read ${bill.billAsOf?.slice(0, 16).replace("T", " ") ?? "?"}Z): list ${money(bill.towerBillListUsd)}, real ${money(bill.towerBillRealUsd)}.`);
|
|
56
|
+
lines.push(`The ledger explains ${bill.coveragePct === null ? "n/a" : `${bill.coveragePct}%`} of it (list ${money(bill.ledgerOpenAiListUsd)}). Untagged: list ${money(bill.untaggedListUsd)}, real ${money(bill.untaggedRealUsd)}, which no tagged call accounts for.`);
|
|
57
|
+
if (bill.tokenGaps.length > 0) {
|
|
58
|
+
lines.push("Untagged input tokens by model and tier (bill minus ledger): " + bill.tokenGaps.slice(0, 5).map((gap) => `${gap.model} ${gap.tier} ${millions(gap.untaggedInputTokens)}`).join("; ") + ".");
|
|
59
|
+
}
|
|
60
|
+
if ((bill.otherAppsBillListUsd ?? 0) > 0)
|
|
61
|
+
lines.push(`Other apps on the same OpenAI org (not Tower's, not counted above): list ${money(bill.otherAppsBillListUsd)}.`);
|
|
62
|
+
}
|
|
63
|
+
if (body.reads.failures.length > 0)
|
|
64
|
+
lines.push("", `Could not read: ${body.reads.failures.join(", ")}. The rest is shown as read.`);
|
|
65
|
+
for (const note of body.notes)
|
|
66
|
+
lines.push(note);
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
export async function runSpend(command, io) {
|
|
70
|
+
const door = await openAgentDoor("spend", command, io);
|
|
71
|
+
const answer = await askAgentDoor(door, { path: `/api/ops/memory-spend?days=${command.days}`, method: "GET", label: "spend memory", timeoutMs: 60_000 });
|
|
72
|
+
if (!answer.ok)
|
|
73
|
+
return failAgentDoor(door, "[spend cli]", answer.reason, answer.detail);
|
|
74
|
+
if (door.json)
|
|
75
|
+
return emitAgentDoor(door, answer.body);
|
|
76
|
+
for (const line of renderMemorySpend(answer.body))
|
|
77
|
+
writeLine(door.io.stdout, line);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
package/dist/commands/team.js
CHANGED
|
@@ -25,6 +25,8 @@ export async function runTeam(command, io) {
|
|
|
25
25
|
const tower = await openTower("team", command, io);
|
|
26
26
|
if (command.action.startsWith("github-login-"))
|
|
27
27
|
return githubLogin(command, tower, io);
|
|
28
|
+
if (command.action.startsWith("timezone-"))
|
|
29
|
+
return timezone(command, tower, io);
|
|
28
30
|
if (command.action === "members")
|
|
29
31
|
return showMembers(command, tower, io);
|
|
30
32
|
if (command.action === "invite")
|
|
@@ -272,6 +274,39 @@ async function githubLogin(command, tower, io) {
|
|
|
272
274
|
`GitHub logins: ${[body.primary_login, ...(Array.isArray(body.extra_logins) ? body.extra_logins : [])].filter(Boolean).join(", ") || "none"}. History is preserved.`);
|
|
273
275
|
return 0;
|
|
274
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* `cockpit team timezone list|set` (BLI-19277). The server owns every rule: who
|
|
279
|
+
* may ask (super admin), how a person is found (the one identity resolver, exact
|
|
280
|
+
* after case folding) and what counts as an IANA zone. This only relays and words
|
|
281
|
+
* the answer, so a stale CLI can be wrong about wording and never about a zone.
|
|
282
|
+
*/
|
|
283
|
+
async function timezone(command, tower, io) {
|
|
284
|
+
const setting = command.action === "timezone-set";
|
|
285
|
+
const result = await callTower(tower, { path: "/api/team/timezones", method: setting ? "PUT" : "GET", label: "team timezone",
|
|
286
|
+
...(setting ? { body: { person: command.personRef, timezone: command.zone } } : {}) });
|
|
287
|
+
writeLine(io.stderr, `[team cli] timezone ${JSON.stringify({ action: setting ? "set" : "list", reason: result.ok ? "answered" : result.reason })}`);
|
|
288
|
+
if (!result.ok)
|
|
289
|
+
return writeCommandFailure(io, command.json, result);
|
|
290
|
+
const body = asRecord(result.body);
|
|
291
|
+
if (command.json) {
|
|
292
|
+
writeLine(io.stdout, JSON.stringify(body));
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
if (setting) {
|
|
296
|
+
const name = String(body.display_name ?? command.personRef);
|
|
297
|
+
const before = String(body.before ?? "unknown");
|
|
298
|
+
const after = String(body.after ?? "unknown");
|
|
299
|
+
writeLine(io.stdout, body.changed === false ? `${name} already has ${after}. Nothing changed.` : `${name}: ${before} -> ${after}.`);
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
const people = (Array.isArray(body.people) ? body.people : []).map(asRecord);
|
|
303
|
+
const width = Math.max(6, ...people.map(person => String(person.display_name ?? "").length));
|
|
304
|
+
for (const person of people) {
|
|
305
|
+
writeLine(io.stdout, `${String(person.display_name ?? "").padEnd(width)} ${String(person.timezone_label ?? "unknown").padEnd(24)}${person.active === false ? " (inactive)" : ""}`);
|
|
306
|
+
}
|
|
307
|
+
writeLine(io.stdout, `${people.length} people, ${String(body.unknown_count ?? 0)} with an unknown timezone.`);
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
275
310
|
async function changeGrant(command, tower, io) {
|
|
276
311
|
const result = await callTower(tower, { path: "/api/team/grants", method: "POST", label: "team section grant",
|
|
277
312
|
body: { person_id: command.targetUserId, section: command.section, level: command.level ?? "see", action: command.action } });
|
package/dist/commands/usage.js
CHANGED
|
@@ -15,6 +15,8 @@ export async function runUsage(command, io) {
|
|
|
15
15
|
query.set("groupBy", "task_type");
|
|
16
16
|
if (command.byRepo)
|
|
17
17
|
query.set("groupBy", "repo");
|
|
18
|
+
if (command.byClient)
|
|
19
|
+
query.set("groupBy", "client");
|
|
18
20
|
if (command.until)
|
|
19
21
|
query.set("until", command.until);
|
|
20
22
|
if (command.includeAutomated)
|
|
@@ -43,6 +45,13 @@ export async function runUsage(command, io) {
|
|
|
43
45
|
if (!answer.ok)
|
|
44
46
|
return failAgentDoor(door, "[usage]", answer.reason, answer.detail);
|
|
45
47
|
const body = answer.body;
|
|
48
|
+
// Ticket 19258: the client app split reads everyone and prints the door's own words (medians always with their N).
|
|
49
|
+
if (command.byClient) {
|
|
50
|
+
if (door.json)
|
|
51
|
+
return emitAgentDoor(door, answer.body);
|
|
52
|
+
writeLine(io.stdout, String(answer.body.text ?? ""));
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
46
55
|
if (command.person) {
|
|
47
56
|
const email = command.person === "me" ? (await loadPairedSession("usage", command.homeDir)).email : command.person;
|
|
48
57
|
if (!email)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
const TAG = "[who cli]";
|
|
4
|
+
const READ_DEADLINE_MS = 30_000;
|
|
5
|
+
export async function runWho(command, io) {
|
|
6
|
+
let door;
|
|
7
|
+
try {
|
|
8
|
+
door = await openAgentDoor("who", command, io);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const detail = error instanceof Error ? error.message : "this machine is not paired with Tower";
|
|
12
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "not_paired" })}`);
|
|
13
|
+
if (command.json)
|
|
14
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "not_paired", detail }));
|
|
15
|
+
else
|
|
16
|
+
writeLine(io.stderr, detail);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const query = new URLSearchParams();
|
|
20
|
+
if (command.topic)
|
|
21
|
+
query.set("topic", command.topic);
|
|
22
|
+
if (command.layer)
|
|
23
|
+
query.set("layer", command.layer);
|
|
24
|
+
if (command.limit)
|
|
25
|
+
query.set("limit", String(command.limit));
|
|
26
|
+
const asked = query.toString();
|
|
27
|
+
const answer = await askAgentDoor(door, { path: asked ? `/api/who?${asked}` : "/api/who", method: "GET", label: "who", timeoutMs: READ_DEADLINE_MS });
|
|
28
|
+
if (!answer.ok)
|
|
29
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
30
|
+
const body = answer.body;
|
|
31
|
+
writeLine(io.stderr, `${TAG} answered ${JSON.stringify({ resolved_as: body.resolved_as, areas: body.areas.length, as_of: body.as_of })}`);
|
|
32
|
+
if (command.json)
|
|
33
|
+
return emitAgentDoor(door, body);
|
|
34
|
+
writeLine(io.stdout, body.text);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { encodeEvidenceBytes } from "@bli-cockpit/telemetry-core/evidence-storage-encoding";
|
|
2
|
+
import { noteSyncProgress } from "./sync-lock.js";
|
|
3
|
+
/** The signed capability is the only credential sent to Storage. Never log it,
|
|
4
|
+
* its query string, provider bodies, or thrown errors that might contain it. */
|
|
5
|
+
export async function putSignedChunk(options, signedUrl, bytes, uploadId, index) {
|
|
6
|
+
// Identical manifest + gzip framing to the server chunk route staging write.
|
|
7
|
+
const encoded = encodeEvidenceBytes(bytes);
|
|
8
|
+
console.error("[evidence-upload] chunk encoded for storage", JSON.stringify({
|
|
9
|
+
upload_id: uploadId, chunk_index: index, chunk_encoding: "gzip",
|
|
10
|
+
bytes_before: bytes.length, bytes_after: encoded.length,
|
|
11
|
+
}));
|
|
12
|
+
const attempts = options.maxAttemptsPerRequest ?? 3;
|
|
13
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
14
|
+
let status = 0;
|
|
15
|
+
try {
|
|
16
|
+
const response = await options.fetchImpl(signedUrl, {
|
|
17
|
+
method: "PUT", headers: { "Content-Type": "application/octet-stream", "x-upsert": "false" },
|
|
18
|
+
body: new Uint8Array(encoded), signal: AbortSignal.timeout(60_000), redirect: "error",
|
|
19
|
+
});
|
|
20
|
+
status = response.status;
|
|
21
|
+
await response.body?.cancel();
|
|
22
|
+
if (response.ok) {
|
|
23
|
+
noteSyncProgress();
|
|
24
|
+
log("signed_chunk_uploaded", uploadId, index, attempt, status);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
log("signed_upload_refused", uploadId, index, attempt, status);
|
|
28
|
+
if (status < 500 && status !== 429)
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
log("signed_upload_network_failure", uploadId, index, attempt, status);
|
|
33
|
+
}
|
|
34
|
+
if (attempt < attempts)
|
|
35
|
+
await (options.sleep ?? sleep)(250 * attempt);
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
function log(reason, uploadId, index, attempt, status) {
|
|
40
|
+
console.error("[evidence-upload] direct chunk", JSON.stringify({
|
|
41
|
+
reason, upload_id: uploadId, chunk_index: index, attempt, http_status: status,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { putSignedChunk } from "./evidence-upload-direct.js";
|
|
1
2
|
import { gzipSync } from "node:zlib";
|
|
2
3
|
import { COMMIT_CRASHED_PLATFORM, RawEvidenceUploadCommitResponseSchema, isPermanentUploadFailure, } from "@bli-cockpit/telemetry-core";
|
|
3
4
|
import { beginOneObject } from "./evidence-upload-terminal.js";
|
|
@@ -17,10 +18,11 @@ export async function uploadOneObject(options, entry, disposition, chunkSizeByte
|
|
|
17
18
|
if (disposition.disposition === "conflict" || !disposition.upload_id) {
|
|
18
19
|
return failedOutcome(entry.file, disposition.reason ?? "upload_conflict");
|
|
19
20
|
}
|
|
20
|
-
const
|
|
21
|
+
const directChunks = [];
|
|
22
|
+
const chunked = await uploadObjectChunks(options, entry, disposition, chunkSizeBytes, objectKey, directChunks);
|
|
21
23
|
if (!chunked.ok)
|
|
22
24
|
return chunked.outcome;
|
|
23
|
-
const committed = await commitUploadedObject(options, entry, disposition, objectKey, chunked.uploadedChunks);
|
|
25
|
+
const committed = await commitUploadedObject(options, entry, disposition, objectKey, chunked.uploadedChunks, directChunks);
|
|
24
26
|
if (committed.reason !== "commit_failed_http_409_staging_missing_between_passes")
|
|
25
27
|
return committed;
|
|
26
28
|
if (restageAttempted)
|
|
@@ -37,7 +39,7 @@ export async function uploadOneObject(options, entry, disposition, chunkSizeByte
|
|
|
37
39
|
}
|
|
38
40
|
return retried;
|
|
39
41
|
}
|
|
40
|
-
async function uploadObjectChunks(options, entry, disposition, chunkSizeBytes, objectKey) {
|
|
42
|
+
async function uploadObjectChunks(options, entry, disposition, chunkSizeBytes, objectKey, directChunks) {
|
|
41
43
|
const received = new Set(disposition.received_chunk_indexes);
|
|
42
44
|
let uploadedChunks = 0;
|
|
43
45
|
if (disposition.disposition === "already_committed" || disposition.commit_ready) {
|
|
@@ -47,6 +49,17 @@ async function uploadObjectChunks(options, entry, disposition, chunkSizeBytes, o
|
|
|
47
49
|
if (received.has(index))
|
|
48
50
|
continue;
|
|
49
51
|
const chunk = entry.bytes.subarray(index * chunkSizeBytes, Math.min((index + 1) * chunkSizeBytes, entry.bytes.byteLength));
|
|
52
|
+
const offer = disposition.signed_uploads?.find(upload => upload.chunk_index === index);
|
|
53
|
+
if (offer && await putSignedChunk(options, offer.signed_url, chunk, disposition.upload_id, index)) {
|
|
54
|
+
directChunks.push({ chunk_index: index, byte_size: chunk.length,
|
|
55
|
+
chunk_hash_sha256: sha256(chunk) });
|
|
56
|
+
uploadedChunks += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
console.error("[evidence-upload] using chunk route", JSON.stringify({
|
|
60
|
+
reason: offer ? "signed_upload_refused_fallback" : "signed_upload_not_offered",
|
|
61
|
+
upload_id: disposition.upload_id, chunk_index: index,
|
|
62
|
+
}));
|
|
50
63
|
const encodedChunk = gzipSync(chunk);
|
|
51
64
|
console.error("[evidence-upload] chunk encoded", JSON.stringify({
|
|
52
65
|
upload_id: disposition.upload_id, chunk_index: index, chunk_encoding: "gzip",
|
|
@@ -97,13 +110,14 @@ async function uploadObjectChunks(options, entry, disposition, chunkSizeBytes, o
|
|
|
97
110
|
}
|
|
98
111
|
return { ok: true, uploadedChunks };
|
|
99
112
|
}
|
|
100
|
-
async function commitUploadedObject(options, entry, disposition, objectKey, uploadedChunks) {
|
|
113
|
+
async function commitUploadedObject(options, entry, disposition, objectKey, uploadedChunks, directChunks) {
|
|
101
114
|
const commit = await requestJson(options, "/api/ambient/evidence/upload/commit", {
|
|
102
115
|
schema_version: "ambient-raw-evidence-upload-commit.v1",
|
|
103
116
|
generated_at: options.generatedAt,
|
|
104
117
|
provenance: options.provenance,
|
|
105
118
|
upload_id: disposition.upload_id,
|
|
106
119
|
object_key: objectKey,
|
|
120
|
+
...(disposition.signed_uploads ? { direct_chunks: directChunks } : {}),
|
|
107
121
|
});
|
|
108
122
|
if (!commit.ok) {
|
|
109
123
|
// The server can tell us this object will be refused again. Take its reason
|
|
@@ -225,6 +225,7 @@ export function readBeginDispositions(body) {
|
|
|
225
225
|
upload_id: entry.upload_id ?? null,
|
|
226
226
|
received_chunk_indexes: entry.received_chunk_indexes,
|
|
227
227
|
commit_ready: entry.commit_ready === true,
|
|
228
|
+
signed_uploads: entry.signed_uploads,
|
|
228
229
|
reason: entry.reason ?? null,
|
|
229
230
|
raw_evidence_pointer_id: entry.raw_evidence_pointer_id,
|
|
230
231
|
});
|
|
@@ -22,6 +22,8 @@ export async function requestJson(options, routePath, body) {
|
|
|
22
22
|
headers: {
|
|
23
23
|
"Authorization": `Bearer ${options.deviceToken}`,
|
|
24
24
|
"Content-Type": "application/json",
|
|
25
|
+
...(routePath.endsWith("/evidence/upload/begin")
|
|
26
|
+
? { "x-tower-evidence-direct": "staged-gzip-v1" } : {}),
|
|
25
27
|
},
|
|
26
28
|
body: JSON.stringify(body),
|
|
27
29
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.237",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.69",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.142",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.76"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|