@bli-cockpit/cli 0.2.51 → 0.2.53
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/clean.js +244 -0
- package/dist/commands/doctor.js +73 -0
- package/dist/commands/jarvis.js +101 -9
- package/dist/commands/local-args-collector.js +30 -0
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-help.js +26 -0
- package/dist/commands/local.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-attribution.js +55 -0
- package/dist/commands/session-sync-failures.js +140 -0
- package/dist/commands/session-sync-health.js +102 -0
- package/dist/commands/session-sync-plan.js +81 -0
- package/dist/commands/session-sync-record.js +279 -0
- package/dist/commands/session-sync-scan.js +209 -0
- package/dist/commands/session-sync-types.js +12 -0
- package/dist/commands/session-sync-upload.js +215 -0
- package/dist/commands/session-sync.js +44 -987
- package/dist/commands/sync-followups.js +140 -2
- package/dist/commands/sync.js +4 -1
- package/dist/cursors/raw-evidence-reconcile-cursor.js +132 -0
- package/dist/disk-prune.js +246 -0
- package/dist/disk-retention.js +157 -0
- package/dist/disk-usage.js +392 -0
- package/dist/evidence-reconcile-client.js +224 -0
- package/dist/log-rotation.js +106 -2
- package/dist/tower-stream.js +5 -1
- package/package.json +3 -3
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asks `POST /api/ambient/evidence/reconcile` about every hash this laptop
|
|
3
|
+
* can no longer classify on its own, then writes the answer into
|
|
4
|
+
* `cursors/raw-evidence-reconcile.json` so `disk-usage.ts` reads it back on
|
|
5
|
+
* every run after this one.
|
|
6
|
+
*
|
|
7
|
+
* BLI-3619. Three properties, same as the prune it feeds:
|
|
8
|
+
*
|
|
9
|
+
* - **Never deletes.** This module only asks and records; `disk-prune.ts`
|
|
10
|
+
* decides what goes, unchanged.
|
|
11
|
+
* - **A server failure never blocks the prune.** A batch that cannot be asked
|
|
12
|
+
* leaves its hashes exactly as unresolved as they were — still `unknown`
|
|
13
|
+
* locally — and the run says `reconcile_unavailable: <reason>` rather than
|
|
14
|
+
* throwing. The ordinary retention prune runs whether this succeeded,
|
|
15
|
+
* partly succeeded, or could not run at all.
|
|
16
|
+
* - **Bounded.** `maxBatches` lets a caller (the daily sync fold) ask only one
|
|
17
|
+
* batch of up to 500 hashes per tick rather than draining years of
|
|
18
|
+
* accumulated `unknown` state in one request storm; `cockpit clean
|
|
19
|
+
* --reconcile` itself asks every batch there is.
|
|
20
|
+
*/
|
|
21
|
+
import { EVIDENCE_RECONCILE_MAX_BATCH, EVIDENCE_RECONCILE_SCHEMA_VERSION, EvidenceReconcileResponseSchema, } from "@bli-cockpit/telemetry-core";
|
|
22
|
+
import { describeError } from "./health-detail.js";
|
|
23
|
+
import { getCollectorRuntimePaths, readLocalCollectorSessionFile } from "./local-state.js";
|
|
24
|
+
import { readReconcileCursor, recordReconcileResult, writeReconcileCursor, } from "./cursors/raw-evidence-reconcile-cursor.js";
|
|
25
|
+
import { readStagingInventory } from "./disk-usage.js";
|
|
26
|
+
const RECONCILE_BATCH_TIMEOUT_MS = 15_000;
|
|
27
|
+
/** The prune the sync tick and `cockpit clean --reconcile` both call. Never throws. */
|
|
28
|
+
export async function runEvidenceReconcile(options) {
|
|
29
|
+
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
30
|
+
const now = options.now ?? new Date();
|
|
31
|
+
const doFetch = options.fetch ?? fetch;
|
|
32
|
+
try {
|
|
33
|
+
const inventory = options.inventory ?? (await readStagingInventory(paths, now));
|
|
34
|
+
const hashes = uniqueUnknownHashes(inventory);
|
|
35
|
+
if (hashes.length === 0) {
|
|
36
|
+
// No stderr line here on purpose: the sync tick calls this every 15
|
|
37
|
+
// minutes and the steady state is "nothing unknown" — logging that
|
|
38
|
+
// every tick forever is exactly the receipt-on-95-of-96-ticks spam the
|
|
39
|
+
// daily prune throttle exists to avoid. `cockpit clean --reconcile`
|
|
40
|
+
// still tells a person this on stdout (`reconcileLines` in clean.ts).
|
|
41
|
+
return emptyResult("nothing_unknown");
|
|
42
|
+
}
|
|
43
|
+
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
44
|
+
if (!session ||
|
|
45
|
+
session.session_state !== "valid" ||
|
|
46
|
+
typeof session.device_token !== "string" ||
|
|
47
|
+
!session.device_token) {
|
|
48
|
+
const result = {
|
|
49
|
+
...emptyResult("no_device_session"),
|
|
50
|
+
total_batches: chunk(hashes, EVIDENCE_RECONCILE_MAX_BATCH).length,
|
|
51
|
+
};
|
|
52
|
+
reportReconcile(result);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
const batches = chunk(hashes, EVIDENCE_RECONCILE_MAX_BATCH);
|
|
56
|
+
const bounded = options.maxBatches != null ? batches.slice(0, options.maxBatches) : batches;
|
|
57
|
+
const cursor = await readReconcileCursor(paths);
|
|
58
|
+
let committed = 0;
|
|
59
|
+
let notCommitted = 0;
|
|
60
|
+
let unknownToServer = 0;
|
|
61
|
+
let failedBatches = 0;
|
|
62
|
+
let firstFailureReason = null;
|
|
63
|
+
let askedThisRun = 0;
|
|
64
|
+
for (const batch of bounded) {
|
|
65
|
+
const outcome = await sendReconcileBatch({
|
|
66
|
+
dashboardUrl: options.dashboardUrl,
|
|
67
|
+
deviceToken: session.device_token,
|
|
68
|
+
hashes: batch,
|
|
69
|
+
fetchImpl: doFetch,
|
|
70
|
+
});
|
|
71
|
+
if (!outcome.ok) {
|
|
72
|
+
failedBatches += 1;
|
|
73
|
+
firstFailureReason ??= outcome.reason;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
askedThisRun += batch.length;
|
|
77
|
+
for (const item of outcome.results) {
|
|
78
|
+
recordReconcileResult(cursor, item.content_hash_sha256, {
|
|
79
|
+
verdict: item.verdict,
|
|
80
|
+
checked_at: now.toISOString(),
|
|
81
|
+
server_committed_at: item.verdict === "committed" ? item.committed_at : null,
|
|
82
|
+
});
|
|
83
|
+
if (item.verdict === "committed")
|
|
84
|
+
committed += 1;
|
|
85
|
+
else if (item.verdict === "not_committed")
|
|
86
|
+
notCommitted += 1;
|
|
87
|
+
else
|
|
88
|
+
unknownToServer += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (askedThisRun > 0) {
|
|
92
|
+
cursor.updated_at = now.toISOString();
|
|
93
|
+
await writeReconcileCursor(paths, cursor).catch((error) => {
|
|
94
|
+
// The prune's own forgetStagedObjects follows the same rule: the
|
|
95
|
+
// answer is not lost (the server has it), but a reader of THIS
|
|
96
|
+
// machine will ask again next run rather than silently drift.
|
|
97
|
+
console.error("[collector reconcile] reconcile cursor not written after answers landed", JSON.stringify({
|
|
98
|
+
reason: "reconcile_cursor_write_failed",
|
|
99
|
+
answered_count: askedThisRun,
|
|
100
|
+
...describeError(error),
|
|
101
|
+
}));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const result = {
|
|
105
|
+
status: askedThisRun > 0 ? "ok" : "fail",
|
|
106
|
+
reason: reconcileReason({
|
|
107
|
+
askedThisRun,
|
|
108
|
+
failedBatches,
|
|
109
|
+
firstFailureReason,
|
|
110
|
+
boundedShortOfAvailable: bounded.length < batches.length,
|
|
111
|
+
}),
|
|
112
|
+
asked: askedThisRun,
|
|
113
|
+
committed,
|
|
114
|
+
not_committed: notCommitted,
|
|
115
|
+
unknown_to_server: unknownToServer,
|
|
116
|
+
batches: bounded.length,
|
|
117
|
+
failed_batches: failedBatches,
|
|
118
|
+
total_batches: batches.length,
|
|
119
|
+
};
|
|
120
|
+
reportReconcile(result);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const result = {
|
|
125
|
+
...emptyResult(`reconcile_unavailable: ${describeError(error).error_name}`),
|
|
126
|
+
status: "fail",
|
|
127
|
+
};
|
|
128
|
+
console.error("[collector reconcile] the reconcile pass threw; nothing was answered or deleted", JSON.stringify({ reason: "reconcile_threw", ...describeError(error) }));
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function reconcileReason(input) {
|
|
133
|
+
if (input.askedThisRun === 0) {
|
|
134
|
+
return `reconcile_unavailable: ${input.firstFailureReason ?? "unknown"}`;
|
|
135
|
+
}
|
|
136
|
+
if (input.failedBatches > 0)
|
|
137
|
+
return "reconciled_partial";
|
|
138
|
+
if (input.boundedShortOfAvailable)
|
|
139
|
+
return "reconciled_bounded";
|
|
140
|
+
return "reconciled";
|
|
141
|
+
}
|
|
142
|
+
/** Every `unknown`-state object's hash, deduplicated — the question is per hash. */
|
|
143
|
+
function uniqueUnknownHashes(inventory) {
|
|
144
|
+
const hashes = new Set();
|
|
145
|
+
for (const pack of inventory.packs) {
|
|
146
|
+
for (const object of pack.objects) {
|
|
147
|
+
if (object.state === "unknown" && object.content_hash) {
|
|
148
|
+
hashes.add(object.content_hash);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [...hashes];
|
|
153
|
+
}
|
|
154
|
+
function chunk(items, size) {
|
|
155
|
+
const batches = [];
|
|
156
|
+
for (let index = 0; index < items.length; index += size) {
|
|
157
|
+
batches.push(items.slice(index, index + size));
|
|
158
|
+
}
|
|
159
|
+
return batches;
|
|
160
|
+
}
|
|
161
|
+
async function sendReconcileBatch(options) {
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
const timeout = setTimeout(() => controller.abort(), RECONCILE_BATCH_TIMEOUT_MS);
|
|
164
|
+
try {
|
|
165
|
+
const response = await options.fetchImpl(`${options.dashboardUrl}/api/ambient/evidence/reconcile`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
Authorization: `Bearer ${options.deviceToken}`,
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
schema_version: EVIDENCE_RECONCILE_SCHEMA_VERSION,
|
|
173
|
+
items: options.hashes.map((hash) => ({ content_hash_sha256: hash })),
|
|
174
|
+
}),
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
});
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
return { ok: false, reason: `http_${response.status}`, results: [] };
|
|
179
|
+
}
|
|
180
|
+
const body = await response.json().catch(() => null);
|
|
181
|
+
const parsed = EvidenceReconcileResponseSchema.safeParse(body);
|
|
182
|
+
if (!parsed.success) {
|
|
183
|
+
return { ok: false, reason: "invalid_response", results: [] };
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, reason: "ok", results: parsed.data.results };
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return { ok: false, reason: describeError(error).error_name, results: [] };
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function emptyResult(reason) {
|
|
195
|
+
return {
|
|
196
|
+
status: "skipped",
|
|
197
|
+
reason,
|
|
198
|
+
asked: 0,
|
|
199
|
+
committed: 0,
|
|
200
|
+
not_committed: 0,
|
|
201
|
+
unknown_to_server: 0,
|
|
202
|
+
batches: 0,
|
|
203
|
+
failed_batches: 0,
|
|
204
|
+
total_batches: 0,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The receipt, on stderr, exactly the shape the ticket names:
|
|
209
|
+
* `{asked, committed, not_committed, unknown_to_server, batches, failed_batches}`,
|
|
210
|
+
* plus `total_batches` so a bounded (daily-fold) run says the bound it hit.
|
|
211
|
+
* Metadata only — hashes and pack ids never travel in this line.
|
|
212
|
+
*/
|
|
213
|
+
function reportReconcile(result) {
|
|
214
|
+
console.error("[collector reconcile]", JSON.stringify({
|
|
215
|
+
reason: result.reason,
|
|
216
|
+
asked: result.asked,
|
|
217
|
+
committed: result.committed,
|
|
218
|
+
not_committed: result.not_committed,
|
|
219
|
+
unknown_to_server: result.unknown_to_server,
|
|
220
|
+
batches: result.batches,
|
|
221
|
+
total_batches: result.total_batches,
|
|
222
|
+
failed_batches: result.failed_batches,
|
|
223
|
+
}));
|
|
224
|
+
}
|
package/dist/log-rotation.js
CHANGED
|
@@ -50,7 +50,8 @@ export async function rotateCollectorLogs(paths, options = {}) {
|
|
|
50
50
|
const maxBytes = options.maxBytes ?? DEFAULT_LOG_ROTATION_MAX_BYTES;
|
|
51
51
|
const keep = options.keep ?? DEFAULT_LOG_ROTATION_KEEP;
|
|
52
52
|
const names = options.names ?? ROTATED_LOG_NAMES;
|
|
53
|
-
const result = { rotated: [], failures: [] };
|
|
53
|
+
const result = { rotated: [], failures: [], swept: [] };
|
|
54
|
+
result.swept = await sweepOrphanedLogArchives(paths, { keep, names });
|
|
54
55
|
for (const name of names) {
|
|
55
56
|
const filePath = path.join(paths.state_dir, name);
|
|
56
57
|
const size = await fs
|
|
@@ -107,18 +108,114 @@ async function rotateOne(filePath, maxBytes, keep) {
|
|
|
107
108
|
await handle.close();
|
|
108
109
|
}
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Archives of the tick's own logs that the keep window does not cover.
|
|
113
|
+
*
|
|
114
|
+
* BLI-3553 promised `maxBytes x (keep + 1)` per stream as the worst case on
|
|
115
|
+
* disk. Two shapes escape it, and both were sitting on the reference Mac on
|
|
116
|
+
* 2026-09-05 (BLI-3619):
|
|
117
|
+
*
|
|
118
|
+
* - `sync.log.4` and beyond — `rotateOne` unlinks exactly `.{keep}` and renames
|
|
119
|
+
* downward, so an archive that already exists above the window is never
|
|
120
|
+
* touched again. Lowering `keep`, or a machine that once ran a build with a
|
|
121
|
+
* larger one, strands them forever.
|
|
122
|
+
* - `sync.log.tail-20260825` / `sync.err.log.tail-20260825`, 7 MB — a hand
|
|
123
|
+
* rotation from before this module existed. They are tails of the same two
|
|
124
|
+
* streams, superseded by `.1`…`.3`, and nothing in the codebase writes or
|
|
125
|
+
* reads them.
|
|
126
|
+
*
|
|
127
|
+
* Each removal is named with its own byte count, because "the cap works" and
|
|
128
|
+
* "the cap works and also quietly deleted a file you made" are different
|
|
129
|
+
* sentences and an operator is owed the second one.
|
|
130
|
+
*/
|
|
131
|
+
export async function sweepOrphanedLogArchives(paths, options) {
|
|
132
|
+
const entries = await fs
|
|
133
|
+
.readdir(paths.state_dir, { withFileTypes: true })
|
|
134
|
+
.catch(() => []);
|
|
135
|
+
const swept = [];
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (!entry.isFile())
|
|
138
|
+
continue;
|
|
139
|
+
const reason = orphanArchiveReason(entry.name, options);
|
|
140
|
+
if (!reason)
|
|
141
|
+
continue;
|
|
142
|
+
const file = path.join(paths.state_dir, entry.name);
|
|
143
|
+
const size = await fs
|
|
144
|
+
.stat(file)
|
|
145
|
+
.then((info) => info.size)
|
|
146
|
+
.catch(() => 0);
|
|
147
|
+
const removed = await fs.rm(file, { force: true }).then(() => true, () => false);
|
|
148
|
+
if (!removed)
|
|
149
|
+
continue;
|
|
150
|
+
swept.push({ name: entry.name, byte_size: size, reason });
|
|
151
|
+
}
|
|
152
|
+
return swept;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Which stream this file belongs to and why it is outside the keep window, or
|
|
156
|
+
* null when the rotation already accounts for it. Exact suffix matching only —
|
|
157
|
+
* `sync.log` never matches `sync.err.log`, and neither ever matches a file the
|
|
158
|
+
* collector did not write.
|
|
159
|
+
*/
|
|
160
|
+
function orphanArchiveReason(name, options) {
|
|
161
|
+
for (const stream of options.names) {
|
|
162
|
+
if (!name.startsWith(`${stream}.`))
|
|
163
|
+
continue;
|
|
164
|
+
const suffix = name.slice(stream.length + 1);
|
|
165
|
+
if (suffix.startsWith("tail-"))
|
|
166
|
+
return "manual_tail_archive";
|
|
167
|
+
if (!/^\d+$/u.test(suffix))
|
|
168
|
+
continue;
|
|
169
|
+
return Number(suffix) > options.keep ? "archive_beyond_keep" : null;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
/** Live logs plus every archive beside them, for the disk row and `clean`. */
|
|
174
|
+
export async function rotatedLogFootprint(paths, names = ROTATED_LOG_NAMES) {
|
|
175
|
+
const entries = await fs
|
|
176
|
+
.readdir(paths.state_dir, { withFileTypes: true })
|
|
177
|
+
.catch(() => []);
|
|
178
|
+
const footprint = {
|
|
179
|
+
live_bytes: 0,
|
|
180
|
+
archive_bytes: 0,
|
|
181
|
+
archive_count: 0,
|
|
182
|
+
total_bytes: 0,
|
|
183
|
+
};
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (!entry.isFile())
|
|
186
|
+
continue;
|
|
187
|
+
const live = names.includes(entry.name);
|
|
188
|
+
const archive = names.some((stream) => entry.name.startsWith(`${stream}.`));
|
|
189
|
+
if (!live && !archive)
|
|
190
|
+
continue;
|
|
191
|
+
const size = await fs
|
|
192
|
+
.stat(path.join(paths.state_dir, entry.name))
|
|
193
|
+
.then((info) => info.size)
|
|
194
|
+
.catch(() => 0);
|
|
195
|
+
if (live) {
|
|
196
|
+
footprint.live_bytes += size;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
footprint.archive_bytes += size;
|
|
200
|
+
footprint.archive_count += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
footprint.total_bytes = footprint.live_bytes + footprint.archive_bytes;
|
|
204
|
+
return footprint;
|
|
205
|
+
}
|
|
110
206
|
/**
|
|
111
207
|
* Rotates and says what happened, on both branches. Safe to call every tick:
|
|
112
208
|
* the size check is one stat per file and a rotation is rare.
|
|
113
209
|
*/
|
|
114
210
|
export async function rotateCollectorLogsBestEffort(paths, options = {}) {
|
|
115
|
-
let result = { rotated: [], failures: [] };
|
|
211
|
+
let result = { rotated: [], failures: [], swept: [] };
|
|
116
212
|
try {
|
|
117
213
|
result = await rotateCollectorLogs(paths, options);
|
|
118
214
|
}
|
|
119
215
|
catch (error) {
|
|
120
216
|
result = {
|
|
121
217
|
rotated: [],
|
|
218
|
+
swept: [],
|
|
122
219
|
failures: [
|
|
123
220
|
{
|
|
124
221
|
name: "*",
|
|
@@ -137,6 +234,13 @@ export async function rotateCollectorLogsBestEffort(paths, options = {}) {
|
|
|
137
234
|
archives_kept: rotated.archives,
|
|
138
235
|
}));
|
|
139
236
|
}
|
|
237
|
+
for (const swept of result.swept) {
|
|
238
|
+
console.error("[log-rotation] removed a log archive outside the keep window", JSON.stringify({
|
|
239
|
+
file: swept.name,
|
|
240
|
+
reason: swept.reason,
|
|
241
|
+
byte_size: swept.byte_size,
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
140
244
|
for (const failure of result.failures) {
|
|
141
245
|
console.error("[log-rotation] could not cap a scheduled-tick log", JSON.stringify({ file: failure.name, reason: failure.reason }));
|
|
142
246
|
}
|
package/dist/tower-stream.js
CHANGED
|
@@ -124,6 +124,10 @@ export async function readTowerTurn(response, options = {}) {
|
|
|
124
124
|
options.onToken?.(item.event);
|
|
125
125
|
continue;
|
|
126
126
|
}
|
|
127
|
+
if (item.event.type === "revision") {
|
|
128
|
+
options.onRevision?.(item.event);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
127
131
|
activity.push(item.event);
|
|
128
132
|
options.onActivity?.(item.event);
|
|
129
133
|
}
|
|
@@ -277,7 +281,7 @@ function parseLine(line) {
|
|
|
277
281
|
return { kind: "note", reason: "unexpected_event_type" };
|
|
278
282
|
}
|
|
279
283
|
const type = parsed.type;
|
|
280
|
-
if (type !== "activity" && type !== "token" && type !== "final") {
|
|
284
|
+
if (type !== "activity" && type !== "token" && type !== "revision" && type !== "final") {
|
|
281
285
|
return { kind: "note", reason: "unexpected_event_type" };
|
|
282
286
|
}
|
|
283
287
|
return { kind: "event", event: parsed };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.53",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-runtime-files.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/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.2",
|
|
31
|
+
"@bli-cockpit/telemetry-core": "0.1.27"
|
|
32
32
|
}
|
|
33
33
|
}
|