@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.
- package/README.md +46 -33
- package/dist/adapters/raw-evidence.js +121 -17
- package/dist/agent-rules.js +49 -22
- package/dist/autostart.js +23 -2
- package/dist/commands/local-args.js +19 -1
- package/dist/commands/local.js +227 -68
- package/dist/commands/public-root.js +4 -3
- package/dist/evidence-upload-client.js +51 -5
- package/dist/local-state.js +20 -3
- package/dist/onboarding-roots.js +104 -0
- package/dist/upload.js +37 -20
- package/package.json +1 -1
|
@@ -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 =
|
|
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
|
-
*
|
|
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
|
|
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
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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.
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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,
|