@bli-cockpit/cli 0.2.277 → 0.2.279
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/codex-session-signals.js +17 -9
- package/dist/adapters/raw-evidence-facts.js +3 -0
- package/dist/adapters/raw-evidence-snapshot-supersede.js +7 -3
- package/dist/adapters/raw-evidence-transcript-body.js +23 -4
- package/dist/adapters/raw-evidence-transcript-stream.js +7 -6
- package/dist/adapters/raw-evidence-volatile-reuse.js +10 -0
- package/dist/commands/ai-spend.js +32 -0
- package/dist/commands/image.js +36 -2
- package/dist/commands/local-args-tower-image.js +11 -3
- package/dist/commands/local-args-tower.js +3 -1
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-help-commands-tower-ops.js +24 -0
- package/dist/commands/local-help.js +6 -0
- package/dist/commands/local-run-ops.js +4 -0
- package/dist/commands/local.js +2 -0
- package/dist/commands/pr-output.js +79 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/sources-verdict.js +2 -2
- package/dist/evidence-upload-direct.js +3 -1
- package/dist/evidence-upload-object.js +63 -8
- package/dist/evidence-upload-plan.js +75 -9
- package/dist/evidence-upload-staged.js +74 -7
- package/dist/evidence-upload-terminal.js +6 -9
- package/dist/image-description-session.js +49 -15
- package/dist/image-mode-state.js +5 -5
- package/dist/image-store-send.js +326 -0
- package/dist/image-store-stage.js +54 -0
- package/dist/image-store-state.js +78 -0
- package/dist/slim/keep-line-copy.js +34 -0
- package/package.json +4 -4
|
@@ -34,7 +34,11 @@ export async function readCodexMetadataSignals(filePath) {
|
|
|
34
34
|
const state = makeSignalExtractionState();
|
|
35
35
|
const stream = createReadStream(filePath);
|
|
36
36
|
let byteSize = 0;
|
|
37
|
-
|
|
37
|
+
// The line being assembled, in the pieces the chunks gave and its byte count kept as a running sum. The
|
|
38
|
+
// old `lineBuffer += segment` plus `Buffer.byteLength(lineBuffer)` flattened the whole growing string on
|
|
39
|
+
// every 64 KiB chunk, so a 1 MiB record made about 8 MiB of short-lived copies (BLI-23521).
|
|
40
|
+
let lineParts = [];
|
|
41
|
+
let lineBytes = 0;
|
|
38
42
|
let discardingOversizedLine = false;
|
|
39
43
|
for await (const chunk of stream) {
|
|
40
44
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
@@ -50,25 +54,29 @@ export async function readCodexMetadataSignals(filePath) {
|
|
|
50
54
|
discardingOversizedLine = false;
|
|
51
55
|
continue;
|
|
52
56
|
}
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
lineParts.push(segment);
|
|
58
|
+
lineBytes += Buffer.byteLength(segment, "utf8");
|
|
59
|
+
if (lineBytes > CODEX_ATTRIBUTION_MAX_LINE_BYTES) {
|
|
55
60
|
state.lineCount += 1;
|
|
56
61
|
state.parseErrorCount += 1;
|
|
57
|
-
|
|
62
|
+
lineParts = [];
|
|
63
|
+
lineBytes = 0;
|
|
58
64
|
discardingOversizedLine = !lineEnded;
|
|
59
65
|
continue;
|
|
60
66
|
}
|
|
61
67
|
if (lineEnded) {
|
|
62
|
-
absorbCodexSessionLine(state,
|
|
63
|
-
|
|
68
|
+
absorbCodexSessionLine(state, lineParts.length === 1 ? lineParts[0] : lineParts.join(""));
|
|
69
|
+
lineParts = [];
|
|
70
|
+
lineBytes = 0;
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
74
|
const rest = decoder.end();
|
|
68
75
|
if (rest)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
lineParts.push(rest);
|
|
77
|
+
const lastLine = lineParts.join("");
|
|
78
|
+
if (lastLine.trim())
|
|
79
|
+
absorbCodexSessionLine(state, lastLine);
|
|
72
80
|
observeSessionFactsFile(state.facts, { bytes: byteSize, lines: state.lineCount });
|
|
73
81
|
return {
|
|
74
82
|
signals: codexSignalsFromState(state),
|
|
@@ -269,6 +269,9 @@ function uploadFileFromEntry(entry) {
|
|
|
269
269
|
? { artifact_metadata: entry.artifact_metadata }
|
|
270
270
|
: {}),
|
|
271
271
|
...(entry.snapshot ? { source_key: entry.source_key, snapshot: entry.snapshot } : {}),
|
|
272
|
+
...(entry.supersedes?.rule === "prefix" && entry.supersedes.base
|
|
273
|
+
? { extend_base: { object_key: entry.supersedes.pointer_id, ...entry.supersedes.base } }
|
|
274
|
+
: {}),
|
|
272
275
|
};
|
|
273
276
|
}
|
|
274
277
|
function stageReasonLabel(state, priorPackCount) {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* says so: `snapshot_rewritten`, both sizes, and the byte range the first
|
|
21
21
|
* difference falls in. Never content.
|
|
22
22
|
*/
|
|
23
|
-
import {
|
|
23
|
+
import { chunksOfFileReusingOneBuffer } from "../evidence-upload-staged.js";
|
|
24
24
|
import { REDACTION_STUB_LABEL } from "@bli-cockpit/telemetry-core";
|
|
25
25
|
import { firstDifferenceRange, isPrefixSuccessor, prefixCheckpoints, prefixDigestsFromStream, } from "@bli-cockpit/telemetry-core/snapshot-supersede";
|
|
26
26
|
/**
|
|
@@ -73,7 +73,11 @@ function decideClaim(options, isPrefix, differenceRange) {
|
|
|
73
73
|
previous_byte_size: prior.byte_size,
|
|
74
74
|
byte_size: options.byteSize,
|
|
75
75
|
}));
|
|
76
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
pointer_id: prior.raw_evidence_pointer_id,
|
|
78
|
+
rule: "prefix",
|
|
79
|
+
base: { content_hash_sha256: prior.content_hash_sha256, byte_size: prior.byte_size },
|
|
80
|
+
};
|
|
77
81
|
}
|
|
78
82
|
const range = differenceRange(prior);
|
|
79
83
|
console.error("[evidence-upload] snapshot rewritten, keeping both", JSON.stringify({
|
|
@@ -101,7 +105,7 @@ export async function describeStagedSnapshot(options) {
|
|
|
101
105
|
const priorOffsets = wantClaim
|
|
102
106
|
? [prior.byte_size, ...Object.keys(prior.checkpoints ?? {}).map(Number)].filter((at) => at > 0 && at <= Math.min(prior.byte_size, byteLength))
|
|
103
107
|
: [];
|
|
104
|
-
const result = await prefixDigestsFromStream(
|
|
108
|
+
const result = await prefixDigestsFromStream(chunksOfFileReusingOneBuffer(options.filePath, 1024 * 1024), [...checkpointOffsets, ...priorOffsets]);
|
|
105
109
|
if (result.total_bytes !== byteLength) {
|
|
106
110
|
// The staged file is not the bytes that were hashed: a claim made on it would be a lie.
|
|
107
111
|
console.error("[evidence-upload] staged file size differs from what was written; no claim", JSON.stringify({
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* The order stays image replacement, slim, redact.
|
|
11
11
|
*/
|
|
12
12
|
import { isUtf8 } from "node:buffer";
|
|
13
|
-
import { mightHoldEmbeddedImages } from "@bli-cockpit/telemetry-core";
|
|
13
|
+
import { mightHoldEmbeddedImages, modeDropsImages, modeStoresImages } from "@bli-cockpit/telemetry-core";
|
|
14
14
|
import { describeError } from "../health-detail.js";
|
|
15
|
+
import { makeCaptureProvenance } from "./common.js";
|
|
15
16
|
import { roomToStreamToStaging } from "./raw-evidence-budget-admission.js";
|
|
16
17
|
import { readTranscriptWhole, streamTranscriptBody, volatilityOf } from "./raw-evidence-transcript-stream.js";
|
|
17
18
|
/** Which agent's transcript rules apply to this evidence kind; null for anything the image step does not know. */
|
|
@@ -27,8 +28,22 @@ export function holdDecodedText(source) {
|
|
|
27
28
|
if (!lossless) {
|
|
28
29
|
console.error("[raw-evidence] transcript is not valid UTF-8; keeping its bytes beside the text so an unchanged file uploads as it is", JSON.stringify({ reason: "invalid_utf8_bytes_kept", byte_size: bytes.byteLength }));
|
|
29
30
|
}
|
|
31
|
+
if (lossless)
|
|
32
|
+
releaseBackingStore(bytes);
|
|
30
33
|
return { text, exactBytes: lossless ? null : bytes };
|
|
31
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Free a file-sized buffer now instead of at the next collection (BLI-23521). Dropping the reference only
|
|
37
|
+
* makes it collectable; an ArrayBuffer is not collected until a GC runs, so on a 10 MB transcript the bytes,
|
|
38
|
+
* the decoded text and the encoded output were all resident at once (arrayBuffers 21 MB, RSS +30 MB over a
|
|
39
|
+
* 2.6 MB file). Only a buffer that owns its whole ArrayBuffer is detached: a slice of the shared pool is not ours.
|
|
40
|
+
*/
|
|
41
|
+
function releaseBackingStore(bytes) {
|
|
42
|
+
const store = bytes.buffer;
|
|
43
|
+
if (bytes.byteOffset !== 0 || store.byteLength !== bytes.byteLength)
|
|
44
|
+
return;
|
|
45
|
+
store.transfer?.(0);
|
|
46
|
+
}
|
|
32
47
|
/**
|
|
33
48
|
* BLI-16977: describe each distinct embedded image once and, when the one switch says so, swap the
|
|
34
49
|
* base64 for a short marker BEFORE masking, so the masker and the content hash see the final bytes.
|
|
@@ -41,6 +56,10 @@ export function holdDecodedText(source) {
|
|
|
41
56
|
export async function prepareTranscriptBody(collection, file) {
|
|
42
57
|
const session = collection.budget.images;
|
|
43
58
|
const agent = agentOfKind(file.kind);
|
|
59
|
+
// BLI-23329: `store_and_drop` talks to the upload doors for the images of this transcript, as this sync.
|
|
60
|
+
const provenance = session && modeStoresImages(session.mode) && collection.context
|
|
61
|
+
? makeCaptureProvenance(collection.context, "collector_runtime")
|
|
62
|
+
: undefined;
|
|
44
63
|
if (file.source.stream) {
|
|
45
64
|
// BLI-23317 stage D: the slimmed bytes are written to this pack's `files` directory as they are made,
|
|
46
65
|
// but only when the budget and the disk gate would admit a file of the source's size: never a byte ahead of the gate.
|
|
@@ -48,7 +67,7 @@ export async function prepareTranscriptBody(collection, file) {
|
|
|
48
67
|
if (refusal) {
|
|
49
68
|
console.error("[raw-evidence] streamed output is not written ahead of the staging gate", JSON.stringify({ reason: "stream_to_staging_declined", gate: refusal, kind: file.kind, input_bytes: file.source.stream.inputBytes ?? null }));
|
|
50
69
|
}
|
|
51
|
-
const streamed = await streamTranscriptBody(session, agent, { ...file.source.stream, sessionId: file.sessionId, kind: file.kind, stageDir: refusal ? undefined : collection.filesDir });
|
|
70
|
+
const streamed = await streamTranscriptBody(session, agent, { ...file.source.stream, sessionId: file.sessionId, kind: file.kind, stageDir: refusal ? undefined : collection.filesDir, ...(provenance ? { provenance } : {}) });
|
|
52
71
|
if (streamed) {
|
|
53
72
|
file.source.readBytes = streamed.readBytes;
|
|
54
73
|
return { held: streamed.held, volatile: streamed.volatile, pendingImages: streamed.pendingImages };
|
|
@@ -60,12 +79,12 @@ export async function prepareTranscriptBody(collection, file) {
|
|
|
60
79
|
if (!session || !agent)
|
|
61
80
|
return untouched(false, null);
|
|
62
81
|
// Drop mode slims every transcript, image-bearing or not; keep mode only describes the ones that can hold an image.
|
|
63
|
-
if (session.mode
|
|
82
|
+
if (!modeDropsImages(session.mode) && !mightHoldEmbeddedImages(file.source.bytes))
|
|
64
83
|
return untouched(false, null);
|
|
65
84
|
let held;
|
|
66
85
|
try {
|
|
67
86
|
held = holdDecodedText(file.source);
|
|
68
|
-
const prepared = await session.prepareTranscript({ text: held.text, source: agent, sessionId: file.sessionId });
|
|
87
|
+
const prepared = await session.prepareTranscript({ text: held.text, source: agent, sessionId: file.sessionId, ...(provenance ? { provenance } : {}) });
|
|
69
88
|
// BLI-3936: "volatile" = the next tick could build different bytes, or do image work, from
|
|
70
89
|
// this very file: an image still without a stored description (keep mode never replaces, so
|
|
71
90
|
// any image counts) or a slim step that failed and should be retried.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The transcript that is never read whole (BLI-23317
|
|
3
|
-
* streaming passes, and what comes back is the one slimmed string the sanitizer takes.
|
|
2
|
+
* The transcript that is never read whole (BLI-23317; keep mode too since BLI-23524, where the second pass
|
|
3
|
+
* copies the lines): its path goes to the image session's two streaming passes, and what comes back is the one slimmed string the sanitizer takes.
|
|
4
4
|
*
|
|
5
5
|
* Three endings, each named:
|
|
6
6
|
* - a body: the text is the slim form of the file (valid UTF-8 was checked across every chunk, so
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* read gives (`file_read_failed`, `file_too_large`), never a shorter file.
|
|
13
13
|
*/
|
|
14
14
|
import fs from "node:fs/promises";
|
|
15
|
+
import { modeDropsImages } from "@bli-cockpit/telemetry-core";
|
|
15
16
|
import { describeError } from "../health-detail.js";
|
|
16
17
|
import { TranscriptNotStreamable, TranscriptReadError, TranscriptTooLargeError, } from "./transcript-text-stream.js";
|
|
17
18
|
const TAG = "[raw-evidence]";
|
|
@@ -22,10 +23,10 @@ const TAG = "[raw-evidence]";
|
|
|
22
23
|
* stored description means the bytes change only when one of them gains one, so the memo may keep the entry.
|
|
23
24
|
*/
|
|
24
25
|
export function volatilityOf(mode, prepared) {
|
|
25
|
-
const imagesPending = mode
|
|
26
|
+
const imagesPending = modeDropsImages(mode)
|
|
26
27
|
? prepared.imageCount > prepared.replacedCount
|
|
27
28
|
: prepared.imageCount > 0;
|
|
28
|
-
const slimFailed = mode
|
|
29
|
+
const slimFailed = modeDropsImages(mode) && prepared.slim === null;
|
|
29
30
|
const volatile = imagesPending || slimFailed;
|
|
30
31
|
return { volatile, pendingImages: volatile && !slimFailed ? prepared.pendingImages : null };
|
|
31
32
|
}
|
|
@@ -35,9 +36,9 @@ export async function streamTranscriptBody(session, agent, file) {
|
|
|
35
36
|
return null;
|
|
36
37
|
}
|
|
37
38
|
try {
|
|
38
|
-
const prepared = await session.prepareTranscriptFile({ filePath: file.filePath, source: agent, sessionId: file.sessionId, maxFileBytes: file.maxFileBytes, stageDir: file.stageDir, omitText: file.stageDir !== undefined });
|
|
39
|
+
const prepared = await session.prepareTranscriptFile({ filePath: file.filePath, source: agent, sessionId: file.sessionId, maxFileBytes: file.maxFileBytes, stageDir: file.stageDir, omitText: file.stageDir !== undefined, ...(file.provenance ? { provenance: file.provenance } : {}) });
|
|
39
40
|
const { volatile, pendingImages } = volatilityOf(session.mode, prepared);
|
|
40
|
-
console.error(TAG + " transcript streamed through image swap and slim", JSON.stringify({ kind: file.kind, byte_size: prepared.readBytes, changed: prepared.changed, images: prepared.imageCount, volatile, staged_bytes: prepared.staged?.byteLength ?? null }));
|
|
41
|
+
console.error(TAG + " transcript streamed through image swap and slim", JSON.stringify({ kind: file.kind, mode: session.mode, byte_size: prepared.readBytes, changed: prepared.changed, images: prepared.imageCount, volatile, staged_bytes: prepared.staged?.byteLength ?? null }));
|
|
41
42
|
// BLI-23344: with a spool the text is not read back; the sanitizer reads the spool line by line.
|
|
42
43
|
return { held: { text: prepared.staged ? null : prepared.text, exactBytes: null, ...(prepared.staged ? { staged: prepared.staged } : {}) }, volatile, pendingImages, readBytes: prepared.readBytes };
|
|
43
44
|
}
|
|
@@ -10,6 +10,16 @@ export function trustVolatileEntry(collection, memo, known, kind, sourceKey, byt
|
|
|
10
10
|
const pending = known.pending_images ?? [];
|
|
11
11
|
if (!session)
|
|
12
12
|
return false;
|
|
13
|
+
// BLI-23329: in store mode the bytes change only when Tower confirms an image, never when a description arrives, so
|
|
14
|
+
// the gate asks about STORAGE progress: rebuild while the store could still take one of them (a closed session whose
|
|
15
|
+
// store failed is stored on the next tick with room), reuse once the budget is spent or every one is backing off.
|
|
16
|
+
if (session.mode === "store_and_drop") {
|
|
17
|
+
if (session.hasStoreRoom(pending)) {
|
|
18
|
+
logVolatileReuse(kind, sourceKey, known.content_hash, "rebuilt_store_has_room");
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
13
23
|
if (pending.some((sha256) => session.isDescribed(sha256))) {
|
|
14
24
|
memo.count("rebuilt_image_now_described", bytes);
|
|
15
25
|
logVolatileReuse(kind, sourceKey, known.content_hash, "rebuilt_image_now_described");
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { parseNamedArgs, optionalNonEmpty, optionalUrl } from "./local-arg-values.js";
|
|
4
|
+
const USAGE = "Usage: cockpit ai-spend flow|people|output [--days <1-365>] [--json]";
|
|
5
|
+
export function parseAiSpendArgs(args) {
|
|
6
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--days", "--json", "--home", "--dashboard-url"], valueFlags: ["--days", "--home", "--dashboard-url"] });
|
|
7
|
+
const [action, ...rest] = values.positionals;
|
|
8
|
+
if ((action !== "flow" && action !== "people" && action !== "output") || rest.length)
|
|
9
|
+
throw new Error(USAGE);
|
|
10
|
+
const raw = values.flags.get("--days");
|
|
11
|
+
let days;
|
|
12
|
+
if (raw !== undefined) {
|
|
13
|
+
days = Number(raw);
|
|
14
|
+
if (!Number.isInteger(days) || days < 1 || days > 365)
|
|
15
|
+
throw new Error(USAGE);
|
|
16
|
+
}
|
|
17
|
+
return { kind: "ai-spend", action, days, json: values.booleans.has("--json"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
18
|
+
}
|
|
19
|
+
export async function runAiSpend(command, io) {
|
|
20
|
+
const door = await openAgentDoor("ai-spend", command, io);
|
|
21
|
+
const query = new URLSearchParams({ view: command.action });
|
|
22
|
+
if (command.days !== undefined)
|
|
23
|
+
query.set("days", String(command.days));
|
|
24
|
+
const answer = await askAgentDoor(door, { path: `/api/ai-spend?${query}`, method: "GET", label: `ai-spend ${command.action}`, timeoutMs: 30_000 });
|
|
25
|
+
if (!answer.ok)
|
|
26
|
+
return failAgentDoor(door, "[ai-spend cli]", answer.reason, answer.detail);
|
|
27
|
+
if (command.json)
|
|
28
|
+
return emitAgentDoor(door, answer.body);
|
|
29
|
+
for (const line of answer.body.lines)
|
|
30
|
+
writeLine(io.stdout, line);
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
package/dist/commands/image.js
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
* large or not an image is refused here, before anything is sent.
|
|
13
13
|
*/
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
|
-
import { readFile, stat } from "node:fs/promises";
|
|
15
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { IMAGE_DESCRIBE_MAX_BYTES, formatImageMarker, imageSizeLabel, sniffImageMediaType, } from "@bli-cockpit/telemetry-core";
|
|
18
|
-
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
18
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, fetchAgentDoorFile, openAgentDoor } from "./agent-door.js";
|
|
19
19
|
import { writeLine } from "./cli-io.js";
|
|
20
20
|
const TAG = "[image cli]";
|
|
21
21
|
const READ_DEADLINE_MS = 30_000;
|
|
@@ -94,7 +94,41 @@ async function describeImage(command, door, request) {
|
|
|
94
94
|
writeLine(out, `Transcript marker: ${formatImageMarker({ sha256: request.sha256, mediaType: request.media_type, byteSize: request.byte_size })}`);
|
|
95
95
|
return 0;
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* `show --bytes <file>` (BLI-23329): the stored image itself, written to a NEW file. The door decodes the evidence
|
|
99
|
+
* envelope and hashes the result; this side hashes again before a byte reaches the disk, so a body that is not the
|
|
100
|
+
* picture its name promises is never written. An existing file is never overwritten.
|
|
101
|
+
*/
|
|
102
|
+
async function saveImageBytes(command, door, file) {
|
|
103
|
+
const sha256 = command.sha256 ?? "";
|
|
104
|
+
const query = new URLSearchParams({ sha256, bytes: "1" });
|
|
105
|
+
const answer = await fetchAgentDoorFile(door, { path: `${DOOR_PATH}?${query.toString()}`, label: "image show bytes", timeoutMs: READ_DEADLINE_MS });
|
|
106
|
+
if (!answer.ok)
|
|
107
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
108
|
+
const actual = createHash("sha256").update(answer.bytes).digest("hex");
|
|
109
|
+
if (actual !== sha256) {
|
|
110
|
+
return failAgentDoor(door, TAG, "bytes_do_not_match_sha256", "Tower sent bytes whose sha256 is not the one you asked for, so nothing was written.");
|
|
111
|
+
}
|
|
112
|
+
const mediaType = sniffImageMediaType(answer.bytes);
|
|
113
|
+
const target = path.resolve(file);
|
|
114
|
+
try {
|
|
115
|
+
await writeFile(target, answer.bytes, { flag: "wx", mode: 0o600 });
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const code = error?.code;
|
|
119
|
+
const reason = code === "EEXIST" ? "output_exists" : "output_unwritable";
|
|
120
|
+
return failAgentDoor(door, TAG, reason, code === "EEXIST" ? `${file} already exists; choose a new file name. Nothing was written.` : `Could not write ${file}. Nothing was written.`);
|
|
121
|
+
}
|
|
122
|
+
writeLine(door.io.stderr, `${TAG} bytes saved ${JSON.stringify({ sha256: sha256.slice(0, 16), byte_size: answer.bytes.length, media_type: mediaType ?? "unknown", verified: true })}`);
|
|
123
|
+
const result = { ok: true, sha256, media_type: mediaType, byte_size: answer.bytes.length, path: target, verified: true };
|
|
124
|
+
if (door.json)
|
|
125
|
+
return emitAgentDoor(door, result);
|
|
126
|
+
writeLine(door.io.stdout, `Wrote ${imageSizeLabel(answer.bytes.length)} ${mediaType?.replace("image/", "") ?? "image"} to ${target}; its sha256 matches ${sha256.slice(0, 16)}….`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
97
129
|
async function showImage(command, door) {
|
|
130
|
+
if (command.bytesFile)
|
|
131
|
+
return saveImageBytes(command, door, command.bytesFile);
|
|
98
132
|
const query = new URLSearchParams({ sha256: command.sha256 ?? "" });
|
|
99
133
|
const answer = await askAgentDoor(door, { path: `${DOOR_PATH}?${query.toString()}`, method: "GET", label: "image show", timeoutMs: READ_DEADLINE_MS });
|
|
100
134
|
if (!answer.ok)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* cockpit image describe <file> [--session <id>] [--json]
|
|
5
5
|
* cockpit image show <sha256-or-prefix> [--json]
|
|
6
|
+
* cockpit image show <sha256> --bytes <file> [--json]
|
|
6
7
|
*
|
|
7
8
|
* Tower swaps the base64 pictures inside collected transcripts for a short written description.
|
|
8
9
|
* `describe` sends one image file through that same door by hand; `show` looks a description up by
|
|
@@ -14,8 +15,8 @@ const ONE_VERB = "image takes one verb: describe or show. Example: `cockpit imag
|
|
|
14
15
|
const SESSION_ID_MAX_CHARS = 200;
|
|
15
16
|
export function parseImageArgs(args) {
|
|
16
17
|
const values = parseNamedArgs(args, {
|
|
17
|
-
allowedFlags: ["--session", "--home", "--dashboard-url", "--json"],
|
|
18
|
-
valueFlags: ["--session", "--home", "--dashboard-url"],
|
|
18
|
+
allowedFlags: ["--session", "--bytes", "--home", "--dashboard-url", "--json"],
|
|
19
|
+
valueFlags: ["--session", "--bytes", "--home", "--dashboard-url"],
|
|
19
20
|
});
|
|
20
21
|
const [verb, ...rest] = values.positionals;
|
|
21
22
|
if (verb !== "describe" && verb !== "show")
|
|
@@ -28,6 +29,9 @@ export function parseImageArgs(args) {
|
|
|
28
29
|
throw new Error("--session does not belong to `cockpit image show`.");
|
|
29
30
|
if (session !== undefined && session.length > SESSION_ID_MAX_CHARS)
|
|
30
31
|
throw new Error(`--session is at most ${SESSION_ID_MAX_CHARS} characters.`);
|
|
32
|
+
const bytesFile = optionalNonEmpty(values.flags.get("--bytes"));
|
|
33
|
+
if (bytesFile !== undefined && verb !== "show")
|
|
34
|
+
throw new Error("--bytes belongs to `cockpit image show`.");
|
|
31
35
|
const common = {
|
|
32
36
|
kind: "image",
|
|
33
37
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
@@ -36,7 +40,11 @@ export function parseImageArgs(args) {
|
|
|
36
40
|
};
|
|
37
41
|
if (verb === "describe")
|
|
38
42
|
return { ...common, action: "describe", file: rest[0], ...(session ? { session } : {}) };
|
|
39
|
-
|
|
43
|
+
const sha256 = readHashQuery(rest[0]);
|
|
44
|
+
if (bytesFile !== undefined && sha256.length !== 64) {
|
|
45
|
+
throw new Error("--bytes needs the whole sha256 (64 hex characters): a 16-character marker prefix names a description, not one stored image. `cockpit image show <prefix>` first, then the full hash it prints.");
|
|
46
|
+
}
|
|
47
|
+
return { ...common, action: "show", sha256, ...(bytesFile ? { bytesFile } : {}) };
|
|
40
48
|
}
|
|
41
49
|
/** A marker is pasted whole as often as not (`sha256:ab12…`), so accept that spelling and trim it. */
|
|
42
50
|
function readHashQuery(typed) {
|
|
@@ -57,4 +57,6 @@ export { parseGlossaryArgs } from "./glossary.js";
|
|
|
57
57
|
export { parseSpendArgs } from "./spend.js";
|
|
58
58
|
export { parsePushArgs } from "./push.js";
|
|
59
59
|
export { parseCiArgs } from "./ci.js";
|
|
60
|
-
export { parseDeliveryArgs } from "./delivery.js";
|
|
60
|
+
export { parseDeliveryArgs } from "./delivery.js";
|
|
61
|
+
export { parsePrOutputArgs } from "./pr-output.js";
|
|
62
|
+
export { parseAiSpendArgs } from "./ai-spend.js";
|
|
@@ -23,7 +23,7 @@ import { parseSlackPrivacyArgs } from "./slack-privacy-args.js";
|
|
|
23
23
|
import { parseMilestoneArgs } from "./local-args-tower-milestone.js";
|
|
24
24
|
import { SURFACE_ALIASES } from "./local-help.js";
|
|
25
25
|
import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseSourcesArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
|
|
26
|
-
import { parseCiArgs, parseDeliveryArgs, parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseGithubArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseSetupArgs, parseSpeedArgs, parseLogsArgs, parseBrainArgs, parseHitsArgs, parseIntentionsArgs, parseWhoArgs, parsePeopleArgs, parseApprovalsArgs, parseDrillsArgs, parseManagerArgs, parseImageArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
|
|
26
|
+
import { parseCiArgs, parseDeliveryArgs, parsePrOutputArgs, parseAiSpendArgs, parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseGithubArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseSetupArgs, parseSpeedArgs, parseLogsArgs, parseBrainArgs, parseHitsArgs, parseIntentionsArgs, parseWhoArgs, parsePeopleArgs, parseApprovalsArgs, parseDrillsArgs, parseManagerArgs, parseImageArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
|
|
27
27
|
// `normalizeUrl` has always been part of this module's surface — `local.ts` and
|
|
28
28
|
// `local-auth.ts` import it from here — so it stays exported from this address
|
|
29
29
|
// even though it now lives next door. The same goes for the four names the
|
|
@@ -67,6 +67,8 @@ const COMMAND_PARSERS = new Map([
|
|
|
67
67
|
["scout", (_command, rest) => parseScoutArgs(rest)],
|
|
68
68
|
["ci", (_command, rest) => parseCiArgs(rest)],
|
|
69
69
|
["delivery", (_command, rest) => parseDeliveryArgs(rest)],
|
|
70
|
+
["pr-output", (_command, rest) => parsePrOutputArgs(rest)],
|
|
71
|
+
["ai-spend", (_command, rest) => parseAiSpendArgs(rest)],
|
|
70
72
|
["ops", (_command, rest) => parseOpsArgs(rest)],
|
|
71
73
|
// BLI-23244: `slack privacy` has its own flags and its own file.
|
|
72
74
|
["slack", (_command, rest) => (rest.includes("privacy") ? parseSlackPrivacyArgs(rest.filter((arg) => arg !== "privacy")) : parseSlackArgs(rest))],
|
|
@@ -298,10 +298,12 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
298
298
|
[
|
|
299
299
|
"Usage: cockpit image describe <file> [--session <id>] [--dashboard-url <url>] [--json]",
|
|
300
300
|
" cockpit image show <sha256-or-prefix> [--dashboard-url <url>] [--json]",
|
|
301
|
+
" cockpit image show <sha256> --bytes <file> [--dashboard-url <url>] [--json]",
|
|
301
302
|
"",
|
|
302
303
|
"Tower swaps the base64 pictures inside collected transcripts for a short written description kept against the image's sha256.",
|
|
303
304
|
" describe send one PNG, JPEG, GIF or WebP file (up to 3 MB) to the describe door; prints the description and the marker a transcript would carry",
|
|
304
305
|
" show look a stored description up by its sha256, or the first 12 to 64 characters of it (a marker carries 16)",
|
|
306
|
+
" show --bytes <file> write the stored IMAGE to a new file (needs the whole 64-character sha256). Tower keeps the image itself only for machines on the store_and_drop switch; the bytes are decoded, checked against the sha256 and refused if they differ. An existing file is never overwritten.",
|
|
305
307
|
"A file that is too large or not an image is refused here by name, before anything is sent. An image Tower already described answers from storage.",
|
|
306
308
|
"--json writes the door's body to stdout and nothing else; a refusal is { ok: false, error, detail } and exits 1.",
|
|
307
309
|
"The bli-tower MCP server has the twins: image_describe, image_show.",
|
|
@@ -409,6 +411,17 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
409
411
|
"Run `cockpit login` first if this machine is not paired.",
|
|
410
412
|
],
|
|
411
413
|
],
|
|
414
|
+
[
|
|
415
|
+
"ai-spend",
|
|
416
|
+
[
|
|
417
|
+
"Usage: cockpit ai-spend flow [--days <1-365>] [--json]",
|
|
418
|
+
" cockpit ai-spend people [--days <1-365>] [--json]",
|
|
419
|
+
" cockpit ai-spend output [--days <1-365>] [--json]",
|
|
420
|
+
"flow: where AI spend went, by what each session's work became: Shipped (a linked PR merged), In review (open), Closed (closed unmerged), No PR (the link check ran and found none), Active (still running) and Unlinked (no branch or commit to check), with dollars, tokens, share and the coverage of the PR link check.",
|
|
421
|
+
"people: per account and tool, what Tower estimated at list price from session token counts and what the tool itself reported, each labelled by source (estimated, client_reported); actual (vendor-billed) and flat-rate plan cost are named as not connected. output: list-price dollars per expert hour of the merged PRs sessions link to (an AI estimate of hours, not measured time); says no output scores yet when none exist.",
|
|
422
|
+
"Dollars are API list-price equivalents, not invoices. MCP twins: ai_spend_flow, ai_spend_people, ai_spend_output. Web: TEAM > AI spend.",
|
|
423
|
+
],
|
|
424
|
+
],
|
|
412
425
|
[
|
|
413
426
|
"ci",
|
|
414
427
|
["Usage: cockpit ci report [--repo <owner/name>] [--json]", "Reads the daily stored CI report, as-of age and coverage. MCP twin: ci_report."],
|
|
@@ -421,6 +434,17 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
421
434
|
"--review-minutes is an assumption for the review-hours line, not a measurement. Counts show their denominators; no verdict is given. MCP twin: delivery_read. Web: TEAM > Delivery.",
|
|
422
435
|
],
|
|
423
436
|
],
|
|
437
|
+
[
|
|
438
|
+
"pr-output",
|
|
439
|
+
[
|
|
440
|
+
"Usage: cockpit pr-output list [--days <1-365>] [--limit <1-200>] [--cursor <value>] [--json]",
|
|
441
|
+
" cockpit pr-output correct <owner/name#number> --hours <0-2000> [--reason <text>] [--json]",
|
|
442
|
+
" cockpit pr-output estimate [--limit <1-25>] [--days <1-365>] [--json]",
|
|
443
|
+
"list: merged PRs with an AI estimate of the hours an expert engineer would have needed before AI (1 unit = 1 expert hour), a one-line reason, type, component, whether a person corrected it, and the coverage line (N of M merged PRs estimated). It is an estimate, not a measured or validated pre-AI time.",
|
|
444
|
+
"correct: sets your corrected hours on a PR's current estimate; the AI estimate and every earlier correction are kept. estimate: super admin only; estimates merged PRs that have no estimate for their current revision (spends model credit; a re-run on the same revision writes nothing).",
|
|
445
|
+
"MCP twins: pr_output_list, pr_output_correct, pr_output_estimate. Web: TEAM > Output.",
|
|
446
|
+
],
|
|
447
|
+
],
|
|
424
448
|
[
|
|
425
449
|
"ops",
|
|
426
450
|
[
|
|
@@ -32,6 +32,7 @@ export const SURFACE_ALIASES = new Map([
|
|
|
32
32
|
["calendar", "cal"],
|
|
33
33
|
["messages", "msg"],
|
|
34
34
|
["meeting-notes", "notes"],
|
|
35
|
+
["pr", "pr-output"], // BLI-23364: the MCP tools are the `pr_output_*` family
|
|
35
36
|
]);
|
|
36
37
|
export const rootCommandNames = new Set([
|
|
37
38
|
"roots",
|
|
@@ -54,6 +55,8 @@ export const rootCommandNames = new Set([
|
|
|
54
55
|
"scout",
|
|
55
56
|
"ci",
|
|
56
57
|
"delivery",
|
|
58
|
+
"pr-output",
|
|
59
|
+
"ai-spend",
|
|
57
60
|
"ops",
|
|
58
61
|
"slack",
|
|
59
62
|
"settings",
|
|
@@ -123,6 +126,8 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
123
126
|
" cockpit scout [board|start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
|
|
124
127
|
" cockpit ci report [--repo <owner/name>] [--json]",
|
|
125
128
|
" cockpit delivery report [--days <1-30>] [--end <YYYY-MM-DD>] [--review-minutes <1-240>] [--json]",
|
|
129
|
+
" cockpit pr-output [list [--days <1-365>] [--limit <1-200>] [--cursor <value>] | correct <owner/name#number> --hours <0-2000> [--reason <text>] | estimate [--limit <1-25>] [--days <1-365>]] [--json]",
|
|
130
|
+
" cockpit ai-spend (flow | people | output) [--days <1-365>] [--json]",
|
|
126
131
|
" cockpit ops [status [--job <id>] [--skips] [--memory] [--models] | recompile --person <email|name|id> [--dry-run]] [--dashboard-url <url>] [--json]",
|
|
127
132
|
" cockpit slack [connect [--workspace bli|blue_pearl] | disconnect --workspace bli|blue_pearl | coverage [--workspace bli|blue_pearl] [--stale-only] [--list] | read [--person <p>] [--channel <c>] [--query <text> [--word]] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--format compact|full]] [--json]",
|
|
128
133
|
" cockpit settings [personal [--chat-model <key>] [--brief-model <key>] | switches [set <key> <value> | set <key> --clear] | models [set --chat <key>] [--memory <id>] | cli-floor [<version>] | env list|set --project <p> --file <f> --content-stdin|delete --id <uuid> [--yes]] [--json]",
|
|
@@ -183,6 +188,7 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
183
188
|
" cockpit ticket [same verbs and flags as cockpit issue] (the Tickets page; projects: `cockpit project list`)",
|
|
184
189
|
" cockpit tickets ... same as cockpit ticket",
|
|
185
190
|
" cockpit work ... same as cockpit ticket (the older name)",
|
|
191
|
+
" cockpit pr ... same as cockpit pr-output (the MCP tools are pr_output_*)",
|
|
186
192
|
" cockpit calendar [same verbs and flags as cockpit cal]",
|
|
187
193
|
" cockpit messages [same verbs and flags as cockpit msg]",
|
|
188
194
|
" cockpit meeting-notes [same verbs and flags as cockpit notes]",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { runScout } from "./scout.js";
|
|
2
2
|
import { runCi } from "./ci.js";
|
|
3
3
|
import { runDelivery } from "./delivery.js";
|
|
4
|
+
import { runPrOutput } from "./pr-output.js";
|
|
5
|
+
import { runAiSpend } from "./ai-spend.js";
|
|
4
6
|
import { runOps } from "./ops.js";
|
|
5
7
|
/** Dispatch the operational report commands. */
|
|
6
8
|
export function runOpsCommand(command, io) {
|
|
@@ -8,6 +10,8 @@ export function runOpsCommand(command, io) {
|
|
|
8
10
|
case "scout": return runScout(command, io);
|
|
9
11
|
case "ci": return runCi(command, io);
|
|
10
12
|
case "delivery": return runDelivery(command, io);
|
|
13
|
+
case "pr-output": return runPrOutput(command, io);
|
|
14
|
+
case "ai-spend": return runAiSpend(command, io);
|
|
11
15
|
case "ops": return runOps(command, io);
|
|
12
16
|
}
|
|
13
17
|
}
|
package/dist/commands/local.js
CHANGED
|
@@ -124,6 +124,8 @@ async function runTowerCommand(command, io) {
|
|
|
124
124
|
case "scout":
|
|
125
125
|
case "ci":
|
|
126
126
|
case "delivery":
|
|
127
|
+
case "pr-output":
|
|
128
|
+
case "ai-spend":
|
|
127
129
|
case "ops":
|
|
128
130
|
return await (await import("./local-run-ops.js")).runOpsCommand(command, io);
|
|
129
131
|
case "slack": return await (await import("./slack.js")).runSlack(command, io);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { parseNamedArgs, optionalNonEmpty, optionalUrl } from "./local-arg-values.js";
|
|
4
|
+
const USAGE = [
|
|
5
|
+
"Usage: cockpit pr-output list [--days <1-365>] [--limit <1-200>] [--cursor <value>] [--json]",
|
|
6
|
+
" cockpit pr-output correct <owner/name#number> --hours <0-2000> [--reason <text>] [--json]",
|
|
7
|
+
" cockpit pr-output estimate [--limit <1-25>] [--days <1-365>] [--json]",
|
|
8
|
+
].join("\n");
|
|
9
|
+
function bounded(raw, min, max) {
|
|
10
|
+
if (raw === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
const n = Number(raw);
|
|
13
|
+
if (!Number.isInteger(n) || n < min || n > max)
|
|
14
|
+
throw new Error(USAGE);
|
|
15
|
+
return n;
|
|
16
|
+
}
|
|
17
|
+
export function parseTarget(raw) {
|
|
18
|
+
const match = /^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)#(\d+)$/.exec(raw ?? "");
|
|
19
|
+
if (!match || Number(match[2]) < 1)
|
|
20
|
+
throw new Error(USAGE);
|
|
21
|
+
return { repo: match[1], number: Number(match[2]) };
|
|
22
|
+
}
|
|
23
|
+
export function parsePrOutputArgs(args) {
|
|
24
|
+
const values = parseNamedArgs(args, {
|
|
25
|
+
allowedFlags: ["--days", "--limit", "--cursor", "--hours", "--reason", "--json", "--home", "--dashboard-url"],
|
|
26
|
+
valueFlags: ["--days", "--limit", "--cursor", "--hours", "--reason", "--home", "--dashboard-url"],
|
|
27
|
+
});
|
|
28
|
+
const [action, target, ...rest] = values.positionals;
|
|
29
|
+
if ((action !== "list" && action !== "correct" && action !== "estimate") || rest.length)
|
|
30
|
+
throw new Error(USAGE);
|
|
31
|
+
const common = { kind: "pr-output", action: action, json: values.booleans.has("--json"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
32
|
+
const flags = values.flags;
|
|
33
|
+
if (action === "correct") {
|
|
34
|
+
const hoursRaw = flags.get("--hours");
|
|
35
|
+
const hours = hoursRaw === undefined ? NaN : Number(hoursRaw);
|
|
36
|
+
if (!Number.isFinite(hours) || hours < 0 || hours > 2000 || flags.has("--days") || flags.has("--limit") || flags.has("--cursor"))
|
|
37
|
+
throw new Error(USAGE);
|
|
38
|
+
const reason = optionalNonEmpty(flags.get("--reason"));
|
|
39
|
+
if (reason !== undefined && reason.length > 600)
|
|
40
|
+
throw new Error(USAGE);
|
|
41
|
+
return { ...common, ...parseTarget(target), hours, ...(reason !== undefined ? { reason } : {}) };
|
|
42
|
+
}
|
|
43
|
+
if (target !== undefined || flags.has("--hours") || flags.has("--reason"))
|
|
44
|
+
throw new Error(USAGE);
|
|
45
|
+
if (action === "estimate") {
|
|
46
|
+
if (flags.has("--cursor"))
|
|
47
|
+
throw new Error(USAGE);
|
|
48
|
+
return { ...common, limit: bounded(flags.get("--limit"), 1, 25), days: bounded(flags.get("--days"), 1, 365) };
|
|
49
|
+
}
|
|
50
|
+
return { ...common, days: bounded(flags.get("--days"), 1, 365), limit: bounded(flags.get("--limit"), 1, 200), cursor: optionalNonEmpty(flags.get("--cursor")) };
|
|
51
|
+
}
|
|
52
|
+
export async function runPrOutput(command, io) {
|
|
53
|
+
const door = await openAgentDoor("pr-output", command, io);
|
|
54
|
+
let request;
|
|
55
|
+
if (command.action === "list") {
|
|
56
|
+
const query = new URLSearchParams();
|
|
57
|
+
if (command.days !== undefined)
|
|
58
|
+
query.set("days", String(command.days));
|
|
59
|
+
if (command.limit !== undefined)
|
|
60
|
+
query.set("limit", String(command.limit));
|
|
61
|
+
if (command.cursor !== undefined)
|
|
62
|
+
query.set("cursor", command.cursor);
|
|
63
|
+
request = { path: `/api/pr-output${query.size ? `?${query}` : ""}`, method: "GET" };
|
|
64
|
+
}
|
|
65
|
+
else if (command.action === "correct") {
|
|
66
|
+
request = { path: "/api/pr-output/correct", method: "POST", body: { repo: command.repo, number: command.number, hours: command.hours, ...(command.reason !== undefined ? { reason: command.reason } : {}) } };
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
request = { path: "/api/pr-output/estimate", method: "POST", body: { ...(command.limit !== undefined ? { limit: command.limit } : {}), ...(command.days !== undefined ? { days: command.days } : {}) } };
|
|
70
|
+
}
|
|
71
|
+
const answer = await askAgentDoor(door, { ...request, label: `pr-output ${command.action}`, timeoutMs: command.action === "estimate" ? 300_000 : 30_000 });
|
|
72
|
+
if (!answer.ok)
|
|
73
|
+
return failAgentDoor(door, "[pr-output cli]", answer.reason, answer.detail);
|
|
74
|
+
if (command.json || command.action !== "list")
|
|
75
|
+
return emitAgentDoor(door, answer.body);
|
|
76
|
+
for (const line of answer.body.lines)
|
|
77
|
+
writeLine(io.stdout, line);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
@@ -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.279");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -38,7 +38,7 @@ import path from "node:path";
|
|
|
38
38
|
import { RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES, REDACTION_STUB_SHA256, } from "@bli-cockpit/telemetry-core";
|
|
39
39
|
import { isWholeFileMask, sanitizeTextEvidenceForUpload } from "../adapters/raw-evidence-sanitize.js";
|
|
40
40
|
import { isSecretLikePath } from "../adapters/raw-evidence-keys.js";
|
|
41
|
-
import { IMAGE_HANDLING_MODE } from "@bli-cockpit/telemetry-core";
|
|
41
|
+
import { IMAGE_HANDLING_MODE, modeDropsImages } from "@bli-cockpit/telemetry-core";
|
|
42
42
|
import { uploadFormOfTranscript } from "../image-description-session.js";
|
|
43
43
|
export async function judgeSource(input) {
|
|
44
44
|
const { file, remotes, cursorEntry } = input;
|
|
@@ -161,7 +161,7 @@ export function fileFacts(file, maxBytes = RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES,
|
|
|
161
161
|
upload = {}) {
|
|
162
162
|
const mode = upload.mode ?? IMAGE_HANDLING_MODE;
|
|
163
163
|
const describedKeys = upload.describedKeys ?? new Set();
|
|
164
|
-
const slimAware = upload.slimAware ?? mode
|
|
164
|
+
const slimAware = upload.slimAware ?? modeDropsImages(mode);
|
|
165
165
|
const redactForUpload = (text, original) => redactTranscriptForm(file, text, original);
|
|
166
166
|
let split = null;
|
|
167
167
|
let whole = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { encodeChunkForStorage } from "./evidence-upload-encoding.js";
|
|
2
2
|
import { fetchWithDeadline, isUploadDeadlineError, uploadDeadlineMs } from "./evidence-upload-deadline.js";
|
|
3
3
|
import { noteSyncProgress } from "./sync-lock.js";
|
|
4
|
-
export async function putSignedChunk(options, signedUrl, bytes, uploadId, index, codec = "gzip") {
|
|
4
|
+
export async function putSignedChunk(options, signedUrl, bytes, uploadId, index, codec = "gzip", meter) {
|
|
5
5
|
// Identical manifest + codec framing to the server chunk route staging write (gzip unless the server listed more).
|
|
6
6
|
const encoded = encodeChunkForStorage(bytes, codec);
|
|
7
7
|
console.error("[evidence-upload] chunk encoded for storage", JSON.stringify({
|
|
@@ -23,6 +23,8 @@ export async function putSignedChunk(options, signedUrl, bytes, uploadId, index,
|
|
|
23
23
|
await response.body?.cancel();
|
|
24
24
|
if (response.ok) {
|
|
25
25
|
noteSyncProgress();
|
|
26
|
+
if (meter)
|
|
27
|
+
meter.wireBytes += encoded.length;
|
|
26
28
|
const elapsedMs = Date.now() - startedAt;
|
|
27
29
|
log("signed_chunk_uploaded", uploadId, index, attempt, status, {
|
|
28
30
|
bytes: encoded.length, elapsed_ms: elapsedMs,
|