@bli-cockpit/cli 0.2.113 → 0.2.115
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/claude-attribution-score.js +24 -4
- package/dist/adapters/claude-attribution-signals.js +13 -6
- package/dist/adapters/claude-attribution-types.js +1 -1
- package/dist/adapters/codex-attribution.js +2 -0
- package/dist/adapters/codex-session-signals.js +13 -1
- package/dist/commands/agent-session-report.js +6 -0
- package/dist/commands/ops-render-ladder.js +13 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync.js +10 -0
- package/dist/commands/usage.js +6 -1
- package/package.json +4 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { finalizeSessionFacts, newSessionFactsAccumulator, observeSessionFactsContent, observeSessionFactsFile, } from "@bli-cockpit/telemetry-core";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import fs from "node:fs/promises";
|
|
@@ -11,9 +12,15 @@ import { CLAUDE_SESSION_MAX_FILE_BYTES, } from "./claude-attribution-types.js";
|
|
|
11
12
|
* Turns one discovered session into an attribution verdict: read (or, for an
|
|
12
13
|
* oversized main, stream) its signals, score them against the known
|
|
13
14
|
* worktrees, and validate its sidecars against the winning worktree.
|
|
15
|
+
*
|
|
16
|
+
* The same pass counts the session's deterministic facts (BLI-4341) so the
|
|
17
|
+
* upload carries its own token totals. Only files that belong to this session
|
|
18
|
+
* are counted: a sidecar rejected for a cwd mismatch is another worktree's
|
|
19
|
+
* work and must not inflate this session's numbers.
|
|
14
20
|
*/
|
|
15
21
|
export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
16
22
|
const fileName = path.basename(session.mainFile);
|
|
23
|
+
const factsAcc = newSessionFactsAccumulator("claude_code");
|
|
17
24
|
const base = {
|
|
18
25
|
file_path: session.mainFile,
|
|
19
26
|
file_name: fileName,
|
|
@@ -29,6 +36,7 @@ export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
29
36
|
oversized_lines_skipped: 0,
|
|
30
37
|
sidecars_capped: session.sidecarsCapped,
|
|
31
38
|
sidecar_files: [],
|
|
39
|
+
session_facts: null,
|
|
32
40
|
};
|
|
33
41
|
if (session.mainByteSize === 0) {
|
|
34
42
|
return skippedResult(base, "empty_file");
|
|
@@ -40,7 +48,7 @@ export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
40
48
|
// bytes themselves are not uploaded until the collection ceiling changes.
|
|
41
49
|
let streamed;
|
|
42
50
|
try {
|
|
43
|
-
streamed = await streamMainSignals(session.mainFile);
|
|
51
|
+
streamed = await streamMainSignals(session.mainFile, factsAcc);
|
|
44
52
|
}
|
|
45
53
|
catch (error) {
|
|
46
54
|
// A read race on one oversized main must not abort the whole scan; honor
|
|
@@ -55,6 +63,10 @@ export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
55
63
|
return skippedResult(base, "file_read_failed");
|
|
56
64
|
}
|
|
57
65
|
signals = streamed.signals;
|
|
66
|
+
observeSessionFactsFile(factsAcc, {
|
|
67
|
+
bytes: streamed.byteSize,
|
|
68
|
+
lines: streamed.signals.line_count,
|
|
69
|
+
});
|
|
58
70
|
base.content_hash_sha256 = streamed.contentHash;
|
|
59
71
|
base.byte_size = streamed.byteSize;
|
|
60
72
|
base.main_file_oversized = true;
|
|
@@ -80,7 +92,11 @@ export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
80
92
|
if (content.trim() === "") {
|
|
81
93
|
return skippedResult(base, "empty_file");
|
|
82
94
|
}
|
|
83
|
-
signals = extractClaudeSessionSignals(content);
|
|
95
|
+
signals = extractClaudeSessionSignals(content, factsAcc);
|
|
96
|
+
observeSessionFactsFile(factsAcc, {
|
|
97
|
+
bytes: raw.byteLength,
|
|
98
|
+
lines: signals.line_count,
|
|
99
|
+
});
|
|
84
100
|
}
|
|
85
101
|
const metaSessionId = sanitizeSessionId(signals.session_ids[0]);
|
|
86
102
|
if (metaSessionId) {
|
|
@@ -116,7 +132,7 @@ export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
116
132
|
if (isSchemaDriftSuspected(signals))
|
|
117
133
|
extraSignals.push("schema_drift_suspected");
|
|
118
134
|
const sidecarFiles = isRawEvidenceUploadableAttributionState(outcome.state, outcome.worktree !== null) && outcome.worktree
|
|
119
|
-
? await collectSidecarDiagnostics(session.sidecars, outcome.worktree)
|
|
135
|
+
? await collectSidecarDiagnostics(session.sidecars, outcome.worktree, factsAcc)
|
|
120
136
|
: session.sidecars.map((sidecar) => ({
|
|
121
137
|
local_path: sidecar.local_path,
|
|
122
138
|
file_name: sidecar.file_name,
|
|
@@ -126,6 +142,7 @@ export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
126
142
|
}));
|
|
127
143
|
return {
|
|
128
144
|
...base,
|
|
145
|
+
session_facts: finalizeSessionFacts(factsAcc),
|
|
129
146
|
state: outcome.state,
|
|
130
147
|
reason: outcome.reason,
|
|
131
148
|
signals: [...outcome.signals, ...extraSignals],
|
|
@@ -143,7 +160,7 @@ export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
143
160
|
* oversized sidecar is recorded with a reason and the session stays
|
|
144
161
|
* harvestable.
|
|
145
162
|
*/
|
|
146
|
-
async function collectSidecarDiagnostics(sidecars, worktree) {
|
|
163
|
+
async function collectSidecarDiagnostics(sidecars, worktree, facts) {
|
|
147
164
|
const out = [];
|
|
148
165
|
for (const sidecar of sidecars) {
|
|
149
166
|
const entry = {
|
|
@@ -181,6 +198,9 @@ async function collectSidecarDiagnostics(sidecars, worktree) {
|
|
|
181
198
|
out.push({ ...entry, skipped_reason: "sidecar_cwd_mismatch" });
|
|
182
199
|
continue;
|
|
183
200
|
}
|
|
201
|
+
// Accepted: this subagent's turns are part of this session, so its tokens
|
|
202
|
+
// are too. Counted only now, after the cwd check has cleared it.
|
|
203
|
+
observeSessionFactsContent(facts, content);
|
|
184
204
|
out.push({
|
|
185
205
|
...entry,
|
|
186
206
|
byte_size: raw.byteLength,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { observeSessionFactsRecord, } from "@bli-cockpit/telemetry-core";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import { createReadStream } from "node:fs";
|
|
3
4
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -7,15 +8,18 @@ import { CONTENT_RECORD_TYPES, MAX_LINE_BUFFER_BYTES, SCHEMA_DRIFT_MIN_HIT_RATE,
|
|
|
7
8
|
* `cwd`, `gitBranch`, `sessionId`, and `prRepository` (pr-link records).
|
|
8
9
|
* Structurally identical to the Codex extractor, with the allowlist enforced
|
|
9
10
|
* by construction — no generic record walk that could surface message bodies.
|
|
11
|
+
*
|
|
12
|
+
* Pass a session-facts accumulator to have the SAME parse feed the upload-time
|
|
13
|
+
* deterministic count (BLI-4341); without one the pass is unchanged.
|
|
10
14
|
*/
|
|
11
|
-
export function extractClaudeSessionSignals(content) {
|
|
12
|
-
const accumulator = createSignalAccumulator();
|
|
15
|
+
export function extractClaudeSessionSignals(content, facts) {
|
|
16
|
+
const accumulator = createSignalAccumulator(facts);
|
|
13
17
|
for (const line of content.split("\n")) {
|
|
14
18
|
accumulator.processLine(line);
|
|
15
19
|
}
|
|
16
20
|
return accumulator.finalize();
|
|
17
21
|
}
|
|
18
|
-
function createSignalAccumulator() {
|
|
22
|
+
function createSignalAccumulator(facts) {
|
|
19
23
|
const sessionIds = new Set();
|
|
20
24
|
const cwds = new Set();
|
|
21
25
|
const branches = new Set();
|
|
@@ -41,9 +45,12 @@ function createSignalAccumulator() {
|
|
|
41
45
|
parseErrorCount += 1;
|
|
42
46
|
return;
|
|
43
47
|
}
|
|
44
|
-
if (!record || typeof record !== "object")
|
|
48
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
45
49
|
return;
|
|
46
50
|
const entry = record;
|
|
51
|
+
// Counting is not reading: the facts module returns totals, never text.
|
|
52
|
+
if (facts)
|
|
53
|
+
observeSessionFactsRecord(facts, entry);
|
|
47
54
|
const type = typeof entry["type"] === "string" ? entry["type"] : "";
|
|
48
55
|
const cwd = stringOrNull(entry["cwd"]);
|
|
49
56
|
const sessionId = stringOrNull(entry["sessionId"]);
|
|
@@ -96,8 +103,8 @@ export function isSchemaDriftSuspected(signals) {
|
|
|
96
103
|
return true;
|
|
97
104
|
return signals.envelope_field_hit_rate < SCHEMA_DRIFT_MIN_HIT_RATE;
|
|
98
105
|
}
|
|
99
|
-
export async function streamMainSignals(filePath) {
|
|
100
|
-
const accumulator = createSignalAccumulator();
|
|
106
|
+
export async function streamMainSignals(filePath, facts) {
|
|
107
|
+
const accumulator = createSignalAccumulator(facts);
|
|
101
108
|
const hash = crypto.createHash("sha256");
|
|
102
109
|
let oversizedLinesSkipped = 0;
|
|
103
110
|
let byteSize = 0;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES } from "@bli-cockpit/telemetry-core";
|
|
1
|
+
import { RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES, } from "@bli-cockpit/telemetry-core";
|
|
2
2
|
/**
|
|
3
3
|
* The shapes and tuning constants `claude-attribution.ts` and its siblings
|
|
4
4
|
* (discovery, score, signals) all agree on. Kept in one module so none of
|
|
@@ -173,6 +173,7 @@ async function attributeOneSession(file, worktrees, collectionRoots) {
|
|
|
173
173
|
session_file_mtime_ms: file.mtimeMs,
|
|
174
174
|
byte_size: file.byteSize,
|
|
175
175
|
content_hash_sha256: null,
|
|
176
|
+
session_facts: null,
|
|
176
177
|
};
|
|
177
178
|
let read;
|
|
178
179
|
try {
|
|
@@ -191,6 +192,7 @@ async function attributeOneSession(file, worktrees, collectionRoots) {
|
|
|
191
192
|
}
|
|
192
193
|
base.content_hash_sha256 = read.contentHashSha256;
|
|
193
194
|
base.byte_size = read.byteSize;
|
|
195
|
+
base.session_facts = read.facts;
|
|
194
196
|
const signals = read.signals;
|
|
195
197
|
const metaSessionId = sanitizeSessionId(signals.session_ids[0]);
|
|
196
198
|
if (metaSessionId) {
|
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
* disk (sessions run to hundreds of megabytes, so the file is never held in
|
|
12
12
|
* memory and its sha256 is taken on the way past), and
|
|
13
13
|
* `extractCodexSessionSignals` takes content already in hand.
|
|
14
|
+
*
|
|
15
|
+
* The same streaming pass also counts the session's deterministic facts
|
|
16
|
+
* (BLI-4341): token totals per model, turn and tool counts, timestamps, size.
|
|
17
|
+
* The bytes are already in hand and already parsed, so counting them here
|
|
18
|
+
* costs one function call per record and saves the server a download.
|
|
14
19
|
*/
|
|
20
|
+
import { finalizeSessionFacts, newSessionFactsAccumulator, observeSessionFactsFile, observeSessionFactsRecord, } from "@bli-cockpit/telemetry-core";
|
|
15
21
|
import { createReadStream } from "node:fs";
|
|
16
22
|
import crypto from "node:crypto";
|
|
17
23
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -63,8 +69,10 @@ export async function readCodexMetadataSignals(filePath) {
|
|
|
63
69
|
lineBuffer += rest;
|
|
64
70
|
if (lineBuffer.trim())
|
|
65
71
|
absorbCodexSessionLine(state, lineBuffer);
|
|
72
|
+
observeSessionFactsFile(state.facts, { bytes: byteSize, lines: state.lineCount });
|
|
66
73
|
return {
|
|
67
74
|
signals: codexSignalsFromState(state),
|
|
75
|
+
facts: finalizeSessionFacts(state.facts),
|
|
68
76
|
contentHashSha256: hash.digest("hex"),
|
|
69
77
|
byteSize,
|
|
70
78
|
};
|
|
@@ -86,6 +94,7 @@ function makeSignalExtractionState() {
|
|
|
86
94
|
repositoryUrls: new Set(),
|
|
87
95
|
lineCount: 0,
|
|
88
96
|
parseErrorCount: 0,
|
|
97
|
+
facts: newSessionFactsAccumulator("codex"),
|
|
89
98
|
};
|
|
90
99
|
}
|
|
91
100
|
function absorbCodexSessionLine(state, line) {
|
|
@@ -105,8 +114,11 @@ function absorbCodexSessionLine(state, line) {
|
|
|
105
114
|
state.parseErrorCount += 1;
|
|
106
115
|
return;
|
|
107
116
|
}
|
|
108
|
-
if (!record || typeof record !== "object")
|
|
117
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
109
118
|
return;
|
|
119
|
+
// Every record feeds the facts count; only the two metadata types below feed
|
|
120
|
+
// attribution.
|
|
121
|
+
observeSessionFactsRecord(state.facts, record);
|
|
110
122
|
const type = record.type;
|
|
111
123
|
if (type !== "session_meta" && type !== "turn_context")
|
|
112
124
|
return;
|
|
@@ -43,6 +43,7 @@ function normalizeCodexResult(result) {
|
|
|
43
43
|
worktree: result.worktree,
|
|
44
44
|
cwd_basename: result.cwd_basename,
|
|
45
45
|
cwd_hash: result.cwd_hash,
|
|
46
|
+
session_facts: result.session_facts,
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
function normalizeClaudeResult(result) {
|
|
@@ -61,6 +62,7 @@ function normalizeClaudeResult(result) {
|
|
|
61
62
|
worktree: result.worktree,
|
|
62
63
|
cwd_basename: result.cwd_basename,
|
|
63
64
|
cwd_hash: result.cwd_hash,
|
|
65
|
+
session_facts: result.session_facts,
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
@@ -154,6 +156,10 @@ function sessionReportEntry(result, context) {
|
|
|
154
156
|
: {}),
|
|
155
157
|
session_file_byte_size: result.byte_size,
|
|
156
158
|
session_file_mtime: result.session_file_mtime,
|
|
159
|
+
// Every session arrives counted (BLI-4341). Absent only when the file
|
|
160
|
+
// could not be read at all, which is the one case the server's extraction
|
|
161
|
+
// pass still has to cover.
|
|
162
|
+
...(result.session_facts ? { session_facts: result.session_facts } : {}),
|
|
157
163
|
...(result.worktree
|
|
158
164
|
? {
|
|
159
165
|
repo_fingerprint: result.worktree.repo_fingerprint,
|
|
@@ -15,9 +15,22 @@ export function renderLadder(ladder, dim) {
|
|
|
15
15
|
}
|
|
16
16
|
if (reasons.length > 8)
|
|
17
17
|
lines.push(dim(` ${reasons.length - 8} more reasons not shown`));
|
|
18
|
+
// BLI-4343: deferred is its OWN count. A row waiting on a corpus pass is
|
|
19
|
+
// pending and not stuck, and the two look identical without this line.
|
|
20
|
+
if ((ladder.deferred ?? 0) > 0) {
|
|
21
|
+
const soonest = ladder.soonest_defer_until;
|
|
22
|
+
const when = typeof soonest === "string" ? `, soonest retry ${soonest}` : "";
|
|
23
|
+
lines.push(dim(` ${ladder.deferred} row(s) deferred, waiting on another pass${when}`));
|
|
24
|
+
}
|
|
18
25
|
if ((ladder.stale_leases ?? 0) > 0) {
|
|
19
26
|
lines.push(dim(` ${ladder.stale_leases} stale lease(s) waiting to be taken over`));
|
|
20
27
|
}
|
|
28
|
+
// BLI-4352: work no rung status can show. A session whose four rungs all
|
|
29
|
+
// succeeded can still be carrying the old token-weight session label, and
|
|
30
|
+
// the re-label run is steered by this number.
|
|
31
|
+
if ((ladder.labels_v1_remaining ?? 0) > 0) {
|
|
32
|
+
lines.push(dim(` ${ladder.labels_v1_remaining} session label(s) still on the old provenance`));
|
|
33
|
+
}
|
|
21
34
|
if (ladder.oldest_pending_session_id) {
|
|
22
35
|
lines.push(dim(` oldest pending session ${ladder.oldest_pending_session_id}`));
|
|
23
36
|
}
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.115");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -108,6 +108,16 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
108
108
|
now,
|
|
109
109
|
claudePriorDurablePointers: worktreePass.claudePriorDurablePointers,
|
|
110
110
|
});
|
|
111
|
+
// BLI-4341: every session should arrive counted. Say how many did, and say
|
|
112
|
+
// it on the success path too: a silent drop from "all" to "some" is exactly
|
|
113
|
+
// the kind of decay nobody notices.
|
|
114
|
+
const sessionsCounted = sessions.filter((session) => session.session_facts).length;
|
|
115
|
+
console.error("[session-sync] session facts counted at upload", JSON.stringify({
|
|
116
|
+
reason: sessionsCounted === sessions.length ? "counted_at_upload" : "some_sessions_unreadable",
|
|
117
|
+
sessions: sessions.length,
|
|
118
|
+
counted: sessionsCounted,
|
|
119
|
+
tokens_total: sessions.reduce((total, session) => total + (session.session_facts?.total_tokens ?? 0), 0),
|
|
120
|
+
}));
|
|
111
121
|
const delivery = await reportSessionsAndAdvanceCursors({
|
|
112
122
|
run: options,
|
|
113
123
|
paths,
|
package/dist/commands/usage.js
CHANGED
|
@@ -43,7 +43,7 @@ export async function runUsage(command, io) {
|
|
|
43
43
|
if (!email)
|
|
44
44
|
return failAgentDoor(door, "[usage]", "caller_email_unavailable", "The paired session has no email. Sign in again or pass --person <email>.");
|
|
45
45
|
body.people = (body.people ?? []).filter((person) => person.email?.toLowerCase() === email.toLowerCase());
|
|
46
|
-
body.coverage = { sessions_labelled: body.people.reduce((total, person) => total + (person.sessions_labelled ?? 0), 0), sessions_extracted: body.people.reduce((total, person) => total + person.sessions_extracted, 0), sessions_observed: body.people.reduce((total, person) => total + person.sessions_observed, 0) };
|
|
46
|
+
body.coverage = { sessions_labelled: body.people.reduce((total, person) => total + (person.sessions_labelled ?? 0), 0), sessions_extracted: body.people.reduce((total, person) => total + person.sessions_extracted, 0), sessions_observed: body.people.reduce((total, person) => total + person.sessions_observed, 0), sessions_counted_at_upload: body.people.reduce((total, person) => total + (person.sessions_counted_at_upload ?? 0), 0), sessions_counted_by_server: body.people.reduce((total, person) => total + (person.sessions_counted_by_server ?? 0), 0), sessions_counted_by_ladder: body.people.reduce((total, person) => total + (person.sessions_counted_by_ladder ?? 0), 0) };
|
|
47
47
|
}
|
|
48
48
|
if (command.byRepo && body.people?.some((person) => !Array.isArray(person.repos))) {
|
|
49
49
|
return failAgentDoor(door, "[usage]", "repo_grouping_unavailable", "The dashboard returned person totals without project rows. Deploy the dashboard project split before using --by-repo.");
|
|
@@ -104,6 +104,11 @@ export async function runUsage(command, io) {
|
|
|
104
104
|
writeLine(io.stdout, "");
|
|
105
105
|
writeLine(io.stdout, body.api_list_price_equivalent_label ?? "API list-price equivalent (not actual spend)");
|
|
106
106
|
writeLine(io.stdout, `${body.coverage?.sessions_extracted ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions extracted`);
|
|
107
|
+
// BLI-4341: a session should arrive counted. This line is how a person sees
|
|
108
|
+
// whether that is happening, or whether something else is doing the work.
|
|
109
|
+
// BLI-4351 added the third counter: the ladder, counting out of Storage the
|
|
110
|
+
// sessions that arrived before the collector counted anything.
|
|
111
|
+
writeLine(io.stdout, `${body.coverage?.sessions_counted_at_upload ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions counted at upload, ${body.coverage?.sessions_counted_by_server ?? 0} by server backfill, ${body.coverage?.sessions_counted_by_ladder ?? 0} by the ladder`);
|
|
107
112
|
if (command.bySubject)
|
|
108
113
|
writeLine(io.stdout, `${(body.people ?? []).reduce((n, person) => n + (person.sessions_summarized ?? 0), 0)} of ${body.coverage?.sessions_observed ?? 0} sessions summarized`);
|
|
109
114
|
if (command.byTopic)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.115",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,8 +27,8 @@
|
|
|
27
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-exit-contract.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.32",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.43",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.48"
|
|
33
33
|
}
|
|
34
34
|
}
|