@bli-cockpit/cli 0.2.166 → 0.2.167
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.
|
@@ -197,6 +197,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
197
197
|
"labelled as such and is never actual spend. A super_admin sees everyone; a member sees their own row.",
|
|
198
198
|
"--since takes <n>d (1 to 365), <n>h (1 to 8760), or an ISO timestamp; --until an ISO timestamp (default now).",
|
|
199
199
|
"--detail adds Output, Input, Cache read and Cache creation. Counts use K/M/B; dollars are rounded.",
|
|
200
|
+
"PRs opened, PRs merged and Commits come from the pull requests Tower mirrors from GitHub, matched to people by GitHub login. n/a means Tower cannot match that person to a login: unknown, not zero. The footer says how many repositories the mirror holds, the range of its pull requests and what matched nobody; --json lists the repositories.",
|
|
200
201
|
"--include-automated adds harness, subagent and scheduled sessions, which are excluded by default.",
|
|
201
202
|
"It presses the same door as the /usage page and the usage_people MCP tool (GET /api/usage/people).",
|
|
202
203
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
@@ -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.167");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -17,4 +17,37 @@ export function formatUsageDollars(value) {
|
|
|
17
17
|
return new Intl.NumberFormat("en-US", {
|
|
18
18
|
style: "currency", currency: "USD", maximumFractionDigits: 0,
|
|
19
19
|
}).format(value);
|
|
20
|
+
}
|
|
21
|
+
const noun = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
22
|
+
/** A row whose counts are numbers, or ride on the same person's other row. Anything else is a gap. */
|
|
23
|
+
const PLACED = new Set(["github_login", "counted_on_other_account"]);
|
|
24
|
+
/**
|
|
25
|
+
* The sentences under the table, worded exactly as the /usage page and the
|
|
26
|
+
* `usage_people` MCP tool word them (`apps/dashboard/src/lib/usage/
|
|
27
|
+
* usage-github-words.ts` is the original; each copy has the same cases).
|
|
28
|
+
* `people` is whoever the table shows, so `--person` narrows the totals too.
|
|
29
|
+
*/
|
|
30
|
+
export function githubWords(github, people) {
|
|
31
|
+
const label = github.counts_label ?? "";
|
|
32
|
+
if (github.status === "unavailable") {
|
|
33
|
+
const why = github.detail ? ` (${github.detail})` : "";
|
|
34
|
+
return { lines: [`GitHub counts are unavailable: ${github.reason ?? "unknown_reason"}${why}.`], label };
|
|
35
|
+
}
|
|
36
|
+
const sum = (field) => people.reduce((total, person) => total + (person.github?.[field] ?? 0), 0);
|
|
37
|
+
const range = github.oldest_pull_request_at && github.newest_pull_request_at
|
|
38
|
+
? `; mirrored pull requests run from ${github.oldest_pull_request_at} to ${github.newest_pull_request_at}`
|
|
39
|
+
: "; the mirror holds no pull requests";
|
|
40
|
+
const lines = [
|
|
41
|
+
`GitHub: ${noun(sum("pull_requests_opened"), "pull request")} opened, ${sum("pull_requests_merged")} merged, ${noun(sum("commits"), "commit")}, ` +
|
|
42
|
+
`from ${noun((github.repositories ?? []).length, "mirrored repository", "mirrored repositories")}${range}.`,
|
|
43
|
+
];
|
|
44
|
+
const unmatched = github.unattributed;
|
|
45
|
+
if (unmatched && unmatched.pull_requests_opened + unmatched.pull_requests_merged + unmatched.commits > 0) {
|
|
46
|
+
lines.push(`Not matched to a person: ${noun(unmatched.pull_requests_opened, "pull request")} opened, ${unmatched.pull_requests_merged} merged, ${noun(unmatched.commits, "commit")}.`);
|
|
47
|
+
}
|
|
48
|
+
const unplaced = people.filter((person) => person.github && !PLACED.has(person.github.match)).length;
|
|
49
|
+
if (unplaced > 0) {
|
|
50
|
+
lines.push(`${noun(unplaced, "person cannot", "people cannot")} be matched to a GitHub login, so their GitHub counts are unknown, not zero.`);
|
|
51
|
+
}
|
|
52
|
+
return { lines, label };
|
|
20
53
|
}
|
package/dist/commands/usage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadPairedSession } from "../tower-client.js";
|
|
2
|
-
import { formatCount, formatUsageDollars } from "./usage-format.js";
|
|
2
|
+
import { formatCount, formatUsageDollars, githubWords } from "./usage-format.js";
|
|
3
3
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
4
4
|
import { writeLine } from "./cli-io.js";
|
|
5
5
|
export async function runUsage(command, io) {
|
|
@@ -71,22 +71,30 @@ export async function runUsage(command, io) {
|
|
|
71
71
|
for (const sub of subject.children ?? [])
|
|
72
72
|
nameWidth = Math.max(nameWidth, sub.subtopic.length + 4);
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
// BLI-12561: the three GitHub columns sit right after Coverage, ahead of the
|
|
75
|
+
// optional token splits, and only when the dashboard sent a `github` block.
|
|
76
|
+
// A sub-row has no GitHub counts of its own and prints blanks in their place,
|
|
77
|
+
// so its token splits stay under the right headings.
|
|
78
|
+
const hasGithub = body.github !== undefined;
|
|
79
|
+
const githubBlanks = hasGithub ? ["", "", ""] : [];
|
|
80
|
+
const githubCells = (row) => hasGithub ? [row.github?.pull_requests_opened, row.github?.pull_requests_merged, row.github?.commits].map((count) => count === null || count === undefined ? "n/a" : String(count)) : [];
|
|
81
|
+
const widths = [nameWidth, 8, 15, 12, ...(hasGithub ? [10, 10, 8] : []), 10, 10, 12, 14];
|
|
75
82
|
const printRow = (cells) => writeLine(io.stdout, cells.map((cell, index) => index === 0 ? cell.padEnd(widths[index]) : cell.padStart(widths[index])).join(" ").trimEnd());
|
|
76
|
-
printRow(["Person", "Tokens", "List equivalent", "Coverage", ...(command.detail ? ["Output", "Input", "Cache read", "Cache creation"] : [])]);
|
|
83
|
+
printRow(["Person", "Tokens", "List equivalent", "Coverage", ...(hasGithub ? ["PRs opened", "PRs merged", "Commits"] : []), ...(command.detail ? ["Output", "Input", "Cache read", "Cache creation"] : [])]);
|
|
77
84
|
for (const row of body.people ?? []) {
|
|
78
85
|
printRow([
|
|
79
86
|
(row.display_name ?? row.email ?? "Unknown").slice(0, 20),
|
|
80
87
|
formatCount(row.tokens_total),
|
|
81
88
|
formatUsageDollars(row.api_list_price_equivalent_usd),
|
|
82
89
|
`${row.sessions_extracted}/${row.sessions_observed}`,
|
|
90
|
+
...githubCells(row),
|
|
83
91
|
...(command.detail ? [row.output_tokens, row.input_tokens, row.cache_read_input_tokens, row.cache_creation_input_tokens].map(formatCount) : []),
|
|
84
92
|
]);
|
|
85
93
|
if (command.bySubject)
|
|
86
94
|
for (const subject of row.subjects ?? []) {
|
|
87
|
-
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) : [])]);
|
|
95
|
+
printRow([` ${subject.repo_label} / ${subject.topic}`, formatCount(subject.tokens), formatUsageDollars(subject.api_list_price_equivalent_usd), `${subject.extracted_sessions}/${subject.sessions}`, ...githubBlanks, ...(command.detail ? [subject.output, subject.input, subject.cache_read, subject.cache_creation].map(formatCount) : [])]);
|
|
88
96
|
for (const sub of subject.children ?? [])
|
|
89
|
-
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) : [])]);
|
|
97
|
+
printRow([` ${sub.subtopic}`, formatCount(sub.tokens), formatUsageDollars(sub.api_list_price_equivalent_usd), `${sub.extracted_sessions}/${sub.sessions}`, ...githubBlanks, ...(command.detail ? [sub.output, sub.input, sub.cache_read, sub.cache_creation].map(formatCount) : [])]);
|
|
90
98
|
}
|
|
91
99
|
if (command.byTask || command.byRepo || command.byTopic) {
|
|
92
100
|
const repos = (command.byTask ? row.tickets : command.byTopic ? row.task_types : row.repos) ?? [];
|
|
@@ -95,7 +103,7 @@ export async function runUsage(command, io) {
|
|
|
95
103
|
writeLine(io.stdout, ` ${repo.ticket_title ?? ""} Sources: ${Object.entries(repo.binding_sources ?? {}).map(([source, count]) => `${source} ${count}`).join(", ")}`);
|
|
96
104
|
if (command.byTopic && repo.confidence)
|
|
97
105
|
writeLine(io.stdout, ` Confidence: ${repo.confidence.high} high, ${repo.confidence.medium} medium, ${repo.confidence.low} low`);
|
|
98
|
-
printRow([` ${repo.repo_label}`, formatCount(repo.tokens), formatUsageDollars(repo.api_list_price_equivalent_usd), `${repo.extracted_sessions}/${repo.sessions}`, ...(command.detail ? [repo.output, repo.input, repo.cache_read, repo.cache_creation].map(formatCount) : [])]);
|
|
106
|
+
printRow([` ${repo.repo_label}`, formatCount(repo.tokens), formatUsageDollars(repo.api_list_price_equivalent_usd), `${repo.extracted_sessions}/${repo.sessions}`, ...githubBlanks, ...(command.detail ? [repo.output, repo.input, repo.cache_read, repo.cache_creation].map(formatCount) : [])]);
|
|
99
107
|
}
|
|
100
108
|
if (!command.all && repos.length > 10)
|
|
101
109
|
writeLine(io.stdout, ` and ${repos.length - 10} more ${command.byTask ? "tasks" : command.byTopic ? "topics" : "repos"}`);
|
|
@@ -126,5 +134,17 @@ export async function runUsage(command, io) {
|
|
|
126
134
|
writeLine(io.stdout, `${(body.people ?? []).reduce((n, person) => n + (person.sessions_summarized ?? 0), 0)} of ${body.coverage?.sessions_observed ?? 0} sessions summarized`);
|
|
127
135
|
if (command.byTopic)
|
|
128
136
|
writeLine(io.stdout, `${body.coverage?.sessions_labelled ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions labelled by topic`);
|
|
137
|
+
// BLI-12561: what the GitHub columns cover and what they leave out. A
|
|
138
|
+
// dashboard that sent no block is named rather than left to look as if there
|
|
139
|
+
// were nothing to show.
|
|
140
|
+
if (body.github === undefined) {
|
|
141
|
+
writeLine(io.stdout, "GitHub counts: this dashboard sent none (it predates them).");
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
const words = githubWords(body.github, body.people ?? []);
|
|
145
|
+
for (const line of words.lines)
|
|
146
|
+
writeLine(io.stdout, line);
|
|
147
|
+
if (words.label)
|
|
148
|
+
writeLine(io.stdout, words.label);
|
|
129
149
|
return 0;
|
|
130
150
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.167",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@bli-cockpit/memory-mcp": "0.1.45",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.85",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.57"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|