@bli-cockpit/cli 0.1.12 → 0.1.14
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/README.md +29 -0
- package/dist/adapters/common.js +2 -0
- package/dist/adapters/local-sources.js +4 -0
- package/dist/adapters/raw-evidence.js +54 -18
- package/dist/commands/local.js +7 -0
- package/dist/local-state.js +16 -3
- package/dist/upload.js +12 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,6 +122,35 @@ Remote dashboard:
|
|
|
122
122
|
- Codex session attribution records (session id, file hash, attribution state
|
|
123
123
|
and reason labels, scores) accepted by `/api/ambient/codex-sessions`.
|
|
124
124
|
|
|
125
|
+
New raw evidence object keys are readable from the Storage browser:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
operators/<operator-slug>-<operator-short>/repos/<repo-slug>/worktrees/<worktree-slug>/tickets/<ticket-id-or-unbound>/dates/<YYYY-MM-DD>/sessions/<session-slug>/ids/<operator_user_id>/<work_context_id>/<source>/<source-path>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The readable folders are for humans. The canonical join still lives in
|
|
132
|
+
`ambient_evidence_refs.raw_evidence_pointer_id`, `storage_bucket`, and
|
|
133
|
+
`object_key`. For SQL recipes that reconstruct a day of work by operator, date,
|
|
134
|
+
repo, ticket, and raw JSONL object, see
|
|
135
|
+
[`docs/runbooks/cockpit-data-traceability.md`](../../docs/runbooks/cockpit-data-traceability.md).
|
|
136
|
+
|
|
137
|
+
## Read or debug captured data
|
|
138
|
+
|
|
139
|
+
Local attribution preview:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
cockpit sessions --repo "$PWD" --json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Remote metadata path:
|
|
146
|
+
|
|
147
|
+
1. Query `ambient_evidence_refs` by `operator_user_id`, `received_at`, and
|
|
148
|
+
optional `ticket_id`.
|
|
149
|
+
2. Join `ambient_work_sessions` only for current labels such as repo, worktree,
|
|
150
|
+
branch, and collector version.
|
|
151
|
+
3. Open the `ambient-raw-evidence` object only after SQL identifies the exact
|
|
152
|
+
row and `object_key`.
|
|
153
|
+
|
|
125
154
|
Never provide Supabase service-role keys, raw DB URLs, root env files, cookies,
|
|
126
155
|
or deployment tokens to this CLI. The collector must never read env files and
|
|
127
156
|
does not collect random desktop screenshots or screen recordings.
|
package/dist/adapters/common.js
CHANGED
|
@@ -14,6 +14,8 @@ export function makeCaptureProvenance(context, captureSource) {
|
|
|
14
14
|
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
15
15
|
repo: context.repoRoot,
|
|
16
16
|
branch: context.branch,
|
|
17
|
+
repo_label: context.repoLabel,
|
|
18
|
+
worktree_label: context.worktreeLabel,
|
|
17
19
|
operator_id: context.operatorId,
|
|
18
20
|
session_id: context.sessionId,
|
|
19
21
|
work_context_id: context.workContextId,
|
|
@@ -8,10 +8,14 @@ import { resolveTicketBinding, } from "./ticket-binding.js";
|
|
|
8
8
|
export async function runLocalSourceCollectors(options) {
|
|
9
9
|
const context = {
|
|
10
10
|
repoRoot: options.repoRoot,
|
|
11
|
+
repoLabel: options.activeWorkContext?.repo_label,
|
|
12
|
+
worktreeLabel: options.activeWorkContext?.worktree_label,
|
|
11
13
|
branch: options.branch,
|
|
12
14
|
operatorId: options.operatorId,
|
|
15
|
+
operatorLabel: options.operatorLabel,
|
|
13
16
|
sessionId: options.sessionId,
|
|
14
17
|
workContextId: options.workContextId,
|
|
18
|
+
activeTicketId: options.activeWorkContext?.active_ticket_id ?? null,
|
|
15
19
|
now: options.now ?? new Date(),
|
|
16
20
|
};
|
|
17
21
|
const git = await collectGitState(context);
|
|
@@ -99,10 +99,9 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
99
99
|
await fs.writeFile(manifestPath, manifestBytes, { mode: 0o600 });
|
|
100
100
|
await chmodPrivate(manifestPath, 0o600);
|
|
101
101
|
const manifestEntry = evidenceEntry({
|
|
102
|
+
context,
|
|
102
103
|
kind: "manifest",
|
|
103
104
|
packId,
|
|
104
|
-
operatorId: context.operatorId,
|
|
105
|
-
workContextId: context.workContextId,
|
|
106
105
|
localPath: manifestPath,
|
|
107
106
|
relativePath: "manifest.json",
|
|
108
107
|
mediaType: "application/json",
|
|
@@ -350,10 +349,9 @@ async function collectOneAgentImageFile(collection, options) {
|
|
|
350
349
|
await fs.writeFile(destination, raw, { mode: 0o600 });
|
|
351
350
|
await chmodPrivate(destination, 0o600);
|
|
352
351
|
collection.entries.push(evidenceEntry({
|
|
352
|
+
context: collection.context,
|
|
353
353
|
kind: options.kind,
|
|
354
354
|
packId: collection.packId,
|
|
355
|
-
operatorId: collection.context.operatorId,
|
|
356
|
-
workContextId: collection.context.workContextId,
|
|
357
355
|
localPath: destination,
|
|
358
356
|
relativePath,
|
|
359
357
|
mediaType: metadata.media_type,
|
|
@@ -434,10 +432,9 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
434
432
|
await fs.writeFile(destination, raw, { mode: 0o600 });
|
|
435
433
|
await chmodPrivate(destination, 0o600);
|
|
436
434
|
collection.entries.push(evidenceEntry({
|
|
435
|
+
context: collection.context,
|
|
437
436
|
kind: options.kind,
|
|
438
437
|
packId: collection.packId,
|
|
439
|
-
operatorId: collection.context.operatorId,
|
|
440
|
-
workContextId: collection.context.workContextId,
|
|
441
438
|
localPath: destination,
|
|
442
439
|
relativePath,
|
|
443
440
|
mediaType: options.mediaType,
|
|
@@ -504,10 +501,9 @@ async function collectGitDiffFiles(collection, repoRoot) {
|
|
|
504
501
|
await fs.writeFile(destination, raw, { mode: 0o600 });
|
|
505
502
|
await chmodPrivate(destination, 0o600);
|
|
506
503
|
collection.entries.push(evidenceEntry({
|
|
504
|
+
context: collection.context,
|
|
507
505
|
kind: "git_diff",
|
|
508
506
|
packId: collection.packId,
|
|
509
|
-
operatorId: collection.context.operatorId,
|
|
510
|
-
workContextId: collection.context.workContextId,
|
|
511
507
|
localPath: destination,
|
|
512
508
|
relativePath,
|
|
513
509
|
mediaType: "text/x-diff",
|
|
@@ -577,6 +573,10 @@ function makeManifest(options) {
|
|
|
577
573
|
work_context_id: options.context.workContextId,
|
|
578
574
|
session_id: options.context.sessionId,
|
|
579
575
|
operator_id: options.context.operatorId,
|
|
576
|
+
operator_label: options.context.operatorLabel,
|
|
577
|
+
repo_label: options.context.repoLabel ?? path.basename(options.context.repoRoot),
|
|
578
|
+
worktree_label: options.context.worktreeLabel,
|
|
579
|
+
active_ticket_id: options.context.activeTicketId ?? null,
|
|
580
580
|
repo_basename: path.basename(options.context.repoRoot),
|
|
581
581
|
branch: options.context.branch,
|
|
582
582
|
storage_bucket: RAW_EVIDENCE_BUCKET,
|
|
@@ -599,8 +599,7 @@ function makeManifest(options) {
|
|
|
599
599
|
function evidenceEntry(options) {
|
|
600
600
|
const digest = sha256(options.bytes);
|
|
601
601
|
const objectKey = remoteObjectKey({
|
|
602
|
-
|
|
603
|
-
workContextId: options.workContextId,
|
|
602
|
+
context: options.context,
|
|
604
603
|
packId: options.packId,
|
|
605
604
|
relativePath: options.relativePath,
|
|
606
605
|
contentAddress: options.contentAddress,
|
|
@@ -661,26 +660,63 @@ function pointerFromEntry(entry) {
|
|
|
661
660
|
};
|
|
662
661
|
}
|
|
663
662
|
/**
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
663
|
+
* Raw evidence keys start with human-readable context, then end in immutable
|
|
664
|
+
* content addresses or pack-relative manifest paths. The local cursor reuses
|
|
665
|
+
* prior content hashes across syncs; the readable date/session folders are for
|
|
666
|
+
* operator debugging and incident response.
|
|
668
667
|
*/
|
|
669
668
|
function remoteObjectKey(options) {
|
|
669
|
+
const namespace = readableEvidenceNamespace(options.context);
|
|
670
670
|
if (options.contentAddress) {
|
|
671
671
|
return posixPath([
|
|
672
|
-
|
|
673
|
-
options.workContextId,
|
|
672
|
+
...namespace,
|
|
674
673
|
options.contentAddress,
|
|
675
674
|
]);
|
|
676
675
|
}
|
|
677
676
|
return posixPath([
|
|
678
|
-
|
|
679
|
-
options.workContextId,
|
|
677
|
+
...namespace,
|
|
680
678
|
options.packId,
|
|
681
679
|
options.relativePath,
|
|
682
680
|
]);
|
|
683
681
|
}
|
|
682
|
+
function readableEvidenceNamespace(context) {
|
|
683
|
+
return [
|
|
684
|
+
"operators",
|
|
685
|
+
operatorSlug(context),
|
|
686
|
+
"repos",
|
|
687
|
+
readableKeySegment(context.repoLabel ?? path.basename(context.repoRoot), "repo"),
|
|
688
|
+
"worktrees",
|
|
689
|
+
readableKeySegment(context.worktreeLabel ?? path.basename(context.repoRoot), "worktree"),
|
|
690
|
+
"tickets",
|
|
691
|
+
readableKeySegment(context.activeTicketId ?? "unbound", "unbound", {
|
|
692
|
+
lowercase: false,
|
|
693
|
+
}),
|
|
694
|
+
"dates",
|
|
695
|
+
context.now.toISOString().slice(0, 10),
|
|
696
|
+
"sessions",
|
|
697
|
+
readableKeySegment(context.sessionId, "session"),
|
|
698
|
+
"ids",
|
|
699
|
+
safeKeySegment(context.operatorId),
|
|
700
|
+
safeKeySegment(context.workContextId),
|
|
701
|
+
];
|
|
702
|
+
}
|
|
703
|
+
function operatorSlug(context) {
|
|
704
|
+
const labelBeforeDomain = (context.operatorLabel ?? context.operatorId)
|
|
705
|
+
.split("@", 1)[0]
|
|
706
|
+
.trim();
|
|
707
|
+
const readable = readableKeySegment(labelBeforeDomain, "operator");
|
|
708
|
+
return `${readable}-${shortHash(context.operatorId).slice(0, 6)}`;
|
|
709
|
+
}
|
|
710
|
+
function readableKeySegment(value, fallback, options = {}) {
|
|
711
|
+
const base = options.lowercase === false ? value : value.toLowerCase();
|
|
712
|
+
const slug = base
|
|
713
|
+
.trim()
|
|
714
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
715
|
+
.replace(/^-+|-+$/g, "")
|
|
716
|
+
.replace(/-{2,}/g, "-")
|
|
717
|
+
.slice(0, 80);
|
|
718
|
+
return slug || fallback;
|
|
719
|
+
}
|
|
684
720
|
function posixPath(parts) {
|
|
685
721
|
return parts.join("/").replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
686
722
|
}
|
package/dist/commands/local.js
CHANGED
|
@@ -862,7 +862,9 @@ async function runStatus(command, io) {
|
|
|
862
862
|
writeLine(io.stdout, `repo: ${status.repo}`);
|
|
863
863
|
writeLine(io.stdout, `branch: ${status.branch}`);
|
|
864
864
|
writeLine(io.stdout, `ticket: ${displayTicketId(status.active_ticket_id)}`);
|
|
865
|
+
writeLine(io.stdout, `work: ${displayWorkLabel(status)}`);
|
|
865
866
|
writeLine(io.stdout, `collector_freshness: ${status.collector_freshness}`);
|
|
867
|
+
writeLine(io.stdout, `collector_version: ${status.collector_version}`);
|
|
866
868
|
writeLine(io.stdout, `upload_state: ${status.upload_state}`);
|
|
867
869
|
writeLine(io.stdout, `last_upload_attempt: ${status.last_upload_attempt_at ?? "never"}`);
|
|
868
870
|
writeLine(io.stdout, `last_upload_success: ${status.last_upload_success_at ?? "never"}`);
|
|
@@ -875,6 +877,11 @@ async function runStatus(command, io) {
|
|
|
875
877
|
function displayTicketId(ticketId) {
|
|
876
878
|
return ticketId ?? "general ambient";
|
|
877
879
|
}
|
|
880
|
+
function displayWorkLabel(status) {
|
|
881
|
+
if (status.work_label && status.work_id)
|
|
882
|
+
return `${status.work_label} (${status.work_id})`;
|
|
883
|
+
return status.work_label ?? status.work_id ?? "no active work context";
|
|
884
|
+
}
|
|
878
885
|
/**
|
|
879
886
|
* Read-only diagnostic: re-runs attribution (no upload, no cursor writes) and
|
|
880
887
|
* prints why each session is or is not collected. Per-session reasons otherwise
|
package/dist/local-state.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { getUserLocalCockpitPaths, LocalCollectorSessionFileSchema, LocalCollectorConfigSchema, LocalUserSessionReferenceSchema, LocalWorkContextSchema, } from "@bli-cockpit/telemetry-core";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
3
4
|
import fs from "node:fs/promises";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { resolveRepoWorktreeIdentity, } from "./repo-identity.js";
|
|
7
8
|
import { summarizeLocalUploadSpool } from "./spool/local-spool.js";
|
|
8
|
-
|
|
9
|
+
const localCollectorPackage = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
10
|
+
export const LOCAL_COLLECTOR_VERSION = typeof localCollectorPackage.version === "string"
|
|
11
|
+
? localCollectorPackage.version
|
|
12
|
+
: "0.0.0";
|
|
9
13
|
export const DEFAULT_DASHBOARD_URL = "https://bli-cockpit-dashboard.vercel.app";
|
|
10
14
|
export function getCollectorRuntimePaths(homeDir = os.homedir()) {
|
|
11
15
|
const paths = getUserLocalCockpitPaths(homeDir);
|
|
@@ -83,7 +87,7 @@ export async function pairLocalCollector(options = {}) {
|
|
|
83
87
|
device_id: deviceId,
|
|
84
88
|
device_name: deviceName,
|
|
85
89
|
claimed_owner_email: claimedOwnerEmail,
|
|
86
|
-
collector_version:
|
|
90
|
+
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
87
91
|
});
|
|
88
92
|
options.onPairStarted?.(startResponse);
|
|
89
93
|
const sessionFile = await pollPairRequest(fetchImpl, dashboardUrl, {
|
|
@@ -218,7 +222,7 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
218
222
|
? "Local user session is valid."
|
|
219
223
|
: "Local user session missing or not paired; upload remains local-only.");
|
|
220
224
|
details.push(context
|
|
221
|
-
? `Active context ${context.work_context_id} last updated ${context.updated_at ?? context.started_at}.`
|
|
225
|
+
? `Active work ${workDisplayLabel(context)} (${context.work_context_id}) last updated ${context.updated_at ?? context.started_at}.`
|
|
222
226
|
: "Active work context missing. Run `cockpit start`.");
|
|
223
227
|
details.push(uploadSpool.last_upload_attempt_at
|
|
224
228
|
? `Last upload attempt: ${uploadSpool.last_upload_attempt_at}.`
|
|
@@ -236,10 +240,14 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
236
240
|
installed: Boolean(config),
|
|
237
241
|
config_file: paths.config_file,
|
|
238
242
|
session_file: paths.session_file,
|
|
243
|
+
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
244
|
+
config_collector_version: config?.collector_version ?? null,
|
|
239
245
|
session_state: session.session_state,
|
|
240
246
|
repo: context?.repo ?? identity.repo_root,
|
|
241
247
|
branch,
|
|
242
248
|
active_ticket_id: context?.active_ticket_id ?? null,
|
|
249
|
+
work_label: context ? workDisplayLabel(context) : null,
|
|
250
|
+
work_id: context?.work_context_id ?? null,
|
|
243
251
|
work_context_id: context?.work_context_id ?? null,
|
|
244
252
|
repo_label: context?.repo_label ?? identity.repo_label,
|
|
245
253
|
repo_fingerprint: context?.repo_fingerprint ?? identity.repo_fingerprint,
|
|
@@ -256,6 +264,11 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
256
264
|
details,
|
|
257
265
|
};
|
|
258
266
|
}
|
|
267
|
+
function workDisplayLabel(context) {
|
|
268
|
+
const repoLabel = (context.repo_label ?? path.basename(context.repo)) || "workspace";
|
|
269
|
+
const worktreeLabel = context.worktree_label ?? repoLabel;
|
|
270
|
+
return `${repoLabel}/${worktreeLabel}`;
|
|
271
|
+
}
|
|
259
272
|
export async function readLocalCollectorConfig(paths) {
|
|
260
273
|
return LocalCollectorConfigSchema.parse(await readJsonFile(paths.config_file));
|
|
261
274
|
}
|
package/dist/upload.js
CHANGED
|
@@ -41,15 +41,18 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
41
41
|
repoLabel,
|
|
42
42
|
now,
|
|
43
43
|
});
|
|
44
|
-
const contextNamespacePrefix = `${session.operator_id}/${uploadContext.work_context_id}/`;
|
|
45
44
|
const skipContentHashes = options.skipContentHashes ??
|
|
46
45
|
new Set(Object.entries(options.cursorObjects ?? {})
|
|
47
|
-
.filter(([, entry]) => entry.object_key
|
|
46
|
+
.filter(([, entry]) => rawEvidenceObjectKeyBelongsToWorkContext(entry.object_key, {
|
|
47
|
+
operatorId: session.operator_id,
|
|
48
|
+
workContextId: uploadContext.work_context_id,
|
|
49
|
+
}))
|
|
48
50
|
.map(([hash]) => hash));
|
|
49
51
|
const sourceCollection = await runLocalSourceCollectors({
|
|
50
52
|
repoRoot,
|
|
51
53
|
branch: uploadContext.branch,
|
|
52
54
|
operatorId: session.operator_id,
|
|
55
|
+
operatorLabel: session.email ?? session.auth_subject_id,
|
|
53
56
|
sessionId: session.session_id,
|
|
54
57
|
workContextId: uploadContext.work_context_id,
|
|
55
58
|
activeWorkContext: activeContext,
|
|
@@ -92,7 +95,7 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
92
95
|
const envelope = TelemetryIngestEnvelopeSchema.parse({
|
|
93
96
|
envelope_version: "telemetry-ingest.v1",
|
|
94
97
|
generated_at: now.toISOString(),
|
|
95
|
-
collector_version:
|
|
98
|
+
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
96
99
|
session_reference: sanitizeSessionReference(session),
|
|
97
100
|
work_context: uploadWorkContext,
|
|
98
101
|
source_scan_results: sanitizeSourceScanResults(sourceCollection.scans, repoLabel),
|
|
@@ -767,4 +770,10 @@ function normalizeDashboardUrl(value) {
|
|
|
767
770
|
function safeRepoLabel(repoRoot) {
|
|
768
771
|
const basename = path.basename(repoRoot.replace(/[\\/]+$/, ""));
|
|
769
772
|
return basename || "repo";
|
|
773
|
+
}
|
|
774
|
+
function rawEvidenceObjectKeyBelongsToWorkContext(objectKey, context) {
|
|
775
|
+
const legacyPrefix = `${context.operatorId}/${context.workContextId}/`;
|
|
776
|
+
const readableIdGuard = `/ids/${context.operatorId}/${context.workContextId}/`;
|
|
777
|
+
return (objectKey.startsWith(legacyPrefix) ||
|
|
778
|
+
(objectKey.startsWith("operators/") && objectKey.includes(readableIdGuard)));
|
|
770
779
|
}
|