@bli-cockpit/cli 0.2.108 → 0.2.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/local-args-tower-usage.js +8 -6
- package/dist/commands/local-help-commands-tower.js +4 -1
- package/dist/commands/local-help.js +4 -1
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/usage.js +34 -0
- package/dist/evidence-upload-object.js +9 -2
- package/dist/raw-evidence-staging.js +22 -1
- package/package.json +4 -4
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
2
2
|
export function parseUsageArgs(args) {
|
|
3
|
-
const values = parseNamedArgs(args, { allowedFlags: ["--min-confidence", "--by-topic", "--by-repo", "--person", "--all", "--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--min-confidence", "--person", "--since", "--until", "--home", "--dashboard-url"] });
|
|
4
|
-
if (
|
|
5
|
-
throw new Error("--by-repo and --by-topic cannot be used together.");
|
|
3
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--by-subject", "--subject", "--repo", "--session", "--from-window", "--min-confidence", "--by-topic", "--by-repo", "--person", "--all", "--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--subject", "--repo", "--session", "--from-window", "--min-confidence", "--person", "--since", "--until", "--home", "--dashboard-url"] });
|
|
4
|
+
if (["--by-repo", "--by-topic", "--by-subject"].filter(flag => values.booleans.has(flag)).length > 1)
|
|
5
|
+
throw new Error("--by-repo and --by-topic cannot be used together or with --by-subject.");
|
|
6
6
|
const minConfidence = values.flags.get("--min-confidence");
|
|
7
7
|
if (minConfidence !== undefined && !["high", "medium", "low"].includes(minConfidence))
|
|
8
8
|
throw new Error("--min-confidence accepts high, medium or low.");
|
|
9
9
|
const action = values.positionals[0] ?? "people";
|
|
10
|
-
if (
|
|
11
|
-
throw new Error("usage takes
|
|
12
|
-
|
|
10
|
+
if (!["people", "sessions"].includes(action) || values.positionals.length > 1)
|
|
11
|
+
throw new Error("usage takes people or sessions.");
|
|
12
|
+
if (action === "sessions" && !values.flags.get("--subject") && !values.flags.get("--session"))
|
|
13
|
+
throw new Error("usage sessions requires --subject or --session.");
|
|
14
|
+
return { bySubject: values.booleans.has("--by-subject"), subject: optionalNonEmpty(values.flags.get("--subject")), repo: optionalNonEmpty(values.flags.get("--repo")), session: optionalNonEmpty(values.flags.get("--session")), fromWindow: optionalNonEmpty(values.flags.get("--from-window")), kind: "usage", ...(minConfidence ? { minConfidence: minConfidence } : {}), byTopic: values.booleans.has("--by-topic"), byRepo: values.booleans.has("--by-repo"), person: optionalNonEmpty(values.flags.get("--person")), all: values.booleans.has("--all"), action: action, since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), detail: values.booleans.has("--detail"), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
|
|
13
15
|
}
|
|
@@ -143,7 +143,10 @@ export const TOWER_COMMAND_HELP = [
|
|
|
143
143
|
[
|
|
144
144
|
"usage",
|
|
145
145
|
[
|
|
146
|
-
"Usage: cockpit usage people [--by-repo | --by-topic] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
|
|
146
|
+
"Usage: cockpit usage people [--by-repo | --by-topic | --by-subject] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
|
|
147
|
+
" cockpit usage sessions --subject <name> [--repo <label>] [--since <window>] [--json]",
|
|
148
|
+
" cockpit usage sessions --session <id> [--from-window <n>] [--json]",
|
|
149
|
+
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
147
150
|
"",
|
|
148
151
|
"--by-topic adds topic rows, including (unlabelled). Choose only one grouping. --by-repo adds project rows under each person (top 10; --all shows every repo). --person me uses your signed-in email.",
|
|
149
152
|
"Claude Code and Codex usage per person: sessions observed and extracted, tokens (total, output,",
|
|
@@ -102,7 +102,10 @@ export function localCommandHelp(command) {
|
|
|
102
102
|
` cockpit search "<words>" [--kind ${SEARCH_KINDS.join(",")}] [--limit <n>] [--dashboard-url <url>] [--json]`,
|
|
103
103
|
" cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
104
104
|
" cockpit careers [list|show <id>|rescreen <id>] [--role <slug>] [--min-score <n>] [--since <date>] [--json]",
|
|
105
|
-
" cockpit usage
|
|
105
|
+
" cockpit usage sessions --subject <name> [--repo <label>] [--since <window>] [--json]",
|
|
106
|
+
" cockpit usage sessions --session <id> [--from-window <n>] [--json]",
|
|
107
|
+
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
108
|
+
" cockpit usage people [--by-repo | --by-topic | --by-subject] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--detail] [--since <n>d|<n>h|<iso>] [--until <iso>] [--include-automated] [--dashboard-url <url>] [--json]",
|
|
106
109
|
"",
|
|
107
110
|
`Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
|
|
108
111
|
].join("\n");
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.110");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
package/dist/commands/usage.js
CHANGED
|
@@ -5,6 +5,8 @@ import { writeLine } from "./cli-io.js";
|
|
|
5
5
|
export async function runUsage(command, io) {
|
|
6
6
|
const door = await openAgentDoor("usage", command, io);
|
|
7
7
|
const query = new URLSearchParams({ since: command.since });
|
|
8
|
+
if (command.bySubject)
|
|
9
|
+
query.set("groupBy", "topic");
|
|
8
10
|
if (command.minConfidence)
|
|
9
11
|
query.set("minConfidence", command.minConfidence);
|
|
10
12
|
if (command.byTopic)
|
|
@@ -15,6 +17,21 @@ export async function runUsage(command, io) {
|
|
|
15
17
|
query.set("until", command.until);
|
|
16
18
|
if (command.includeAutomated)
|
|
17
19
|
query.set("include_automated", "1");
|
|
20
|
+
if (command.action === "sessions") {
|
|
21
|
+
for (const [key, value] of [["subject", command.subject], ["repo", command.repo], ["session", command.session], ["fromWindow", command.fromWindow]])
|
|
22
|
+
if (value)
|
|
23
|
+
query.set(key, value);
|
|
24
|
+
const response = await askAgentDoor(door, { path: `/api/usage/sessions?${query}`, method: "GET", label: "usage sessions", timeoutMs: 30_000 });
|
|
25
|
+
if (!response.ok)
|
|
26
|
+
return failAgentDoor(door, "[usage]", response.reason, response.detail);
|
|
27
|
+
if (door.json || command.session)
|
|
28
|
+
return emitAgentDoor(door, response.body);
|
|
29
|
+
const sessions = response.body.sessions ?? [];
|
|
30
|
+
for (const session of sessions)
|
|
31
|
+
writeLine(io.stdout, `${session.session_id} ${formatCount(session.tokens)} ${session.task_type} ${session.title}`);
|
|
32
|
+
writeLine(io.stdout, `${sessions.length} sessions`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
18
35
|
const answer = await askAgentDoor(door, { path: `/api/usage/people?${query}`, method: "GET", label: "usage people", timeoutMs: 30_000 });
|
|
19
36
|
if (!answer.ok)
|
|
20
37
|
return failAgentDoor(door, "[usage]", answer.reason, answer.detail);
|
|
@@ -31,6 +48,8 @@ export async function runUsage(command, io) {
|
|
|
31
48
|
}
|
|
32
49
|
if (command.byTopic && body.people?.some((person) => !Array.isArray(person.task_types)))
|
|
33
50
|
return failAgentDoor(door, "[usage]", "topic_grouping_unavailable", "The dashboard returned person totals without topic rows. Deploy the dashboard topic split before using --by-topic.");
|
|
51
|
+
if (command.bySubject && body.people?.some(person => !Array.isArray(person.subjects)))
|
|
52
|
+
return failAgentDoor(door, "[usage]", "subject_grouping_unavailable", "Deploy the dashboard subject split before using --by-subject.");
|
|
34
53
|
if (door.json)
|
|
35
54
|
return emitAgentDoor(door, body);
|
|
36
55
|
let nameWidth = 20;
|
|
@@ -41,6 +60,13 @@ export async function runUsage(command, io) {
|
|
|
41
60
|
nameWidth = Math.max(nameWidth, repo.repo_label.length + 2);
|
|
42
61
|
}
|
|
43
62
|
}
|
|
63
|
+
if (command.bySubject)
|
|
64
|
+
for (const person of body.people ?? [])
|
|
65
|
+
for (const subject of person.subjects ?? []) {
|
|
66
|
+
nameWidth = Math.max(nameWidth, subject.repo_label.length + subject.topic.length + 5);
|
|
67
|
+
for (const sub of subject.children ?? [])
|
|
68
|
+
nameWidth = Math.max(nameWidth, sub.subtopic.length + 4);
|
|
69
|
+
}
|
|
44
70
|
const widths = [nameWidth, 8, 15, 12, 10, 10, 12, 14];
|
|
45
71
|
const printRow = (cells) => writeLine(io.stdout, cells.map((cell, index) => index === 0 ? cell.padEnd(widths[index]) : cell.padStart(widths[index])).join(" ").trimEnd());
|
|
46
72
|
printRow(["Person", "Tokens", "List equivalent", "Coverage", ...(command.detail ? ["Output", "Input", "Cache read", "Cache creation"] : [])]);
|
|
@@ -52,6 +78,12 @@ export async function runUsage(command, io) {
|
|
|
52
78
|
`${row.sessions_extracted}/${row.sessions_observed}`,
|
|
53
79
|
...(command.detail ? [row.output_tokens, row.input_tokens, row.cache_read_input_tokens, row.cache_creation_input_tokens].map(formatCount) : []),
|
|
54
80
|
]);
|
|
81
|
+
if (command.bySubject)
|
|
82
|
+
for (const subject of row.subjects ?? []) {
|
|
83
|
+
printRow([` ${subject.repo_label} / ${subject.topic}`, formatCount(subject.tokens), formatUsageDollars(subject.api_list_price_equivalent_usd), `${subject.extracted_sessions}/${subject.sessions}`, ...(command.detail ? [subject.output, subject.input, subject.cache_read, subject.cache_creation].map(formatCount) : [])]);
|
|
84
|
+
for (const sub of subject.children ?? [])
|
|
85
|
+
printRow([` ${sub.subtopic}`, formatCount(sub.tokens), formatUsageDollars(sub.api_list_price_equivalent_usd), `${sub.extracted_sessions}/${sub.sessions}`, ...(command.detail ? [sub.output, sub.input, sub.cache_read, sub.cache_creation].map(formatCount) : [])]);
|
|
86
|
+
}
|
|
55
87
|
if (command.byRepo || command.byTopic) {
|
|
56
88
|
const repos = (command.byTopic ? row.task_types : row.repos) ?? [];
|
|
57
89
|
for (const repo of command.all ? repos : repos.slice(0, 10)) {
|
|
@@ -66,6 +98,8 @@ export async function runUsage(command, io) {
|
|
|
66
98
|
writeLine(io.stdout, "");
|
|
67
99
|
writeLine(io.stdout, body.api_list_price_equivalent_label ?? "API list-price equivalent (not actual spend)");
|
|
68
100
|
writeLine(io.stdout, `${body.coverage?.sessions_extracted ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions extracted`);
|
|
101
|
+
if (command.bySubject)
|
|
102
|
+
writeLine(io.stdout, `${(body.people ?? []).reduce((n, person) => n + (person.sessions_summarized ?? 0), 0)} of ${body.coverage?.sessions_observed ?? 0} sessions summarized`);
|
|
69
103
|
if (command.byTopic)
|
|
70
104
|
writeLine(io.stdout, `${body.coverage?.sessions_labelled ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions labelled by topic`);
|
|
71
105
|
return 0;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { gzipSync } from "node:zlib";
|
|
1
2
|
import { COMMIT_CRASHED_PLATFORM, RawEvidenceUploadCommitResponseSchema, isPermanentUploadFailure, } from "@bli-cockpit/telemetry-core";
|
|
2
3
|
import { beginOneObject } from "./evidence-upload-terminal.js";
|
|
3
4
|
import { isNonJsonResponseBody, requestJson, safeFailureDetail, sha256, } from "./evidence-upload-transport.js";
|
|
@@ -46,8 +47,14 @@ async function uploadObjectChunks(options, entry, disposition, chunkSizeBytes, o
|
|
|
46
47
|
if (received.has(index))
|
|
47
48
|
continue;
|
|
48
49
|
const chunk = entry.bytes.subarray(index * chunkSizeBytes, Math.min((index + 1) * chunkSizeBytes, entry.bytes.byteLength));
|
|
50
|
+
const encodedChunk = gzipSync(chunk);
|
|
51
|
+
console.error("[evidence-upload] chunk encoded", JSON.stringify({
|
|
52
|
+
upload_id: disposition.upload_id, chunk_index: index, chunk_encoding: "gzip",
|
|
53
|
+
bytes_before: chunk.length, bytes_after: encodedChunk.length,
|
|
54
|
+
}));
|
|
49
55
|
const chunkResponse = await requestJson(options, "/api/ambient/evidence/upload/chunk", {
|
|
50
|
-
schema_version: "ambient-raw-evidence-upload-chunk.
|
|
56
|
+
schema_version: "ambient-raw-evidence-upload-chunk.v2",
|
|
57
|
+
encoding: "gzip",
|
|
51
58
|
generated_at: options.generatedAt,
|
|
52
59
|
provenance: options.provenance,
|
|
53
60
|
upload_id: disposition.upload_id,
|
|
@@ -55,7 +62,7 @@ async function uploadObjectChunks(options, entry, disposition, chunkSizeBytes, o
|
|
|
55
62
|
chunk_index: index,
|
|
56
63
|
chunk_count: entry.chunkCount,
|
|
57
64
|
chunk_hash_sha256: sha256(chunk),
|
|
58
|
-
content_base64:
|
|
65
|
+
content_base64: encodedChunk.toString("base64"),
|
|
59
66
|
});
|
|
60
67
|
if (!chunkResponse.ok) {
|
|
61
68
|
// Same treatment the commit path has had since BLI-2528: the server's
|
|
@@ -70,7 +70,9 @@ export function rawEvidenceStagingStatePath(stateDir) {
|
|
|
70
70
|
export async function readRawEvidenceStagingState(stateDir) {
|
|
71
71
|
try {
|
|
72
72
|
const raw = JSON.parse(await fs.readFile(rawEvidenceStagingStatePath(stateDir), "utf8"));
|
|
73
|
-
|
|
73
|
+
const state = parseStagingState(raw);
|
|
74
|
+
resetStorage403ForEncoding(state);
|
|
75
|
+
return state;
|
|
74
76
|
}
|
|
75
77
|
catch (error) {
|
|
76
78
|
// Empty is right before the first staged pack. Unreadable is not: this
|
|
@@ -160,6 +162,7 @@ export function recordDeliveryFailure(state, contentHash, options) {
|
|
|
160
162
|
const attemptedAtIso = options.attemptedAt.toISOString();
|
|
161
163
|
const entry = {
|
|
162
164
|
attempts,
|
|
165
|
+
...(previous?.encoding_retry_version ? { encoding_retry_version: previous.encoding_retry_version } : {}),
|
|
163
166
|
...(terminalDeliveryReason(options.reason) ? { terminal_reason: options.reason } : {}),
|
|
164
167
|
first_failed_at: previous?.first_failed_at ?? attemptedAtIso,
|
|
165
168
|
last_attempt_at: attemptedAtIso,
|
|
@@ -295,6 +298,7 @@ function parseAttemptEntry(value) {
|
|
|
295
298
|
const attempts = optionalNumber(record["attempts"]);
|
|
296
299
|
return {
|
|
297
300
|
...(terminalDeliveryReason(optionalString(record["last_reason"])) ? { terminal_reason: String(record["last_reason"]) } : {}),
|
|
301
|
+
...(record["encoding_retry_version"] === 1 ? { encoding_retry_version: 1 } : {}),
|
|
298
302
|
attempts: attempts > 0 ? attempts : 1,
|
|
299
303
|
first_failed_at: optionalString(record["first_failed_at"]) ?? lastAttemptAt,
|
|
300
304
|
last_attempt_at: lastAttemptAt,
|
|
@@ -320,4 +324,21 @@ function shortHash(value) {
|
|
|
320
324
|
/** A persisted clock cannot extend a retry beyond the last attempt's ceiling. */
|
|
321
325
|
export function deliveryRetryAtMs(entry) {
|
|
322
326
|
return Math.min(Date.parse(entry.next_attempt_at), Date.parse(entry.last_attempt_at) + EVIDENCE_DELIVERY_BACKOFF_MAX_MS);
|
|
327
|
+
}
|
|
328
|
+
/** One retry per object for the encoding repair, without erasing other refusals. */
|
|
329
|
+
export function resetStorage403ForEncoding(state) {
|
|
330
|
+
let count = 0;
|
|
331
|
+
for (const entry of Object.values(state.delivery_attempts)) {
|
|
332
|
+
if (entry.encoding_retry_version === 1 || !/^chunk_\d+_failed_http_503_chunk_staging_upload_refused_storage_403$/u.test(entry.last_reason))
|
|
333
|
+
continue;
|
|
334
|
+
entry.encoding_retry_version = 1;
|
|
335
|
+
entry.next_attempt_at = new Date(0).toISOString();
|
|
336
|
+
delete entry.terminal_reason;
|
|
337
|
+
count += 1;
|
|
338
|
+
}
|
|
339
|
+
if (count > 0)
|
|
340
|
+
console.error("[raw-evidence-staging] storage refusals offered with encoding", JSON.stringify({
|
|
341
|
+
reason: "storage_403_encoding_retry", chunk_encoding: "gzip", objects_reset: count,
|
|
342
|
+
}));
|
|
343
|
+
return count;
|
|
323
344
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.110",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,8 +27,8 @@
|
|
|
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-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.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.28",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.38",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.44"
|
|
33
33
|
}
|
|
34
34
|
}
|