@bli-cockpit/cli 0.2.70 → 0.2.74
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/doctor-disk-words.js +59 -0
- package/dist/commands/doctor-mcp.js +98 -0
- package/dist/commands/doctor-pipeline.js +15 -21
- package/dist/commands/doctor.js +29 -8
- package/dist/commands/heartbeat.js +35 -0
- package/dist/commands/mcp-stdio-probe.js +267 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/status.js +8 -1
- package/dist/commands/sync-followups.js +64 -1
- package/dist/commands/sync.js +13 -2
- package/dist/disk-retention.js +5 -1
- package/dist/disk-usage.js +62 -9
- package/dist/evidence-redelivery-plan.js +277 -0
- package/dist/evidence-redelivery.js +280 -0
- package/dist/local-state-status.js +34 -0
- package/package.json +5 -5
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offering undelivered staged evidence again, on the next tick and every tick
|
|
3
|
+
* after, until it lands or names why it cannot.
|
|
4
|
+
*
|
|
5
|
+
* BLI-3797. `evidence-redelivery-plan.ts` says WHICH bytes and rebuilds the
|
|
6
|
+
* offer; this file spends it against the same chunked upload client the live
|
|
7
|
+
* sync uses, writes each success into the commit ledger and each failure into
|
|
8
|
+
* the delivery-attempt history (so the existing 15-min → 6-h backoff applies
|
|
9
|
+
* and one doomed object is not re-offered 96 times a day), and says what
|
|
10
|
+
* happened on every branch.
|
|
11
|
+
*
|
|
12
|
+
* Four properties, the same ones the reconcile and prune follow-ups keep:
|
|
13
|
+
*
|
|
14
|
+
* - **It never throws and never fails a tick.** A drain that cannot run is a
|
|
15
|
+
* log line and a receipt, never a reason collection did not happen.
|
|
16
|
+
* - **It never deletes.** Nothing here removes a staged byte; the prune that
|
|
17
|
+
* follows it decides that, and it only ever takes what the ledger vouches
|
|
18
|
+
* for — which now includes whatever this just delivered.
|
|
19
|
+
* - **It is bounded.** One drain rate per tick (see the plan module), so the
|
|
20
|
+
* backlog clears in hours without competing with the tick's own collection.
|
|
21
|
+
* - **It says so on both branches.** A tick with nothing undelivered is silent
|
|
22
|
+
* (the steady state, 96 times a day); every other outcome, including a clean
|
|
23
|
+
* drain, gets one `[evidence-redelivery]` line with counts and reasons.
|
|
24
|
+
*/
|
|
25
|
+
import { uploadRawEvidenceFilesChunked, } from "./evidence-upload-client.js";
|
|
26
|
+
import { planEvidenceRedelivery, } from "./evidence-redelivery-plan.js";
|
|
27
|
+
import { markObjectCommitted, readRawEvidenceCursor, writeRawEvidenceCursor, } from "./cursors/raw-evidence-cursor.js";
|
|
28
|
+
import { readStagingInventory } from "./disk-usage.js";
|
|
29
|
+
import { describeError } from "./health-detail.js";
|
|
30
|
+
import { getCollectorRuntimePaths, readLocalCollectorSessionFile, } from "./local-state.js";
|
|
31
|
+
import { clearDeliveryAttempt, readRawEvidenceStagingState, recordDeliveryFailure, writeRawEvidenceStagingState, } from "./raw-evidence-staging.js";
|
|
32
|
+
/**
|
|
33
|
+
* The drain the sync tick runs and `cockpit doctor`'s disk fix reuses.
|
|
34
|
+
* Never throws.
|
|
35
|
+
*/
|
|
36
|
+
export async function runEvidenceRedelivery(options) {
|
|
37
|
+
const env = options.env ?? process.env;
|
|
38
|
+
const now = options.now ?? new Date();
|
|
39
|
+
if (env["COCKPIT_DISABLE_EVIDENCE_REDELIVERY"] === "1") {
|
|
40
|
+
return emptyResult("disabled", "skipped");
|
|
41
|
+
}
|
|
42
|
+
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
43
|
+
try {
|
|
44
|
+
const inventory = options.inventory ?? (await readStagingInventory(paths, now));
|
|
45
|
+
if (inventory.uncommitted_count === 0) {
|
|
46
|
+
// The steady state, 96 ticks a day. Saying so every time would bury the
|
|
47
|
+
// ticks that matter; `cockpit doctor`'s disk row still says it out loud
|
|
48
|
+
// to a person who asks.
|
|
49
|
+
return emptyResult("nothing_uncommitted", "ok");
|
|
50
|
+
}
|
|
51
|
+
const staging = await readRawEvidenceStagingState(paths.state_dir);
|
|
52
|
+
const plan = await planEvidenceRedelivery({
|
|
53
|
+
stateDir: paths.state_dir,
|
|
54
|
+
inventory,
|
|
55
|
+
staging,
|
|
56
|
+
now,
|
|
57
|
+
...(options.maxBytes != null ? { maxBytes: options.maxBytes } : {}),
|
|
58
|
+
...(options.maxObjects != null ? { maxObjects: options.maxObjects } : {}),
|
|
59
|
+
});
|
|
60
|
+
if (plan.object_count === 0) {
|
|
61
|
+
const result = emptyPlanResult(plan);
|
|
62
|
+
reportRedelivery(result, plan);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
66
|
+
if (!session ||
|
|
67
|
+
session.session_state !== "valid" ||
|
|
68
|
+
typeof session.device_token !== "string" ||
|
|
69
|
+
!session.device_token) {
|
|
70
|
+
// Not a failure of the backlog: this machine simply cannot speak to the
|
|
71
|
+
// dashboard right now. The bytes stay, the reason is named, and the next
|
|
72
|
+
// tick after a `cockpit login` picks them straight back up.
|
|
73
|
+
const result = {
|
|
74
|
+
...emptyPlanResult(plan),
|
|
75
|
+
status: "skipped",
|
|
76
|
+
reason: "no_device_session",
|
|
77
|
+
};
|
|
78
|
+
reportRedelivery(result, plan);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
const outcome = await deliverPlan({
|
|
82
|
+
plan,
|
|
83
|
+
dashboardUrl: options.dashboardUrl,
|
|
84
|
+
deviceToken: session.device_token,
|
|
85
|
+
fetchImpl: options.fetch ?? fetch,
|
|
86
|
+
now,
|
|
87
|
+
});
|
|
88
|
+
await recordOutcomes(paths, staging, outcome.outcomes, now);
|
|
89
|
+
const result = {
|
|
90
|
+
status: outcome.failed > 0 && outcome.uploaded === 0 ? "fail" : "ok",
|
|
91
|
+
reason: "redelivered",
|
|
92
|
+
offered: plan.object_count,
|
|
93
|
+
offered_bytes: plan.bytes,
|
|
94
|
+
uploaded: outcome.uploaded,
|
|
95
|
+
uploaded_bytes: outcome.uploadedBytes,
|
|
96
|
+
reused: outcome.reused,
|
|
97
|
+
failed: outcome.failed,
|
|
98
|
+
held: plan.held_count,
|
|
99
|
+
deferred: plan.deferred_count,
|
|
100
|
+
deferred_bytes: plan.deferred_bytes,
|
|
101
|
+
packs: plan.packs.length,
|
|
102
|
+
failure_reasons: outcome.failureReasons,
|
|
103
|
+
};
|
|
104
|
+
reportRedelivery(result, plan);
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.error("[evidence-redelivery] the drain could not run; nothing was re-offered", JSON.stringify({ reason: "redelivery_threw", ...describeError(error) }));
|
|
109
|
+
return { ...emptyResult("redelivery_threw", "fail") };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One `uploadRawEvidenceFilesChunked` call per pack, because provenance is a
|
|
114
|
+
* PACK fact: the repo, branch, operator, session and work context these bytes
|
|
115
|
+
* belonged to were recorded when they were staged, and sending one pack's
|
|
116
|
+
* objects under another pack's labels would file six-week-old evidence against
|
|
117
|
+
* today's ticket.
|
|
118
|
+
*/
|
|
119
|
+
async function deliverPlan(options) {
|
|
120
|
+
const result = {
|
|
121
|
+
outcomes: [],
|
|
122
|
+
uploaded: 0,
|
|
123
|
+
uploadedBytes: 0,
|
|
124
|
+
reused: 0,
|
|
125
|
+
failed: 0,
|
|
126
|
+
failureReasons: [],
|
|
127
|
+
};
|
|
128
|
+
for (const pack of options.plan.packs) {
|
|
129
|
+
const uploaded = await deliverOnePack(pack, options);
|
|
130
|
+
for (const outcome of uploaded) {
|
|
131
|
+
result.outcomes.push(outcome);
|
|
132
|
+
if (outcome.upload_state === "uploaded") {
|
|
133
|
+
result.uploaded += 1;
|
|
134
|
+
result.uploadedBytes += outcome.pointer.byte_size ?? 0;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (outcome.upload_state === "reused_existing") {
|
|
138
|
+
result.reused += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
result.failed += 1;
|
|
142
|
+
const reason = outcome.reason ?? "unknown";
|
|
143
|
+
if (!result.failureReasons.includes(reason)) {
|
|
144
|
+
result.failureReasons.push(reason);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
async function deliverOnePack(pack, options) {
|
|
151
|
+
try {
|
|
152
|
+
const uploaded = await uploadRawEvidenceFilesChunked({
|
|
153
|
+
fetchImpl: options.fetchImpl,
|
|
154
|
+
dashboardUrl: options.dashboardUrl,
|
|
155
|
+
deviceToken: options.deviceToken,
|
|
156
|
+
provenance: pack.provenance,
|
|
157
|
+
generatedAt: options.now.toISOString(),
|
|
158
|
+
files: pack.files,
|
|
159
|
+
});
|
|
160
|
+
return uploaded.outcomes;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
// The upload client already reports its own transport failures; this is the
|
|
164
|
+
// net for anything it does not, and it must not abandon the packs behind
|
|
165
|
+
// this one. Each file is reported failed so the backoff history records the
|
|
166
|
+
// attempt rather than re-offering all of them again in fifteen minutes.
|
|
167
|
+
console.error("[evidence-redelivery] a pack's re-offer threw; the rest of the drain continues", JSON.stringify({
|
|
168
|
+
reason: "pack_redelivery_threw",
|
|
169
|
+
pack_id: pack.pack_id,
|
|
170
|
+
object_count: pack.files.length,
|
|
171
|
+
...describeError(error),
|
|
172
|
+
}));
|
|
173
|
+
return pack.files.map((file) => ({
|
|
174
|
+
pointer: file.pointer,
|
|
175
|
+
object_key: file.pointer.object_key ?? "",
|
|
176
|
+
codex_session_id: null,
|
|
177
|
+
kind: file.kind ?? "unknown",
|
|
178
|
+
upload_state: "upload_failed",
|
|
179
|
+
reason: "pack_redelivery_threw",
|
|
180
|
+
uploaded_chunk_count: 0,
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Write what happened where the next tick will read it: the commit ledger for
|
|
186
|
+
* anything that landed, the delivery-attempt history for anything that did not.
|
|
187
|
+
*
|
|
188
|
+
* Both writes happen even when the run partly failed, and the ledger is written
|
|
189
|
+
* FIRST — a delivered object whose ledger row was lost would be re-offered
|
|
190
|
+
* forever, which is the failure this whole module exists to end.
|
|
191
|
+
*/
|
|
192
|
+
async function recordOutcomes(paths, staging, outcomes, now) {
|
|
193
|
+
const cursor = await readRawEvidenceCursor(paths);
|
|
194
|
+
let ledgerChanged = false;
|
|
195
|
+
let stagingChanged = false;
|
|
196
|
+
for (const outcome of outcomes) {
|
|
197
|
+
const hash = outcome.pointer.content_hash_sha256;
|
|
198
|
+
if (!hash)
|
|
199
|
+
continue;
|
|
200
|
+
if (outcome.upload_state === "upload_failed") {
|
|
201
|
+
recordDeliveryFailure(staging, hash, {
|
|
202
|
+
reason: outcome.reason ?? "unknown",
|
|
203
|
+
attemptedAt: now,
|
|
204
|
+
byteSize: outcome.pointer.byte_size ?? 0,
|
|
205
|
+
});
|
|
206
|
+
stagingChanged = true;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
markObjectCommitted(cursor, hash, {
|
|
210
|
+
object_key: outcome.object_key,
|
|
211
|
+
byte_size: outcome.pointer.byte_size ?? 0,
|
|
212
|
+
committed_at: now.toISOString(),
|
|
213
|
+
});
|
|
214
|
+
ledgerChanged = true;
|
|
215
|
+
if (clearDeliveryAttempt(staging, hash))
|
|
216
|
+
stagingChanged = true;
|
|
217
|
+
}
|
|
218
|
+
if (ledgerChanged)
|
|
219
|
+
await writeRawEvidenceCursor(paths, cursor);
|
|
220
|
+
if (stagingChanged) {
|
|
221
|
+
await writeRawEvidenceStagingState(paths.state_dir, staging);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* One line per non-steady-state tick. Counts, byte totals and reason labels
|
|
226
|
+
* only — no pack id, no hash, no path, per the logging contract.
|
|
227
|
+
*/
|
|
228
|
+
function reportRedelivery(result, plan) {
|
|
229
|
+
const fields = {
|
|
230
|
+
reason: result.reason,
|
|
231
|
+
status: result.status,
|
|
232
|
+
offered: result.offered,
|
|
233
|
+
offered_bytes: result.offered_bytes,
|
|
234
|
+
uploaded: result.uploaded,
|
|
235
|
+
uploaded_bytes: result.uploaded_bytes,
|
|
236
|
+
reused: result.reused,
|
|
237
|
+
failed: result.failed,
|
|
238
|
+
held_by_backoff: result.held,
|
|
239
|
+
deferred_to_next_tick: result.deferred,
|
|
240
|
+
deferred_bytes: result.deferred_bytes,
|
|
241
|
+
packs: result.packs,
|
|
242
|
+
unplannable: plan.unplannable,
|
|
243
|
+
failure_reasons: result.failure_reasons,
|
|
244
|
+
};
|
|
245
|
+
if (result.status === "fail") {
|
|
246
|
+
console.error("[evidence-redelivery] every re-offered object was refused", JSON.stringify(fields));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
console.error(result.uploaded > 0
|
|
250
|
+
? "[evidence-redelivery] undelivered staged evidence landed"
|
|
251
|
+
: "[evidence-redelivery] undelivered staged evidence was not re-offered this tick", JSON.stringify(fields));
|
|
252
|
+
}
|
|
253
|
+
function emptyPlanResult(plan) {
|
|
254
|
+
const nothingPlannable = plan.held_count > 0 && plan.deferred_count === 0
|
|
255
|
+
? "all_held_by_backoff"
|
|
256
|
+
: "nothing_plannable";
|
|
257
|
+
return {
|
|
258
|
+
...emptyResult(nothingPlannable, "ok"),
|
|
259
|
+
held: plan.held_count,
|
|
260
|
+
deferred: plan.deferred_count,
|
|
261
|
+
deferred_bytes: plan.deferred_bytes,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function emptyResult(reason, status) {
|
|
265
|
+
return {
|
|
266
|
+
status,
|
|
267
|
+
reason,
|
|
268
|
+
offered: 0,
|
|
269
|
+
offered_bytes: 0,
|
|
270
|
+
uploaded: 0,
|
|
271
|
+
uploaded_bytes: 0,
|
|
272
|
+
reused: 0,
|
|
273
|
+
failed: 0,
|
|
274
|
+
held: 0,
|
|
275
|
+
deferred: 0,
|
|
276
|
+
deferred_bytes: 0,
|
|
277
|
+
packs: 0,
|
|
278
|
+
failure_reasons: [],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { readStagingInventory } from "./disk-usage.js";
|
|
4
|
+
import { describeError } from "./health-detail.js";
|
|
3
5
|
import { summarizeLocalUploadSpool } from "./spool/local-spool.js";
|
|
4
6
|
import { summarizeInstallEventOutbox } from "./spool/install-event-outbox.js";
|
|
5
7
|
import { readRawEvidenceStagingState, summarizeStuckEvidence, } from "./raw-evidence-staging.js";
|
|
@@ -55,6 +57,10 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
55
57
|
stuck_evidence_max_attempts: stuckEvidence.max_attempts,
|
|
56
58
|
stuck_evidence_oldest_failure_at: stuckEvidence.oldest_first_failed_at,
|
|
57
59
|
stuck_evidence_reasons: stuckEvidence.reasons,
|
|
60
|
+
staging_uncommitted_count: reading.staging?.uncommitted_count ?? null,
|
|
61
|
+
staging_uncommitted_bytes: reading.staging?.uncommitted_bytes ?? null,
|
|
62
|
+
staging_uncommitted_reason: reading.staging?.dominant_uncommitted_reason?.reason ?? null,
|
|
63
|
+
staging_oldest_uncommitted_at: reading.staging?.oldest_uncommitted_at ?? null,
|
|
58
64
|
details: describeCollectorStatus(reading),
|
|
59
65
|
};
|
|
60
66
|
}
|
|
@@ -76,7 +82,20 @@ async function readCollectorStatus(options, now) {
|
|
|
76
82
|
readRawEvidenceStagingState(paths.state_dir),
|
|
77
83
|
]);
|
|
78
84
|
const stuckEvidence = summarizeStuckEvidence(stagingState, now);
|
|
85
|
+
// Best-effort by construction: a status read must never fail because the
|
|
86
|
+
// staging tree could not be walked, and an absent backlog reads as "this read
|
|
87
|
+
// did not ask", never as "there is none".
|
|
88
|
+
const staging = options.includeStagingBacklog
|
|
89
|
+
? await readStagingInventory(paths, now).catch((error) => {
|
|
90
|
+
console.error("[collector status] the staging tree could not be read; the undelivered backlog is unknown", JSON.stringify({
|
|
91
|
+
reason: "staging_inventory_unreadable",
|
|
92
|
+
...describeError(error),
|
|
93
|
+
}));
|
|
94
|
+
return null;
|
|
95
|
+
})
|
|
96
|
+
: null;
|
|
79
97
|
return {
|
|
98
|
+
staging,
|
|
80
99
|
paths,
|
|
81
100
|
config,
|
|
82
101
|
session,
|
|
@@ -134,6 +153,18 @@ function describeCollectorStatus(reading) {
|
|
|
134
153
|
if (healthOutbox.pending_count > 0) {
|
|
135
154
|
details.push(`Collector health retry pending: ${healthOutbox.pending_count} sanitized receipt(s) queued since ${healthOutbox.oldest_created_at ?? "unknown"}.`);
|
|
136
155
|
}
|
|
156
|
+
// BLI-3797: the line above only sees objects whose delivery FAILED. This one
|
|
157
|
+
// sees evidence that is on this disk and not in the bucket, however it got
|
|
158
|
+
// that way — which is the only question that answers "has everything landed?".
|
|
159
|
+
if (reading.staging && reading.staging.uncommitted_count > 0) {
|
|
160
|
+
const dominant = reading.staging.dominant_uncommitted_reason;
|
|
161
|
+
details.push(`Staged evidence not yet accepted: ${reading.staging.uncommitted_count} object(s), ${mib(reading.staging.uncommitted_bytes)} MB, oldest staged ${reading.staging.oldest_uncommitted_at ?? "unknown"}${dominant
|
|
162
|
+
? ` — biggest reason ${dominant.reason} on ${dominant.count} object(s)`
|
|
163
|
+
: ""}. \`cockpit sync\` re-offers them each tick; \`cockpit doctor --fix\` does it now.`);
|
|
164
|
+
}
|
|
165
|
+
else if (reading.staging) {
|
|
166
|
+
details.push("Staged evidence: every object on this disk is accepted.");
|
|
167
|
+
}
|
|
137
168
|
if (stuckEvidence.stuck_object_count > 0) {
|
|
138
169
|
details.push(`Raw evidence stuck: ${stuckEvidence.stuck_object_count} object(s) have never been accepted (${stuckEvidence.held_object_count} waiting on backoff, worst ${stuckEvidence.max_attempts} attempt(s) since ${stuckEvidence.oldest_first_failed_at ?? "unknown"}) — reasons: ${stuckEvidence.reasons.join(", ") || "unknown"}.`);
|
|
139
170
|
}
|
|
@@ -155,6 +186,9 @@ export function classifyCollectorFreshness(context, lastUploadSuccessAt, now) {
|
|
|
155
186
|
return "stale";
|
|
156
187
|
return now.getTime() - latestActivityAt <= 5 * 60 * 1000 ? "fresh" : "stale";
|
|
157
188
|
}
|
|
189
|
+
function mib(bytes) {
|
|
190
|
+
return (bytes / (1024 * 1024)).toFixed(1);
|
|
191
|
+
}
|
|
158
192
|
function latestTimestamp(values) {
|
|
159
193
|
const timestamps = values
|
|
160
194
|
.map((value) => Date.parse(value ?? ""))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.74",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"pretypecheck": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/cli",
|
|
25
25
|
"typecheck": "node -e \"await import('./dist/commands/public-root.js')\"",
|
|
26
26
|
"pretest": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/cli",
|
|
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"
|
|
27
|
+
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.11",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.13",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.32"
|
|
33
33
|
}
|
|
34
34
|
}
|