@bli-cockpit/cli 0.1.3 → 0.1.5

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/upload.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { TelemetryIngestEnvelopeSchema, TelemetryIngestEventDtoSchema, } from "@bli-cockpit/telemetry-core";
2
- import fs from "node:fs/promises";
3
2
  import path from "node:path";
4
- import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, readLocalWorkContext, } from "./local-state.js";
3
+ import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, readLocalWorkContextForRepo, } from "./local-state.js";
5
4
  import { runLocalSourceCollectors } from "./adapters/local-sources.js";
5
+ import { uploadRawEvidenceFilesChunked, } from "./evidence-upload-client.js";
6
+ import { markObjectCommitted, readRawEvidenceCursor, writeRawEvidenceCursor, } from "./cursors/raw-evidence-cursor.js";
6
7
  import { recordUploadBlocked, recordUploadFailure, recordUploadSuccess, } from "./spool/local-spool.js";
7
8
  export class LocalUploadBlockedError extends Error {
8
9
  blocker;
@@ -29,17 +30,22 @@ export async function buildLocalAmbientEnvelope(options = {}) {
29
30
  ? "Collector session expired. Run `cockpit login` or `cockpit pair` again before `cockpit sync`."
30
31
  : "Collector is not paired. Run `cockpit login` or `cockpit pair` before `cockpit sync`.", "cockpit login");
31
32
  }
32
- const activeContext = await readLocalWorkContext(paths).catch(() => {
33
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
34
+ const activeContext = await readLocalWorkContextForRepo(paths, repoRoot).catch(() => {
33
35
  throw new LocalUploadBlockedError("missing_context", "Active work context missing. Run `cockpit start --repo \"$PWD\"` before `cockpit sync`.", "cockpit start --repo \"$PWD\"");
34
36
  });
35
- const repoRoot = path.resolve(options.repoRoot ?? activeContext.repo);
36
- const repoLabel = safeRepoLabel(repoRoot);
37
+ const repoLabel = safeRepoLabel(activeContext.repo_label ?? repoRoot);
37
38
  const uploadContext = makeUploadWorkContext({
38
39
  activeContext,
39
40
  session,
40
41
  repoLabel,
41
42
  now,
42
43
  });
44
+ const contextNamespacePrefix = `${session.operator_id}/${uploadContext.work_context_id}/`;
45
+ const skipContentHashes = options.skipContentHashes ??
46
+ new Set(Object.entries(options.cursorObjects ?? {})
47
+ .filter(([, entry]) => entry.object_key.startsWith(contextNamespacePrefix))
48
+ .map(([hash]) => hash));
43
49
  const sourceCollection = await runLocalSourceCollectors({
44
50
  repoRoot,
45
51
  branch: uploadContext.branch,
@@ -49,6 +55,9 @@ export async function buildLocalAmbientEnvelope(options = {}) {
49
55
  activeWorkContext: activeContext,
50
56
  rawEvidenceStateDir: paths.state_dir,
51
57
  rawEvidenceSessionsDir: path.join(paths.home_dir, ".codex", "sessions"),
58
+ rawEvidenceIncludeCodexJsonl: options.rawEvidenceIncludeCodexJsonl,
59
+ rawEvidenceCodexSessionFiles: options.codexSessionFiles,
60
+ rawEvidenceSkipContentHashes: skipContentHashes,
52
61
  now,
53
62
  });
54
63
  const binding = sourceCollection.binding;
@@ -93,15 +102,21 @@ export async function buildLocalAmbientEnvelope(options = {}) {
93
102
  source_scan_count: envelope.source_scan_results.length,
94
103
  risk_flag_count: safeRiskFlags.length,
95
104
  repo_label: repoLabel,
105
+ head_sha: uploadContext.head_sha ?? null,
96
106
  raw_evidence_upload_files: sourceCollection.facts.raw_evidence?.upload_files ?? [],
107
+ raw_evidence_facts: sourceCollection.facts.raw_evidence,
97
108
  };
98
109
  }
99
110
  export async function syncLocalAmbientEnvelope(options = {}) {
100
111
  const attemptedAt = (options.now ?? new Date()).toISOString();
101
112
  const paths = getCollectorRuntimePaths(options.homeDir);
113
+ const cursor = await readRawEvidenceCursor(paths);
102
114
  let built;
103
115
  try {
104
- built = await buildLocalAmbientEnvelope(options);
116
+ built = await buildLocalAmbientEnvelope({
117
+ ...options,
118
+ cursorObjects: options.cursorObjects ?? cursor.objects,
119
+ });
105
120
  }
106
121
  catch (error) {
107
122
  if (error instanceof LocalUploadBlockedError) {
@@ -116,49 +131,84 @@ export async function syncLocalAmbientEnvelope(options = {}) {
116
131
  if (!fetchImpl) {
117
132
  throw new Error("global fetch is unavailable; use Node.js 20 or newer.");
118
133
  }
119
- let uploadedRawEvidenceObjects = [];
134
+ const provenance = built.envelope.work_context.provenance;
135
+ if (built.raw_evidence_upload_files.length > 0 && !provenance) {
136
+ throw new Error("Raw evidence upload requires collector provenance.");
137
+ }
138
+ let uploadOutcomes = [];
139
+ let uploadedChunkCount = 0;
140
+ let ingestAccepted = false;
120
141
  try {
121
- uploadedRawEvidenceObjects = await uploadRawEvidenceFiles({
122
- fetchImpl,
123
- dashboardUrl: built.dashboard_url,
124
- deviceToken: built.device_token,
125
- envelope: built.envelope,
126
- files: built.raw_evidence_upload_files,
127
- });
142
+ if (built.raw_evidence_upload_files.length > 0 && provenance) {
143
+ const upload = await uploadRawEvidenceFilesChunked({
144
+ fetchImpl,
145
+ dashboardUrl: built.dashboard_url,
146
+ deviceToken: built.device_token,
147
+ provenance,
148
+ generatedAt: built.envelope.generated_at,
149
+ files: built.raw_evidence_upload_files,
150
+ });
151
+ uploadOutcomes = upload.outcomes;
152
+ uploadedChunkCount = upload.uploaded_chunk_count;
153
+ }
154
+ const envelope = pruneUndurablePointers(built.envelope, uploadOutcomes);
128
155
  const response = await fetchImpl(`${built.dashboard_url}/api/ambient/ingest`, {
129
156
  method: "POST",
130
157
  headers: {
131
158
  "Authorization": `Bearer ${built.device_token}`,
132
159
  "Content-Type": "application/json",
133
160
  },
134
- body: JSON.stringify(built.envelope),
161
+ body: JSON.stringify(envelope),
135
162
  });
136
163
  const responseBody = await readResponseJson(response);
137
164
  if (!response.ok) {
138
165
  throw new Error(responseErrorMessage(responseBody, `Ambient ingest failed with HTTP ${response.status}`));
139
166
  }
167
+ ingestAccepted = true;
168
+ for (const outcome of uploadOutcomes) {
169
+ if (outcome.upload_state === "upload_failed")
170
+ continue;
171
+ const contentHash = outcome.pointer.content_hash_sha256;
172
+ if (!contentHash)
173
+ continue;
174
+ markObjectCommitted(cursor, contentHash, {
175
+ object_key: outcome.object_key,
176
+ byte_size: outcome.pointer.byte_size ?? 0,
177
+ committed_at: attemptedAt,
178
+ });
179
+ }
180
+ cursor.updated_at = attemptedAt;
181
+ await writeRawEvidenceCursor(paths, cursor);
140
182
  await recordUploadSuccess(paths, { attemptedAt });
141
183
  return {
142
184
  status: "uploaded",
143
185
  dashboard_url: built.dashboard_url,
144
186
  ticket_id: built.ticket_id,
145
187
  work_context_id: built.envelope.work_context.work_context_id,
188
+ head_sha: built.head_sha,
146
189
  event_count: built.event_count,
147
190
  source_scan_count: built.source_scan_count,
148
191
  risk_flag_count: built.risk_flag_count,
149
- raw_evidence_file_count: built.raw_evidence_upload_files.length,
150
192
  http_status: response.status,
193
+ ...rawEvidenceSummary(built, uploadOutcomes, uploadedChunkCount, cursor),
151
194
  };
152
195
  }
153
196
  catch (error) {
154
197
  const failureReason = error instanceof Error ? error.message : String(error);
155
- const cleanupFailureReason = await cleanupUploadedRawEvidenceObjects({
156
- fetchImpl,
157
- dashboardUrl: built.dashboard_url,
158
- deviceToken: built.device_token,
159
- envelope: built.envelope,
160
- uploadedObjects: uploadedRawEvidenceObjects,
161
- });
198
+ // Once ingest accepted the envelope, the evidence refs are indexed and the
199
+ // objects are load-bearing — a later local write failure (cursor, spool)
200
+ // must not delete durable evidence.
201
+ const cleanupFailureReason = ingestAccepted
202
+ ? null
203
+ : await cleanupUploadedRawEvidenceObjects({
204
+ fetchImpl,
205
+ dashboardUrl: built.dashboard_url,
206
+ deviceToken: built.device_token,
207
+ envelope: built.envelope,
208
+ objectKeys: uploadOutcomes
209
+ .filter((outcome) => outcome.upload_state === "uploaded")
210
+ .map((outcome) => outcome.object_key),
211
+ });
162
212
  const spooledFailureReason = cleanupFailureReason
163
213
  ? `${failureReason}; raw evidence cleanup failed: ${cleanupFailureReason}`
164
214
  : failureReason;
@@ -181,16 +231,163 @@ export async function syncLocalAmbientEnvelope(options = {}) {
181
231
  dashboard_url: built.dashboard_url,
182
232
  ticket_id: built.ticket_id,
183
233
  work_context_id: built.envelope.work_context.work_context_id,
234
+ head_sha: built.head_sha,
184
235
  event_count: built.event_count,
185
236
  source_scan_count: built.source_scan_count,
186
237
  risk_flag_count: built.risk_flag_count,
187
- raw_evidence_file_count: built.raw_evidence_upload_files.length,
188
238
  failure_reason: spooledFailureReason,
189
239
  spool_entry_id: entry.spool_id,
190
240
  retry_command: entry.retry_command,
241
+ ...rawEvidenceSummary(built, uploadOutcomes, uploadedChunkCount, cursor),
191
242
  };
192
243
  }
193
244
  }
245
+ /**
246
+ * Posts a Codex session attribution report using the paired collector
247
+ * credentials and the work context of a representative repo. Failures come
248
+ * back as reason labels so the harvest never hard-fails on reporting.
249
+ */
250
+ export async function postCodexSessionReport(options) {
251
+ if (options.sessions.length === 0) {
252
+ return { posted: false, reason: "no_sessions_observed" };
253
+ }
254
+ const fetchImpl = options.fetch ?? globalThis.fetch;
255
+ if (!fetchImpl) {
256
+ throw new Error("global fetch is unavailable; use Node.js 20 or newer.");
257
+ }
258
+ try {
259
+ const paths = getCollectorRuntimePaths(options.homeDir);
260
+ const config = await readLocalCollectorConfig(paths);
261
+ const sessionFile = await readLocalCollectorSessionFile(paths);
262
+ const session = await readLocalSessionReference(paths);
263
+ if (session.session_state !== "valid") {
264
+ return { posted: false, reason: "collector_not_paired" };
265
+ }
266
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
267
+ const activeContext = await readLocalWorkContextForRepo(paths, repoRoot);
268
+ const repoLabel = safeRepoLabel(activeContext.repo_label ?? repoRoot);
269
+ const provenance = makeCollectorProvenance({
270
+ context: makeUploadWorkContext({
271
+ activeContext,
272
+ session,
273
+ repoLabel,
274
+ now: options.now ?? new Date(),
275
+ }),
276
+ session,
277
+ repoLabel,
278
+ });
279
+ return await reportCodexSessionAttributions({
280
+ fetchImpl,
281
+ dashboardUrl: normalizeDashboardUrl(options.dashboardUrl ?? sessionFile.dashboard_url ?? config.dashboard_url),
282
+ deviceToken: sessionFile.device_token,
283
+ provenance,
284
+ generatedAt: (options.now ?? new Date()).toISOString(),
285
+ sessions: options.sessions,
286
+ });
287
+ }
288
+ catch {
289
+ return { posted: false, reason: "collector_not_ready" };
290
+ }
291
+ }
292
+ /**
293
+ * Reports Codex session attribution outcomes after sync. Non-fatal by design:
294
+ * older dashboards without the endpoint must not fail the harvest, so the
295
+ * caller receives a posted/skipped label instead of an exception.
296
+ */
297
+ export async function reportCodexSessionAttributions(options) {
298
+ if (options.sessions.length === 0) {
299
+ return { posted: false, reason: "no_sessions_observed" };
300
+ }
301
+ try {
302
+ const response = await options.fetchImpl(`${options.dashboardUrl}/api/ambient/codex-sessions`, {
303
+ method: "POST",
304
+ headers: {
305
+ "Authorization": `Bearer ${options.deviceToken}`,
306
+ "Content-Type": "application/json",
307
+ },
308
+ body: JSON.stringify({
309
+ schema_version: "ambient-codex-session-attributions.v1",
310
+ generated_at: options.generatedAt,
311
+ provenance: options.provenance,
312
+ sessions: options.sessions,
313
+ }),
314
+ });
315
+ if (response.status === 404) {
316
+ return { posted: false, reason: "codex_session_api_unavailable" };
317
+ }
318
+ if (!response.ok) {
319
+ return { posted: false, reason: `report_failed_http_${response.status}` };
320
+ }
321
+ return { posted: true, reason: "recorded" };
322
+ }
323
+ catch {
324
+ return { posted: false, reason: "report_network_error" };
325
+ }
326
+ }
327
+ function rawEvidenceSummary(built, outcomes, uploadedChunkCount, cursor) {
328
+ const cursorReused = built.raw_evidence_facts?.reused ?? [];
329
+ const serverReusedCount = outcomes.filter((outcome) => outcome.upload_state === "reused_existing").length;
330
+ const failed = outcomes.filter((outcome) => outcome.upload_state === "upload_failed");
331
+ const cursorReusedOutcomes = cursorReused.map((entry) => ({
332
+ object_key: cursor.objects[entry.content_hash_sha256]?.object_key ?? "",
333
+ raw_evidence_pointer_id: cursor.objects[entry.content_hash_sha256]?.object_key ?? "",
334
+ kind: entry.kind,
335
+ codex_session_id: entry.codex_session_id,
336
+ upload_state: "reused_existing",
337
+ reason: "cursor_content_match",
338
+ }));
339
+ return {
340
+ raw_evidence_file_count: built.raw_evidence_upload_files.length,
341
+ raw_evidence_uploaded_object_count: outcomes.filter((outcome) => outcome.upload_state === "uploaded").length,
342
+ raw_evidence_uploaded_chunk_count: uploadedChunkCount,
343
+ raw_evidence_reused_count: cursorReused.length + serverReusedCount,
344
+ raw_evidence_failed_count: failed.length,
345
+ raw_evidence_failure_reasons: [
346
+ ...new Set(failed.map((outcome) => outcome.reason ?? "unknown")),
347
+ ],
348
+ raw_evidence_outcomes: [
349
+ ...outcomes.map((outcome) => ({
350
+ object_key: outcome.object_key,
351
+ raw_evidence_pointer_id: outcome.pointer.raw_evidence_pointer_id,
352
+ kind: outcome.kind,
353
+ codex_session_id: outcome.codex_session_id,
354
+ upload_state: outcome.upload_state,
355
+ reason: outcome.reason,
356
+ })),
357
+ ...cursorReusedOutcomes,
358
+ ],
359
+ cursor_tracked_object_count: Object.keys(cursor.objects).length,
360
+ };
361
+ }
362
+ /**
363
+ * Ingest refuses pointers whose objects never became durable, so failed
364
+ * uploads are pruned from the envelope instead of failing the whole sync;
365
+ * the failures stay visible via reason labels and the server-side ledger.
366
+ */
367
+ function pruneUndurablePointers(envelope, outcomes) {
368
+ // Content-addressed keys mean one pointer id can carry several outcomes
369
+ // (byte-identical files); the pointer is durable if ANY outcome succeeded.
370
+ const durablePointerIds = new Set(outcomes
371
+ .filter((outcome) => outcome.upload_state !== "upload_failed")
372
+ .map((outcome) => outcome.pointer.raw_evidence_pointer_id));
373
+ const failedPointerIds = new Set(outcomes
374
+ .filter((outcome) => outcome.upload_state === "upload_failed" &&
375
+ !durablePointerIds.has(outcome.pointer.raw_evidence_pointer_id))
376
+ .map((outcome) => outcome.pointer.raw_evidence_pointer_id));
377
+ if (failedPointerIds.size === 0)
378
+ return envelope;
379
+ return {
380
+ ...envelope,
381
+ events: envelope.events.map((event) => ({
382
+ ...event,
383
+ raw_evidence_pointers: event.raw_evidence_pointers.filter((pointer) => !failedPointerIds.has(pointer.raw_evidence_pointer_id)),
384
+ redaction: {
385
+ ...event.redaction,
386
+ raw_evidence_pointer_ids: event.redaction.raw_evidence_pointer_ids.filter((pointerId) => !failedPointerIds.has(pointerId)),
387
+ },
388
+ })),
389
+ };
390
+ }
194
391
  function makeUploadWorkContext(options) {
195
392
  const provenance = makeCollectorProvenance({
196
393
  context: options.activeContext,
@@ -200,6 +397,13 @@ function makeUploadWorkContext(options) {
200
397
  return {
201
398
  ...options.activeContext,
202
399
  repo: options.repoLabel,
400
+ repo_label: options.activeContext.repo_label ?? options.repoLabel,
401
+ repo_fingerprint: options.activeContext.repo_fingerprint,
402
+ repo_origin_url: options.activeContext.repo_origin_url,
403
+ head_sha: options.activeContext.head_sha,
404
+ worktree_label: options.activeContext.worktree_label,
405
+ worktree_fingerprint: options.activeContext.worktree_fingerprint,
406
+ worktree_is_primary: options.activeContext.worktree_is_primary,
203
407
  operator_id: options.session.operator_id,
204
408
  session_id: options.session.session_id,
205
409
  updated_at: options.now.toISOString(),
@@ -252,9 +456,14 @@ function makeSourceScanCompletedEvent(options) {
252
456
  raw_evidence_file_count: options.rawEvidenceFacts?.file_count ?? 0,
253
457
  raw_evidence_byte_size: options.rawEvidenceFacts?.byte_size ?? 0,
254
458
  raw_evidence_skipped_count: options.rawEvidenceFacts?.skipped_count ?? 0,
459
+ raw_evidence_reused_count: options.rawEvidenceFacts?.reused_count ?? 0,
255
460
  },
256
461
  attributes: {
257
462
  repo_label: options.context.repo,
463
+ repo_fingerprint: options.context.repo_fingerprint ?? "unknown",
464
+ worktree_label: options.context.worktree_label ?? "unknown",
465
+ worktree_fingerprint: options.context.worktree_fingerprint ?? "unknown",
466
+ worktree_is_primary: options.context.worktree_is_primary ?? false,
258
467
  branch: options.context.branch,
259
468
  ticket_binding_state: options.binding.state,
260
469
  ticket_binding_source: options.binding.selected_source ?? "none",
@@ -271,78 +480,40 @@ function makeSourceScanCompletedEvent(options) {
271
480
  raw_evidence_pointers: rawEvidencePointers,
272
481
  });
273
482
  }
274
- async function uploadRawEvidenceFiles(options) {
275
- if (options.files.length === 0)
276
- return [];
277
- const provenance = options.envelope.work_context.provenance;
278
- if (!provenance) {
279
- throw new Error("Raw evidence upload requires collector provenance.");
280
- }
281
- const files = await Promise.all(options.files.map(async (file) => ({
282
- pointer: file.pointer,
283
- content_base64: (await fs.readFile(file.local_path)).toString("base64"),
284
- })));
285
- const response = await options.fetchImpl(`${options.dashboardUrl}/api/ambient/evidence/upload`, {
286
- method: "POST",
287
- headers: {
288
- "Authorization": `Bearer ${options.deviceToken}`,
289
- "Content-Type": "application/json",
290
- },
291
- body: JSON.stringify({
292
- schema_version: "ambient-raw-evidence-upload.v1",
293
- generated_at: options.envelope.generated_at,
294
- provenance,
295
- files,
296
- }),
297
- });
298
- const responseBody = await readResponseJson(response);
299
- if (!response.ok) {
300
- throw new Error(responseErrorMessage(responseBody, `Raw evidence upload failed with HTTP ${response.status}`));
301
- }
302
- return readUploadedRawEvidenceObjects(responseBody);
303
- }
483
+ const CLEANUP_BATCH_SIZE = 25;
304
484
  async function cleanupUploadedRawEvidenceObjects(options) {
305
- if (options.uploadedObjects.length === 0)
485
+ if (options.objectKeys.length === 0)
306
486
  return null;
307
487
  const provenance = options.envelope.work_context.provenance;
308
488
  if (!provenance)
309
489
  return "missing collector provenance";
310
- try {
311
- const response = await options.fetchImpl(`${options.dashboardUrl}/api/ambient/evidence/upload`, {
312
- method: "DELETE",
313
- headers: {
314
- "Authorization": `Bearer ${options.deviceToken}`,
315
- "Content-Type": "application/json",
316
- },
317
- body: JSON.stringify({
318
- schema_version: "ambient-raw-evidence-cleanup.v1",
319
- generated_at: options.envelope.generated_at,
320
- provenance,
321
- object_keys: options.uploadedObjects.map((object) => object.object_key),
322
- }),
323
- });
324
- const responseBody = await readResponseJson(response);
325
- if (!response.ok) {
326
- return responseErrorMessage(responseBody, `Raw evidence cleanup failed with HTTP ${response.status}`);
490
+ const failures = [];
491
+ for (let offset = 0; offset < options.objectKeys.length; offset += CLEANUP_BATCH_SIZE) {
492
+ const batch = options.objectKeys.slice(offset, offset + CLEANUP_BATCH_SIZE);
493
+ try {
494
+ const response = await options.fetchImpl(`${options.dashboardUrl}/api/ambient/evidence/upload`, {
495
+ method: "DELETE",
496
+ headers: {
497
+ "Authorization": `Bearer ${options.deviceToken}`,
498
+ "Content-Type": "application/json",
499
+ },
500
+ body: JSON.stringify({
501
+ schema_version: "ambient-raw-evidence-cleanup.v1",
502
+ generated_at: options.envelope.generated_at,
503
+ provenance,
504
+ object_keys: batch,
505
+ }),
506
+ });
507
+ const responseBody = await readResponseJson(response);
508
+ if (!response.ok) {
509
+ failures.push(responseErrorMessage(responseBody, `Raw evidence cleanup failed with HTTP ${response.status}`));
510
+ }
511
+ }
512
+ catch (error) {
513
+ failures.push(error instanceof Error ? error.message : String(error));
327
514
  }
328
- return null;
329
- }
330
- catch (error) {
331
- return error instanceof Error ? error.message : String(error);
332
515
  }
333
- }
334
- function readUploadedRawEvidenceObjects(value) {
335
- if (!value || typeof value !== "object")
336
- return [];
337
- const uploaded = value.uploaded;
338
- if (!Array.isArray(uploaded))
339
- return [];
340
- return uploaded.flatMap((entry) => {
341
- if (!entry || typeof entry !== "object")
342
- return [];
343
- const objectKey = entry.object_key;
344
- return typeof objectKey === "string" && objectKey ? [{ object_key: objectKey }] : [];
345
- });
516
+ return failures.length > 0 ? failures.join("; ") : null;
346
517
  }
347
518
  function makeCollectorProvenance(options) {
348
519
  return {
@@ -351,6 +522,12 @@ function makeCollectorProvenance(options) {
351
522
  collector_version: LOCAL_COLLECTOR_VERSION,
352
523
  repo: options.repoLabel,
353
524
  branch: options.context.branch,
525
+ repo_label: options.context.repo_label ?? options.repoLabel,
526
+ repo_fingerprint: options.context.repo_fingerprint,
527
+ repo_origin_url: options.context.repo_origin_url,
528
+ worktree_label: options.context.worktree_label,
529
+ worktree_fingerprint: options.context.worktree_fingerprint,
530
+ worktree_is_primary: options.context.worktree_is_primary,
354
531
  operator_id: options.session.operator_id,
355
532
  session_id: options.session.session_id,
356
533
  work_context_id: options.context.work_context_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,6 @@
26
26
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
27
27
  },
28
28
  "dependencies": {
29
- "@bli-cockpit/telemetry-core": "0.1.1"
29
+ "@bli-cockpit/telemetry-core": "0.1.3"
30
30
  }
31
31
  }