@bli-cockpit/cli 0.1.22 → 0.1.24

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.
@@ -0,0 +1,104 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export const COLLECTION_ROOT_REQUIRED = "collection_root_required";
5
+ export async function resolveOnboardingRoots(options) {
6
+ const explicitRoots = normalizeRoots(options.explicitRoots ?? []);
7
+ if (explicitRoots.length > 0) {
8
+ return { roots: explicitRoots, source: "explicit", confirmed: true };
9
+ }
10
+ const savedRoots = normalizeRoots([
11
+ ...(options.savedRoots ?? []),
12
+ ...(options.config?.default_repo_paths ?? []),
13
+ ]);
14
+ if (savedRoots.length > 0) {
15
+ if (!options.interactive) {
16
+ return { roots: savedRoots, source: "saved", confirmed: false };
17
+ }
18
+ const confirmed = await requirePrompt(options).confirm(savedRootsPrompt(savedRoots));
19
+ if (confirmed) {
20
+ return { roots: savedRoots, source: "saved", confirmed: true };
21
+ }
22
+ return promptForRoots(options, "Collection root(s), comma-separated: ");
23
+ }
24
+ if (!options.interactive) {
25
+ throw new Error(`${COLLECTION_ROOT_REQUIRED}: pass --workspace <path> or run cockpit onboard in a terminal to confirm a collection root.`);
26
+ }
27
+ const cwd = path.resolve(options.cwd ?? process.cwd());
28
+ const cwdRoot = likelyBliRootFromCwd(cwd);
29
+ if (cwdRoot) {
30
+ const confirmed = await requirePrompt(options).confirm(`Collect from ${cwdRoot}? [Y/n] `);
31
+ if (confirmed) {
32
+ return { roots: [cwdRoot], source: "cwd_likely_root", confirmed: true };
33
+ }
34
+ }
35
+ const homeRoot = path.join(options.homeDir ?? os.homedir(), "BLI");
36
+ if (await directoryExists(homeRoot)) {
37
+ const confirmed = await requirePrompt(options).confirm(`I found ${homeRoot}. Collect from there? [Y/n] `);
38
+ if (confirmed) {
39
+ return { roots: [path.resolve(homeRoot)], source: "home_likely_root", confirmed: true };
40
+ }
41
+ }
42
+ return promptForRoots(options, "Collection root(s), comma-separated: ");
43
+ }
44
+ export function normalizeRoots(roots) {
45
+ const seen = new Set();
46
+ const normalized = [];
47
+ for (const root of roots) {
48
+ const trimmed = root.trim();
49
+ if (!trimmed)
50
+ continue;
51
+ for (const candidate of splitRootInput(trimmed)) {
52
+ const resolved = path.resolve(candidate);
53
+ if (resolved === path.parse(resolved).root)
54
+ continue;
55
+ if (resolved === os.homedir())
56
+ continue;
57
+ if (seen.has(resolved))
58
+ continue;
59
+ seen.add(resolved);
60
+ normalized.push(resolved);
61
+ }
62
+ }
63
+ return normalized;
64
+ }
65
+ function splitRootInput(value) {
66
+ return value
67
+ .split(/[,\n]/u)
68
+ .map((part) => part.trim())
69
+ .filter(Boolean);
70
+ }
71
+ async function promptForRoots(options, message) {
72
+ const prompt = requirePrompt(options);
73
+ const roots = normalizeRoots([await prompt.input(message)]);
74
+ if (roots.length === 0) {
75
+ throw new Error(`${COLLECTION_ROOT_REQUIRED}: no collection root confirmed; pass --workspace <path>.`);
76
+ }
77
+ return { roots, source: "prompt", confirmed: true };
78
+ }
79
+ function savedRootsPrompt(roots) {
80
+ if (roots.length === 1) {
81
+ return `I'm collecting from ${roots[0]}. Still true? [Y/n] `;
82
+ }
83
+ return [
84
+ "I'm collecting from:",
85
+ ...roots.map((root) => `- ${root}`),
86
+ "Still true? [Y/n] ",
87
+ ].join("\n");
88
+ }
89
+ function requirePrompt(options) {
90
+ if (!options.prompt) {
91
+ throw new Error(`${COLLECTION_ROOT_REQUIRED}: interactive prompt unavailable.`);
92
+ }
93
+ return options.prompt;
94
+ }
95
+ function likelyBliRootFromCwd(cwd) {
96
+ const parts = cwd.split(path.sep).filter(Boolean);
97
+ const index = parts.lastIndexOf("BLI");
98
+ if (index < 0)
99
+ return null;
100
+ return `${path.sep}${parts.slice(0, index + 1).join(path.sep)}`;
101
+ }
102
+ async function directoryExists(dir) {
103
+ return fs.stat(dir).then((stats) => stats.isDirectory(), () => false);
104
+ }
package/dist/upload.js CHANGED
@@ -162,7 +162,7 @@ export async function syncLocalAmbientEnvelope(options = {}) {
162
162
  uploadOutcomes = upload.outcomes;
163
163
  uploadedChunkCount = upload.uploaded_chunk_count;
164
164
  }
165
- const envelope = pruneUndurablePointers(built.envelope, uploadOutcomes);
165
+ const envelope = applyRawEvidenceUploadOutcomes(built.envelope, uploadOutcomes);
166
166
  const response = await fetchImpl(`${built.dashboard_url}/api/ambient/ingest`, {
167
167
  method: "POST",
168
168
  headers: {
@@ -499,6 +499,7 @@ function rawEvidenceSummary(built, outcomes, uploadedChunkCount, cursor) {
499
499
  raw_evidence_uploaded_chunk_count: uploadedChunkCount,
500
500
  raw_evidence_reused_count: cursorReused.length + serverReusedCount,
501
501
  raw_evidence_failed_count: failed.length,
502
+ raw_evidence_sanitized_count: built.raw_evidence_facts?.sanitized_count ?? 0,
502
503
  raw_evidence_failure_reasons: [
503
504
  ...new Set(failed.map((outcome) => outcome.reason ?? "unknown")),
504
505
  ],
@@ -523,32 +524,40 @@ function rawEvidenceSummary(built, outcomes, uploadedChunkCount, cursor) {
523
524
  }
524
525
  /**
525
526
  * Ingest refuses pointers whose objects never became durable, so failed
526
- * uploads are pruned from the envelope instead of failing the whole sync;
527
- * the failures stay visible via reason labels and the server-side ledger.
527
+ * uploads are pruned from the envelope instead of failing the whole sync.
528
+ * Successful upload responses can also carry server-side sanitized hash and
529
+ * redaction metadata; apply those before ingest so refs describe the bytes
530
+ * actually stored in the durable bucket.
528
531
  */
529
- function pruneUndurablePointers(envelope, outcomes) {
532
+ function applyRawEvidenceUploadOutcomes(envelope, outcomes) {
530
533
  // Content-addressed keys mean one pointer id can carry several outcomes
531
534
  // (byte-identical files); the pointer is durable if ANY outcome succeeded.
532
- const durablePointerIds = new Set(outcomes
533
- .filter((outcome) => outcome.upload_state !== "upload_failed")
534
- .map((outcome) => outcome.pointer.raw_evidence_pointer_id));
535
+ const durablePointers = new Map();
536
+ for (const outcome of outcomes) {
537
+ if (outcome.upload_state === "upload_failed")
538
+ continue;
539
+ durablePointers.set(outcome.pointer.raw_evidence_pointer_id, outcome.pointer);
540
+ }
541
+ const durablePointerIds = new Set([...durablePointers.keys()]);
535
542
  const failedPointerIds = new Set(outcomes
536
543
  .filter((outcome) => outcome.upload_state === "upload_failed" &&
537
544
  !durablePointerIds.has(outcome.pointer.raw_evidence_pointer_id))
538
545
  .map((outcome) => outcome.pointer.raw_evidence_pointer_id));
539
546
  const failedOutcomes = outcomes.filter((outcome) => failedPointerIds.has(outcome.pointer.raw_evidence_pointer_id));
540
- if (failedPointerIds.size === 0)
547
+ if (failedPointerIds.size === 0 && durablePointers.size === 0)
541
548
  return envelope;
542
549
  return {
543
550
  ...envelope,
544
551
  events: envelope.events.map((event) => ({
545
552
  ...event,
546
- metrics: {
547
- ...event.metrics,
548
- evidence_failed_count: (event.metrics["evidence_failed_count"] ?? 0) +
549
- failedOutcomes.length,
550
- },
551
- attributes: event.evidence_completeness
553
+ metrics: failedPointerIds.size > 0
554
+ ? {
555
+ ...event.metrics,
556
+ evidence_failed_count: (event.metrics["evidence_failed_count"] ?? 0) +
557
+ failedOutcomes.length,
558
+ }
559
+ : event.metrics,
560
+ attributes: failedPointerIds.size > 0 && event.evidence_completeness
552
561
  ? {
553
562
  ...event.attributes,
554
563
  evidence_completeness_schema_version: event.evidence_completeness.schema_version,
@@ -556,14 +565,21 @@ function pruneUndurablePointers(envelope, outcomes) {
556
565
  evidence_incomplete: true,
557
566
  }
558
567
  : event.attributes,
559
- evidence_completeness: event.evidence_completeness
568
+ evidence_completeness: failedPointerIds.size > 0 && event.evidence_completeness
560
569
  ? markCompletenessUploadFailures(event.evidence_completeness, failedOutcomes)
561
570
  : event.evidence_completeness,
562
- raw_evidence_pointers: event.raw_evidence_pointers.filter((pointer) => !failedPointerIds.has(pointer.raw_evidence_pointer_id)),
563
- redaction: {
564
- ...event.redaction,
565
- raw_evidence_pointer_ids: event.redaction.raw_evidence_pointer_ids.filter((pointerId) => !failedPointerIds.has(pointerId)),
566
- },
571
+ raw_evidence_pointers: event.raw_evidence_pointers.flatMap((pointer) => {
572
+ const pointerId = pointer.raw_evidence_pointer_id;
573
+ if (failedPointerIds.has(pointerId))
574
+ return [];
575
+ return [durablePointers.get(pointerId) ?? pointer];
576
+ }),
577
+ redaction: failedPointerIds.size > 0
578
+ ? {
579
+ ...event.redaction,
580
+ raw_evidence_pointer_ids: event.redaction.raw_evidence_pointer_ids.filter((pointerId) => !failedPointerIds.has(pointerId)),
581
+ }
582
+ : event.redaction,
567
583
  })),
568
584
  };
569
585
  }
@@ -701,6 +717,7 @@ function makeSourceScanCompletedEvent(options) {
701
717
  raw_evidence_file_count: options.rawEvidenceFacts?.file_count ?? 0,
702
718
  raw_evidence_byte_size: options.rawEvidenceFacts?.byte_size ?? 0,
703
719
  raw_evidence_skipped_count: options.rawEvidenceFacts?.skipped_count ?? 0,
720
+ raw_evidence_sanitized_count: options.rawEvidenceFacts?.sanitized_count ?? 0,
704
721
  raw_evidence_reused_count: options.rawEvidenceFacts?.reused_count ?? 0,
705
722
  evidence_scanned_count: evidenceCompleteness?.totals.scanned_count ?? 0,
706
723
  evidence_included_count: evidenceCompleteness?.totals.included_count ?? 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {