@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 +47 -11
- 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/autostart.js +4 -1
- package/dist/commands/local.js +16 -1
- package/dist/commands/public-root.js +3 -0
- package/dist/local-state.js +16 -3
- package/dist/upload.js +13 -4
- package/package.json +1 -1
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
|
|
19
|
-
|
|
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
|
|
31
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
85
|
+
cockpit start \
|
|
79
86
|
--ticket <ticket-id> \
|
|
80
87
|
--repo "$PWD"
|
|
81
88
|
|
|
82
|
-
|
|
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
|
|
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.
|
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/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
|
|
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">',
|
package/dist/commands/local.js
CHANGED
|
@@ -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
|
}
|
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
|
@@ -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.
|
|
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
|
|
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
|
}
|