@bli-cockpit/cli 0.1.11 → 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
@@ -15,8 +15,8 @@ Run this from the repo where work will happen:
15
15
 
16
16
  ```bash
17
17
  COCKPIT_DEVICE_NAME="$(scutil --get ComputerName 2>/dev/null || hostname -s)"
18
- npm exec --yes --package=@bli-cockpit/cli@latest -- cockpit onboard \
19
- --dashboard-url <DASHBOARD_URL> \
18
+ npm install -g @bli-cockpit/cli@latest
19
+ cockpit onboard \
20
20
  --email <APPROVED_EMAIL> \
21
21
  --device-name "$COCKPIT_DEVICE_NAME" \
22
22
  --repo "$PWD"
@@ -27,13 +27,20 @@ If a work folder contains multiple repos, run the same command from the parent:
27
27
  ```bash
28
28
  cd ~/Downloads/bluepearl-workspace
29
29
  COCKPIT_DEVICE_NAME="$(scutil --get ComputerName 2>/dev/null || hostname -s)"
30
- npm exec --yes --package=@bli-cockpit/cli@latest -- cockpit onboard \
31
- --dashboard-url <DASHBOARD_URL> \
30
+ npm install -g @bli-cockpit/cli@latest
31
+ cockpit onboard \
32
32
  --email <APPROVED_EMAIL> \
33
33
  --device-name "$COCKPIT_DEVICE_NAME" \
34
34
  --repo "$PWD"
35
35
  ```
36
36
 
37
+ The CLI defaults to the production dashboard. Normal intern/operator setup,
38
+ updates, and syncs omit `--dashboard-url`. Pass `--dashboard-url` only for
39
+ staging, a custom dashboard, or deliberately forcing a different dashboard
40
+ pairing. Already-onboarded users update with
41
+ `npm install -g @bli-cockpit/cli@latest`, then run
42
+ `cockpit sync --repo "$PWD" --json`.
43
+
37
44
  Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
38
45
  repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
39
46
  if the repo cap truncates discovery), creates one stable work context per
@@ -48,7 +55,7 @@ being duplicated across repos or dropped.
48
55
 
49
56
  What happens:
50
57
 
51
- 1. npm downloads the public `@bli-cockpit/cli` package.
58
+ 1. npm installs or updates the public `@bli-cockpit/cli` package.
52
59
  2. `cockpit onboard` writes local user config.
53
60
  3. Cockpit prints a dashboard pairing URL and code.
54
61
  4. Admin approves the pending request from Ambient -> Collector approvals, or
@@ -63,8 +70,8 @@ What happens:
63
70
  `"Savina MacBook"`, `"Box VM 42"`, or the auto-filled macOS computer name.
64
71
  On reused laptops or VMs, keep `--email <APPROVED_EMAIL>` in the command.
65
72
  `cockpit onboard` skips pairing only when the existing valid session belongs to
66
- that same email and dashboard URL; a different email or dashboard forces a new
67
- approval.
73
+ that same email and dashboard URL (the production default unless overridden); a
74
+ different email or dashboard forces a new approval.
68
75
 
69
76
  Before the command runs, an Admin must create or approve the email in the
70
77
  private dashboard. Default launch path is email + temporary password, handed
@@ -75,12 +82,11 @@ separate invite acceptance step.
75
82
  When ticket work starts later:
76
83
 
77
84
  ```bash
78
- npm exec --yes --package=@bli-cockpit/cli@latest -- cockpit start \
85
+ cockpit start \
79
86
  --ticket <ticket-id> \
80
87
  --repo "$PWD"
81
88
 
82
- npm exec --yes --package=@bli-cockpit/cli@latest -- cockpit sync \
83
- --dashboard-url <DASHBOARD_URL> \
89
+ cockpit sync \
84
90
  --repo "$PWD" \
85
91
  --json
86
92
  ```
@@ -92,7 +98,8 @@ Only pass `--ticket` when the work really belongs to a visible ticket.
92
98
 
93
99
  Local files:
94
100
 
95
- - `~/.config/bli-cockpit/config.json`: dashboard URL and local install config.
101
+ - `~/.config/bli-cockpit/config.json`: dashboard URL (production default unless
102
+ overridden) and local install config.
96
103
  - `~/.config/bli-cockpit/session.json`: normal paired device session.
97
104
  - `~/.local/state/bli-cockpit/spool/`: safe retry records when upload fails.
98
105
  - `~/.local/state/bli-cockpit/cursors/raw-evidence.json`: hashes and labels of
@@ -115,6 +122,35 @@ Remote dashboard:
115
122
  - Codex session attribution records (session id, file hash, attribution state
116
123
  and reason labels, scores) accepted by `/api/ambient/codex-sessions`.
117
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
+
118
154
  Never provide Supabase service-role keys, raw DB URLs, root env files, cookies,
119
155
  or deployment tokens to this CLI. The collector must never read env files and
120
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
  }
package/dist/autostart.js CHANGED
@@ -128,7 +128,10 @@ async function existingWatchPaths(homeDir) {
128
128
  function renderPlist(options) {
129
129
  // The repo path is shell-quoted because it lands inside a `/bin/zsh -lc "…"`
130
130
  // command string; the whole command is then XML-escaped for the <string>.
131
- const command = `npm exec --yes --package=@bli-cockpit/cli@latest -- cockpit sync --repo ${shellQuote(options.workDir)} --dashboard-url ${shellQuote(options.dashboardUrl)} --json`;
131
+ const dashboardArg = options.dashboardUrl === DEFAULT_DASHBOARD_URL
132
+ ? ""
133
+ : ` --dashboard-url ${shellQuote(options.dashboardUrl)}`;
134
+ const command = `cockpit sync --repo ${shellQuote(options.workDir)}${dashboardArg} --json`;
132
135
  return [
133
136
  '<?xml version="1.0" encoding="UTF-8"?>',
134
137
  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { createCollectorServer } from "../server.js";
5
5
  import { parseLocalArgs, normalizeUrl } from "./local-args.js";
6
6
  import { autostartStatus, installAutostartAgent, uninstallAutostartAgent } from "../autostart.js";
7
- import { getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext } from "../local-state.js";
7
+ import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext } from "../local-state.js";
8
8
  import { scanAndAttributeCodexSessions } from "../adapters/codex-attribution.js";
9
9
  import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
10
10
  import { acquireSyncLock } from "../sync-lock.js";
@@ -82,6 +82,8 @@ export function localCommandHelp(command) {
82
82
  " cockpit sessions [--source codex|claude] [--repo <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
83
83
  " cockpit serve [--port <port>] [--repo <path>]",
84
84
  " cockpit autostart [install|uninstall|status] [--repo <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
85
+ "",
86
+ `Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
85
87
  ].join("\n");
86
88
  }
87
89
  function localSubcommandHelp(command) {
@@ -93,6 +95,8 @@ function localSubcommandHelp(command) {
93
95
  "",
94
96
  "Installs, pairs, starts work context(s), syncs once, and prints readiness proof.",
95
97
  "If --repo is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
98
+ `Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
99
+ "Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
96
100
  "Run with no flags in a terminal and it prompts for the dashboard email; pass --email to skip the prompt (and on shared/reused machines, where mismatched sessions are re-paired).",
97
101
  ],
98
102
  ],
@@ -102,6 +106,7 @@ function localSubcommandHelp(command) {
102
106
  "Usage: cockpit install [--dashboard-url <url>] [--repo <path>] [--json]",
103
107
  "",
104
108
  "Writes local collector config. Pair with `cockpit login`, then run `cockpit start` when work begins.",
109
+ "Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
105
110
  ],
106
111
  ],
107
112
  [
@@ -110,6 +115,7 @@ function localSubcommandHelp(command) {
110
115
  "Usage: cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
111
116
  "",
112
117
  "Starts dashboard device pairing and stores the approved local session.",
118
+ "Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
113
119
  ],
114
120
  ],
115
121
  [
@@ -136,6 +142,7 @@ function localSubcommandHelp(command) {
136
142
  "Usage: cockpit sync [--repo <path>] [--dashboard-url <url>] [--json]",
137
143
  "",
138
144
  "Uploads latest local ambient envelope(s), or spools safe retries if blocked.",
145
+ "Omit --dashboard-url for normal production sync; pass it only for staging/custom dashboards or forced re-pairing.",
139
146
  "Parent folders sync each child git worktree; Codex AND Claude Code JSONL",
140
147
  "transcripts (and Claude subagent sidecars) are attributed to repos",
141
148
  "deterministically and ambiguous transcripts are retained as unattributed",
@@ -181,6 +188,7 @@ function localSubcommandHelp(command) {
181
188
  "Installs a macOS launchd LaunchAgent that runs `cockpit sync` at login and",
182
189
  "every 30 min (default), surviving reboots — so machines never drift to Stale.",
183
190
  "Action defaults to `install`. `--repo` is the parent work folder to sync.",
191
+ "Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
184
192
  "macOS-only for now; see docs/runbooks/cockpit-launchd-sync.md.",
185
193
  ],
186
194
  ],
@@ -854,7 +862,9 @@ async function runStatus(command, io) {
854
862
  writeLine(io.stdout, `repo: ${status.repo}`);
855
863
  writeLine(io.stdout, `branch: ${status.branch}`);
856
864
  writeLine(io.stdout, `ticket: ${displayTicketId(status.active_ticket_id)}`);
865
+ writeLine(io.stdout, `work: ${displayWorkLabel(status)}`);
857
866
  writeLine(io.stdout, `collector_freshness: ${status.collector_freshness}`);
867
+ writeLine(io.stdout, `collector_version: ${status.collector_version}`);
858
868
  writeLine(io.stdout, `upload_state: ${status.upload_state}`);
859
869
  writeLine(io.stdout, `last_upload_attempt: ${status.last_upload_attempt_at ?? "never"}`);
860
870
  writeLine(io.stdout, `last_upload_success: ${status.last_upload_success_at ?? "never"}`);
@@ -867,6 +877,11 @@ async function runStatus(command, io) {
867
877
  function displayTicketId(ticketId) {
868
878
  return ticketId ?? "general ambient";
869
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
+ }
870
885
  /**
871
886
  * Read-only diagnostic: re-runs attribution (no upload, no cursor writes) and
872
887
  * prints why each session is or is not collected. Per-session reasons otherwise
@@ -22,7 +22,10 @@ function cockpitHelp() {
22
22
  "Usage:",
23
23
  localCommandHelp(),
24
24
  "",
25
+ "Install/update: `npm install -g @bli-cockpit/cli@latest`.",
25
26
  "Intern path: run `cockpit onboard` from the repo root; add `--ticket <id>` only when work already has a ticket.",
27
+ "Already onboarded: run `cockpit sync --repo \"$PWD\" --json`.",
28
+ "Dashboard URL is optional for normal production use; pass `--dashboard-url` only for staging/custom dashboards or forced re-pairing.",
26
29
  "Manual collector path: `install`, `login`, `start [--ticket <id>]`, `sync`, `status`.",
27
30
  ].join("\n");
28
31
  }
@@ -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
@@ -19,7 +19,7 @@ export async function buildLocalAmbientEnvelope(options = {}) {
19
19
  const now = options.now ?? new Date();
20
20
  const paths = getCollectorRuntimePaths(options.homeDir);
21
21
  const config = await readLocalCollectorConfig(paths).catch(() => {
22
- throw new LocalUploadBlockedError("not_installed", "Local collector config missing. Run `cockpit install` before `cockpit sync`.", "cockpit install --dashboard-url <dashboard-url>");
22
+ throw new LocalUploadBlockedError("not_installed", "Local collector config missing. Install/update the CLI, then run `cockpit onboard` before `cockpit sync`.", "npm install -g @bli-cockpit/cli@latest && cockpit onboard --email <email> --repo \"$PWD\"");
23
23
  });
24
24
  const sessionFile = await readLocalCollectorSessionFile(paths).catch(() => {
25
25
  throw new LocalUploadBlockedError("unpaired", "No paired collector session found. Run `cockpit login` or `cockpit pair` before `cockpit sync`.", "cockpit login");
@@ -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.11",
3
+ "version": "0.1.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {