@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 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.
@@ -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
- operatorId: options.operatorId,
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
- * Codex/Claude JSONL and git diff objects are content-addressed (kind/id/hash)
665
- * so the same content maps to the same remote key across syncs: interrupted
666
- * uploads resume and repeated syncs dedupe server-side. Pack-scoped keys remain
667
- * for the per-sync manifest.
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
- options.operatorId,
673
- options.workContextId,
672
+ ...namespace,
674
673
  options.contentAddress,
675
674
  ]);
676
675
  }
677
676
  return posixPath([
678
- options.operatorId,
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
  }
@@ -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
@@ -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
- export const LOCAL_COLLECTOR_VERSION = "0.1.10";
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: config.collector_version ?? LOCAL_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.startsWith(contextNamespacePrefix))
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: config.collector_version ?? LOCAL_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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {