@bli-cockpit/cli 0.2.244 → 0.2.246

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.
@@ -0,0 +1,157 @@
1
+ import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
2
+ import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
3
+ import { BODY_MAX_CHARS, READ_DEADLINE_MS, WRITE_DEADLINE_MS } from "./issue-contracts.js";
4
+ const TAG = "[milestone cli]";
5
+ export const milestoneLine = (m) => `${m.id} ${m.name} target ${m.target_date ?? "none"}${m.archived_at ? " [archived]" : ""} ${m.progress?.summary ?? ""}`.trimEnd();
6
+ function refuse(door, answer) {
7
+ const mutation = answer.body?.mutation;
8
+ return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
9
+ }
10
+ function projectQuery(command) {
11
+ return command.project ? `?project=${encodeURIComponent(command.project)}` : "";
12
+ }
13
+ /** `--description -` reads the text from stdin; anything else is the text itself. */
14
+ async function readDescription(value, io) {
15
+ if (value !== "-")
16
+ return { ok: true, text: value };
17
+ if (isInteractiveStdin(io))
18
+ return { ok: false, detail: "--description - reads the text from stdin, and nothing is piped in." };
19
+ try {
20
+ return { ok: true, text: await readPipedText(io.stdin, { maxChars: BODY_MAX_CHARS, overflowMessage: `A milestone description is limited to ${BODY_MAX_CHARS} characters.` }) };
21
+ }
22
+ catch (error) {
23
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) };
24
+ }
25
+ }
26
+ export async function runMilestone(command, io) {
27
+ const door = await openAgentDoor("milestone", command, io);
28
+ const ref = command.ref ? encodeURIComponent(command.ref) : "";
29
+ const writeIds = command.requestId ? { request_id: command.requestId } : {};
30
+ switch (command.action) {
31
+ case "list": {
32
+ const query = new URLSearchParams();
33
+ if (command.project)
34
+ query.set("project", command.project);
35
+ if (command.includeArchived)
36
+ query.set("include_archived", "true");
37
+ const answer = await askAgentDoor(door, {
38
+ path: `/api/work/milestones${query.size > 0 ? `?${query}` : ""}`, method: "GET", label: "milestone list", timeoutMs: READ_DEADLINE_MS,
39
+ });
40
+ if (!answer.ok)
41
+ return refuse(door, answer);
42
+ const body = answer.body;
43
+ const rows = body.milestones ?? [];
44
+ console.error(`${TAG} list done`, JSON.stringify({ returned: rows.length, coverage: body.coverage ?? null }));
45
+ if (door.json)
46
+ return emitAgentDoor(door, { ok: true, ...body });
47
+ if (rows.length === 0)
48
+ writeLine(door.io.stdout, "No milestones here. Create one with `cockpit milestone create --project <p> --name <n>`.");
49
+ for (const row of rows)
50
+ writeLine(door.io.stdout, milestoneLine(row));
51
+ if (body.coverage === "capped")
52
+ writeLine(door.io.stdout, "\nOnly the first 500 milestones are listed.");
53
+ return 0;
54
+ }
55
+ case "show": {
56
+ const query = new URLSearchParams();
57
+ if (command.project)
58
+ query.set("project", command.project);
59
+ if (command.limit !== undefined)
60
+ query.set("limit", String(command.limit));
61
+ if (command.cursor)
62
+ query.set("cursor", command.cursor);
63
+ const answer = await askAgentDoor(door, {
64
+ path: `/api/work/milestones/${ref}${query.size > 0 ? `?${query}` : ""}`, method: "GET", label: "milestone show", timeoutMs: READ_DEADLINE_MS,
65
+ });
66
+ if (!answer.ok)
67
+ return refuse(door, answer);
68
+ const body = answer.body;
69
+ console.error(`${TAG} show done`, JSON.stringify({ milestone_id: body.milestone.id, members: body.issues?.length ?? 0, has_more: Boolean(body.has_more) }));
70
+ if (door.json)
71
+ return emitAgentDoor(door, { ok: true, ...body });
72
+ const out = door.io.stdout;
73
+ writeLine(out, milestoneLine(body.milestone));
74
+ writeLine(out, `project ${body.project?.name ?? body.milestone.project_id} updated ${body.milestone.updated_at ?? ""}`);
75
+ if (body.milestone.description)
76
+ writeLine(out, `\n${body.milestone.description}`);
77
+ writeLine(out, `\nProgress: ${body.progress?.summary ?? "no progress summary"}`);
78
+ for (const issue of body.issues ?? [])
79
+ writeLine(out, `${(issue.identifier ?? "").padEnd(9)} ${(issue.state ?? "").padEnd(11)} ${issue.title ?? ""}`);
80
+ if (body.has_more && body.next_cursor)
81
+ writeLine(out, `More tickets: --cursor ${body.next_cursor}`);
82
+ return 0;
83
+ }
84
+ case "create":
85
+ case "update": {
86
+ const description = await readDescription(command.description, io);
87
+ if (!description.ok)
88
+ return failAgentDoor(door, TAG, "invalid_body", description.detail);
89
+ const payload = {
90
+ ...writeIds,
91
+ ...(command.action === "create" ? { project: command.project } : {}),
92
+ ...(command.action === "update" && command.expectedUpdatedAt ? { expected_updated_at: command.expectedUpdatedAt } : {}),
93
+ ...(command.name === undefined ? {} : { name: command.name }),
94
+ ...(description.text === undefined ? {} : { description: description.text }),
95
+ ...(command.targetDate === undefined ? {} : { target_date: command.targetDate }),
96
+ };
97
+ return await write(command, door, command.action === "create"
98
+ ? { path: "/api/work/milestones", method: "POST", body: payload }
99
+ : { path: `/api/work/milestones/${ref}${projectQuery(command)}`, method: "PATCH", body: payload });
100
+ }
101
+ case "archive":
102
+ case "restore":
103
+ return await write(command, door, {
104
+ path: `/api/work/milestones/${ref}${projectQuery(command)}`,
105
+ method: "PATCH",
106
+ body: { ...writeIds, ...(command.expectedUpdatedAt ? { expected_updated_at: command.expectedUpdatedAt } : {}), archived: command.action === "archive" },
107
+ });
108
+ case "add":
109
+ case "remove":
110
+ return await membership(command, door, ref, writeIds);
111
+ }
112
+ }
113
+ async function write(command, door, req) {
114
+ const answer = await askAgentDoor(door, { ...req, label: `milestone ${command.action}`, timeoutMs: WRITE_DEADLINE_MS });
115
+ if (!answer.ok)
116
+ return refuse(door, answer);
117
+ const body = answer.body;
118
+ console.error(`${TAG} ${command.action} committed`, JSON.stringify({ milestone_id: body.milestone.id, request_id: body.mutation?.request_id ?? null, delivery: body.mutation?.delivery?.status ?? null }));
119
+ if (door.json)
120
+ return emitAgentDoor(door, { ok: true, ...body });
121
+ const label = { create: "Created", update: "Updated", archive: "Archived", restore: "Restored" }[command.action] ?? "Saved";
122
+ writeLine(door.io.stdout, `${label}: ${milestoneLine(body.milestone)}`);
123
+ writeLine(door.io.stdout, `Request ID ${body.mutation?.request_id ?? "?"}, delivery ${body.mutation?.delivery?.status ?? "tower_only"} (milestones never reach Linear).`);
124
+ return 0;
125
+ }
126
+ async function membership(command, door, ref, writeIds) {
127
+ const answer = await askAgentDoor(door, {
128
+ path: `/api/work/milestones/${ref}/issues${projectQuery(command)}`,
129
+ method: "POST",
130
+ label: `milestone ${command.action}`,
131
+ timeoutMs: WRITE_DEADLINE_MS,
132
+ body: { ...writeIds, [command.action]: command.tickets },
133
+ });
134
+ // 207 means some items were refused: still a readable answer with every receipt.
135
+ const body = (("body" in answer ? answer.body : undefined) ?? {});
136
+ if (!answer.ok && !Array.isArray(body.items))
137
+ return refuse(door, answer);
138
+ const items = body.items ?? [];
139
+ console.error(`${TAG} ${command.action} done`, JSON.stringify({
140
+ request_id: body.request_id ?? null, asked: items.length, committed: body.committed ?? 0, refused: body.refused ?? 0, skipped: body.skipped ?? 0, unknown: body.unknown ?? 0,
141
+ }));
142
+ const failed = (body.refused ?? 0) + (body.unknown ?? 0) > 0;
143
+ if (door.json) {
144
+ emitAgentDoor(door, { ok: !failed, ...body });
145
+ return failed ? 1 : 0;
146
+ }
147
+ for (const item of items) {
148
+ const linear = item.delivery?.status ? ` Linear copy: ${item.delivery.status}${item.delivery.reason ? ` (${item.delivery.reason})` : ""}` : "";
149
+ writeLine(door.io.stdout, `${(item.identifier ?? item.ref).padEnd(9)} ${item.action} ${item.outcome}${item.reason ? ` ${item.reason}` : ""}${linear}`);
150
+ if (item.message && item.outcome !== "committed")
151
+ writeLine(door.io.stdout, ` ${item.message}`);
152
+ }
153
+ writeLine(door.io.stdout, `\n${body.committed ?? 0} committed, ${body.refused ?? 0} refused, ${body.skipped ?? 0} skipped, ${body.unknown ?? 0} unknown. Request ID ${body.request_id ?? "?"} (retry with it to replay).`);
154
+ if (body.progress?.summary)
155
+ writeLine(door.io.stdout, `Progress: ${body.progress.summary}`);
156
+ return failed ? 1 : 0;
157
+ }
@@ -4,20 +4,94 @@
4
4
  * A description never travels on argv by default: `--file <path>` reads it from a local file (safest on Windows);
5
5
  * `--description <text>` is the short form and `--description none` clears it. Write-safety flags
6
6
  * (`--request-id`, `--expected-updated-at`) belong to the four write verbs.
7
+ * BLI-22360: create/update also take the planning flags (`--lead`, `--member`, `--add-member`, `--remove-member`,
8
+ * `--status`, `--priority`, `--start`, `--target`, their `--*-resolution`, `--content-file` / `--content`); `none` clears.
7
9
  */
8
10
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
9
11
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
12
  const ACTIONS = ["list", "show", "create", "update", "archive", "restore"];
11
13
  const WRITES = ["create", "update", "archive", "restore"];
14
+ /** BLI-22360: the planning fields of `project create|update`. `none` clears a value (update). */
15
+ const PLANNING_FLAGS = [
16
+ "--lead", "--member", "--add-member", "--remove-member", "--status", "--priority",
17
+ "--start", "--start-resolution", "--target", "--target-resolution", "--content-file", "--content",
18
+ ];
19
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
20
+ const PRIORITIES = ["none", "urgent", "high", "medium", "low", "0", "1", "2", "3", "4"];
21
+ const STATUSES = ["backlog", "planned", "started", "in-progress", "in_progress", "paused", "completed", "done", "canceled", "none"];
22
+ const RESOLUTIONS = ["day", "month", "quarter", "halfYear", "year", "none"];
23
+ /** `--member a@x --member b@x` or `--member a@x,b@x`; `none` alone clears (sent as an empty set). */
24
+ function people(values, flag) {
25
+ const raw = values.flagValues.get(flag) ?? (values.flags.has(flag) ? [values.flags.get(flag)] : undefined);
26
+ if (raw === undefined)
27
+ return undefined;
28
+ const list = raw.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
29
+ if (list.length === 0)
30
+ throw new Error(`${flag} needs an email, a user id or me.`);
31
+ return list;
32
+ }
33
+ function day(flag, value) {
34
+ if (value === undefined)
35
+ return undefined;
36
+ if (value.trim().toLowerCase() === "none")
37
+ return null;
38
+ if (!DAY.test(value) || new Date(`${value}T12:00:00Z`).toISOString().slice(0, 10) !== value) {
39
+ throw new Error(`${flag} must be a calendar date as YYYY-MM-DD, or none.`);
40
+ }
41
+ return value;
42
+ }
43
+ function oneOf(flag, value, allowed) {
44
+ if (value === undefined)
45
+ return undefined;
46
+ if (!allowed.includes(value.trim()))
47
+ throw new Error(`${flag} must be one of: ${allowed.join(", ")}.`);
48
+ return value.trim();
49
+ }
50
+ function readPlanning(values, action) {
51
+ const given = PLANNING_FLAGS.filter((flag) => values.flags.has(flag));
52
+ if (given.length === 0)
53
+ return {};
54
+ if (action !== "create" && action !== "update")
55
+ throw new Error(`${given[0]} belongs to project create and update.`);
56
+ if (values.flags.has("--content") && values.flags.has("--content-file"))
57
+ throw new Error("Give --content or --content-file, not both.");
58
+ const members = people(values, "--member");
59
+ const addMembers = people(values, "--add-member");
60
+ const removeMembers = people(values, "--remove-member");
61
+ if (members && (addMembers || removeMembers))
62
+ throw new Error("Give --member (the whole set) or --add-member / --remove-member, not both.");
63
+ if (action === "create" && (addMembers || removeMembers))
64
+ throw new Error("project create takes --member; --add-member and --remove-member belong to update.");
65
+ const lead = values.flags.get("--lead");
66
+ const content = values.flags.get("--content");
67
+ const status = oneOf("--status", values.flags.get("--status"), STATUSES);
68
+ const priority = oneOf("--priority", values.flags.get("--priority"), PRIORITIES);
69
+ return {
70
+ ...(lead === undefined ? {} : { lead: lead.trim().toLowerCase() === "none" ? null : lead.trim() }),
71
+ ...(members === undefined ? {} : { members }),
72
+ ...(addMembers === undefined ? {} : { addMembers }),
73
+ ...(removeMembers === undefined ? {} : { removeMembers }),
74
+ ...(status === undefined ? {} : { status: status === "none" ? null : status }),
75
+ ...(priority === undefined ? {} : { priority }),
76
+ ...(values.flags.has("--start") ? { startDate: day("--start", values.flags.get("--start")) } : {}),
77
+ ...(values.flags.has("--target") ? { targetDate: day("--target", values.flags.get("--target")) } : {}),
78
+ ...(values.flags.has("--start-resolution") ? { startDateResolution: oneOf("--start-resolution", values.flags.get("--start-resolution"), RESOLUTIONS) } : {}),
79
+ ...(values.flags.has("--target-resolution") ? { targetDateResolution: oneOf("--target-resolution", values.flags.get("--target-resolution"), RESOLUTIONS) } : {}),
80
+ ...(content === undefined ? {} : { content: content.trim().toLowerCase() === "none" ? null : content }),
81
+ ...(values.flags.has("--content-file") ? { contentFilePath: optionalNonEmpty(values.flags.get("--content-file")) } : {}),
82
+ };
83
+ }
12
84
  export function parseProjectArgs(args) {
13
85
  const values = parseNamedArgs(args, {
14
86
  allowedFlags: [
15
87
  "--home", "--dashboard-url", "--archived", "--cursor", "--limit", "--json",
16
88
  "--name", "--description", "--file", "--color", "--request-id", "--expected-updated-at",
89
+ ...PLANNING_FLAGS,
17
90
  ],
18
91
  valueFlags: [
19
92
  "--home", "--dashboard-url", "--cursor", "--limit",
20
93
  "--name", "--description", "--file", "--color", "--request-id", "--expected-updated-at",
94
+ ...PLANNING_FLAGS,
21
95
  ],
22
96
  });
23
97
  const first = values.positionals[0];
@@ -59,8 +133,9 @@ export function parseProjectArgs(args) {
59
133
  const rawDescription = values.flags.get("--description");
60
134
  const description = rawDescription === undefined ? undefined : rawDescription.trim().toLowerCase() === "none" ? null : rawDescription;
61
135
  const filePath = optionalNonEmpty(values.flags.get("--file"));
62
- if (action === "update" && name === undefined && description === undefined && filePath === undefined) {
63
- throw new Error("project update needs --name, --description (or none) or --file.");
136
+ const planning = readPlanning(values, action);
137
+ if (action === "update" && name === undefined && description === undefined && filePath === undefined && Object.keys(planning).length === 0) {
138
+ throw new Error("project update needs --name, --description (or none), --file, or a planning flag (--lead, --member, --status, --priority, --start, --target, --content-file).");
64
139
  }
65
140
  return {
66
141
  kind: "project",
@@ -77,6 +152,7 @@ export function parseProjectArgs(args) {
77
152
  ...(optionalNonEmpty(values.flags.get("--color")) ? { color: optionalNonEmpty(values.flags.get("--color")) } : {}),
78
153
  ...(requestId ? { requestId } : {}),
79
154
  ...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
155
+ ...(Object.keys(planning).length > 0 ? { planning } : {}),
80
156
  json: values.booleans.has("--json"),
81
157
  };
82
158
  }
@@ -12,6 +12,22 @@ import { pageFieldsOf, pageFooter } from "./issue-list-page.js";
12
12
  import { readBody } from "./issue-contracts.js";
13
13
  const TAG = "[project cli]";
14
14
  const READ_DEADLINE_MS = 30_000;
15
+ const PRIORITY_WORDS = ["none", "urgent", "high", "medium", "low"];
16
+ const person = (p) => p.name && p.email ? `${p.name} <${p.email}>` : (p.email ?? p.name ?? p.user_id);
17
+ /** "2026-06-01 (month)" for a period, the bare day for an exact date. */
18
+ const dated = (day, resolution) => day ? `${day}${resolution ? ` (${resolution})` : ""}` : "none";
19
+ /** The planning block `project show` prints (BLI-22360). */
20
+ export function planningLines(body) {
21
+ const project = body.project;
22
+ if (!project)
23
+ return [];
24
+ return [
25
+ `status ${project.status ?? "none"} priority ${PRIORITY_WORDS[project.priority ?? 0] ?? project.priority} start ${dated(project.start_date, project.start_date_resolution)} target ${dated(project.target_date, project.target_date_resolution)}`,
26
+ `lead ${body.lead ? person(body.lead) : "none"}`,
27
+ `members ${(body.members ?? []).length === 0 ? "none" : (body.members ?? []).map(person).join(", ")}`,
28
+ ...(body.milestones ?? []).map((m) => `milestone ${m.name}${m.archived_at ? " (archived)" : ""} target ${m.target_date ?? "none"} ${m.progress?.summary ?? ""} ${m.id}`),
29
+ ];
30
+ }
15
31
  export async function runProject(command, io) {
16
32
  const door = await openAgentDoor("project", command, io);
17
33
  if (command.action !== "list")
@@ -65,6 +81,10 @@ async function runProjectVerb(command, door) {
65
81
  writeLine(out, `id ${project?.id ?? ""} updated ${project?.updated_at ?? ""}`);
66
82
  if (project?.description)
67
83
  writeLine(out, project.description);
84
+ for (const line of planningLines(body))
85
+ writeLine(out, line);
86
+ if (project?.content)
87
+ writeLine(out, `\n${project.content}\n`);
68
88
  for (const issue of body.issues ?? [])
69
89
  writeLine(out, `${issue.identifier ?? ""} ${issue.state ?? ""} ${issue.title ?? ""}`);
70
90
  const footer = pageFooter(body, "tickets");
@@ -92,6 +112,38 @@ async function runProjectVerb(command, door) {
92
112
  else if (command.description !== undefined) {
93
113
  payload["description"] = command.description;
94
114
  }
115
+ const planning = command.planning;
116
+ if (planning) {
117
+ if (planning.lead !== undefined)
118
+ payload["lead"] = planning.lead;
119
+ if (planning.members !== undefined)
120
+ payload["members"] = planning.members;
121
+ if (planning.addMembers !== undefined)
122
+ payload["add_members"] = planning.addMembers;
123
+ if (planning.removeMembers !== undefined)
124
+ payload["remove_members"] = planning.removeMembers;
125
+ if (planning.status !== undefined)
126
+ payload["status"] = planning.status;
127
+ if (planning.priority !== undefined)
128
+ payload["priority"] = planning.priority;
129
+ if (planning.startDate !== undefined)
130
+ payload["start_date"] = planning.startDate;
131
+ if (planning.targetDate !== undefined)
132
+ payload["target_date"] = planning.targetDate;
133
+ if (planning.startDateResolution !== undefined)
134
+ payload["start_date_resolution"] = planning.startDateResolution;
135
+ if (planning.targetDateResolution !== undefined)
136
+ payload["target_date_resolution"] = planning.targetDateResolution;
137
+ if (planning.contentFilePath) {
138
+ const read = await readBody({ filePath: planning.contentFilePath }, door.io);
139
+ if (!read.ok)
140
+ return failAgentDoor(door, TAG, read.reason, read.detail);
141
+ payload["content"] = read.text;
142
+ }
143
+ else if (planning.content !== undefined) {
144
+ payload["content"] = planning.content;
145
+ }
146
+ }
95
147
  }
96
148
  const path = command.action === "create" ? base : command.action === "update" ? ref : `${ref}/${command.action}`;
97
149
  const answer = await askAgentDoor(door, {
@@ -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.244");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.246");
25
25
  return 0;
26
26
  }
27
27
 
@@ -15,7 +15,8 @@
15
15
  *
16
16
  * Three reasons close the gate, most urgent first:
17
17
  *
18
- * - `low_disk`: the volume's free space is under max(5 GiB, 5% of the volume).
18
+ * - `low_disk`: the volume's free space is under max(5 GiB, 5% of the volume),
19
+ * for a file bigger than 1% of the free space left (smaller files are admitted).
19
20
  * - `state_over_ceiling`: everything the collector writes under its state dir
20
21
  * (staging, spool, logs, cursors, state; never the study folders
21
22
  * `state-dir-audit.ts` recognises) would pass the hard ceiling, 512 MiB by
@@ -43,6 +44,12 @@ export const STATE_CEILING_ENV = "COCKPIT_STATE_CEILING_BYTES";
43
44
  export const MIN_FREE_ENV = "COCKPIT_MIN_FREE_BYTES";
44
45
  export const LOW_DISK_FLOOR_BYTES = 5 * 1024 * 1024 * 1024;
45
46
  export const LOW_DISK_FLOOR_FRACTION = 0.05;
47
+ /**
48
+ * A file at most this fraction of the free space left is never refused for low
49
+ * disk: a 7 MB upload does not threaten a volume with gigabytes free, even one
50
+ * under the floor. Only a copy that would eat a real share of what is left waits.
51
+ */
52
+ export const LOW_DISK_SMALL_FILE_FRACTION = 0.01;
46
53
  /**
47
54
  * Each pack's `manifest.json` is staged beside the payload and its size grows
48
55
  * with the gap ledger, so it is not known when a file is admitted. A tenth of
@@ -174,9 +181,22 @@ export function admitToStaging(allowance, byteLength, options = {}) {
174
181
  const spent = allowance.admitted_bytes;
175
182
  const stagedNow = snapshot.staging_bytes + spent;
176
183
  let reason = null;
177
- if (snapshot.free_bytes !== null &&
184
+ const freeNow = snapshot.free_bytes === null ? null : snapshot.free_bytes - spent;
185
+ const belowFloor = freeNow !== null &&
178
186
  snapshot.floor_bytes !== null &&
179
- snapshot.free_bytes - spent - byteLength < snapshot.floor_bytes) {
187
+ freeNow - byteLength < snapshot.floor_bytes;
188
+ const smallForFree = freeNow !== null && freeNow > 0 && byteLength <= freeNow * LOW_DISK_SMALL_FILE_FRACTION;
189
+ if (belowFloor && smallForFree) {
190
+ console.error("[staging-gate] low-disk floor waived, file is under 1% of free space", JSON.stringify({
191
+ event: STAGING_DEFERRED,
192
+ reason: "low_disk_waived_small_file",
193
+ kind: options.kind ?? null,
194
+ file_bytes: byteLength,
195
+ free_bytes: freeNow,
196
+ floor_bytes: snapshot.floor_bytes,
197
+ }));
198
+ }
199
+ if (belowFloor && !smallForFree) {
180
200
  reason = "low_disk";
181
201
  }
182
202
  else if (snapshot.collector_bytes + spent + byteLength > snapshot.ceiling_bytes) {
@@ -202,6 +222,7 @@ export function admitToStaging(allowance, byteLength, options = {}) {
202
222
  reason,
203
223
  kind: options.kind ?? null,
204
224
  file_bytes: byteLength,
225
+ needed_bytes: byteLength,
205
226
  staging_bytes: snapshot.staging_bytes,
206
227
  collector_bytes: snapshot.collector_bytes,
207
228
  free_bytes: snapshot.free_bytes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.244",
3
+ "version": "0.2.246",
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.72",
31
- "@bli-cockpit/mcp": "0.1.149",
31
+ "@bli-cockpit/mcp": "0.1.151",
32
32
  "@bli-cockpit/telemetry-core": "0.1.79"
33
33
  },
34
34
  "repository": {