@bli-cockpit/cli 0.1.26 → 0.1.28
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 +12 -5
- package/dist/adapters/codex-attribution.js +164 -75
- package/dist/adapters/local-sources.js +1 -0
- package/dist/adapters/raw-evidence.js +18 -13
- package/dist/commands/local-args.js +42 -3
- package/dist/commands/local.js +238 -3
- package/dist/commands/public-root.js +4 -2
- package/dist/commands/session-sync.js +4 -2
- package/dist/upload.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,11 +45,11 @@ cockpit onboard --email <APPROVED_EMAIL> --workspace ~/BLI --workspace ~/side-pr
|
|
|
45
45
|
The CLI defaults to the production dashboard. Normal intern/operator setup,
|
|
46
46
|
updates, and syncs omit `--dashboard-url`. Pass `--dashboard-url` only for
|
|
47
47
|
staging, a custom dashboard, or deliberately forcing a different dashboard
|
|
48
|
-
pairing. Already-onboarded users update with
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
prompts and the agent ticket-binding guardrail.
|
|
48
|
+
pairing. Already-onboarded users update with `cockpit update` from anywhere. It
|
|
49
|
+
installs the latest public CLI from npm, then reruns onboarding checks to
|
|
50
|
+
refresh pairing, agent rules, autostart, and an initial sync against saved
|
|
51
|
+
roots. `cockpit upgrade` is a compatibility alias. `--repo <path>` remains
|
|
52
|
+
supported for older prompts and the agent ticket-binding guardrail.
|
|
53
53
|
|
|
54
54
|
On machines where Codex or Claude agents will do ticketed work, `cockpit
|
|
55
55
|
onboard` refreshes `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` after harvest
|
|
@@ -191,6 +191,10 @@ Remote dashboard:
|
|
|
191
191
|
for screenshots/images explicitly attached into Codex or Claude sessions;
|
|
192
192
|
- Codex session attribution records (session id, file hash, attribution state
|
|
193
193
|
and reason labels, scores) accepted by `/api/ambient/codex-sessions`.
|
|
194
|
+
The local collector scans both active `~/.codex/sessions` files and archived
|
|
195
|
+
`~/.codex/archived_sessions` files, using only `session_meta` and
|
|
196
|
+
`turn_context` metadata for attribution. Transcript bytes are sanitized or
|
|
197
|
+
blocked later by raw evidence collection before upload.
|
|
194
198
|
|
|
195
199
|
New raw evidence object keys are readable from the Storage browser:
|
|
196
200
|
|
|
@@ -212,6 +216,9 @@ Local attribution preview:
|
|
|
212
216
|
cockpit sessions --workspace "$PWD" --json
|
|
213
217
|
```
|
|
214
218
|
|
|
219
|
+
This preview includes both active and archived Codex sessions in the bounded
|
|
220
|
+
backfill window, plus Claude Code sessions when Claude collection is enabled.
|
|
221
|
+
|
|
215
222
|
Remote metadata path:
|
|
216
223
|
|
|
217
224
|
1. Query `ambient_evidence_refs` by `operator_user_id`, `received_at`, and
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { SECRET_FILE_SEGMENT_PATTERN,
|
|
1
|
+
import { SECRET_FILE_SEGMENT_PATTERN, } from "@bli-cockpit/telemetry-core";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import crypto from "node:crypto";
|
|
2
4
|
import fs from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
6
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
7
|
import { normalizeGitOrigin } from "../repo-identity.js";
|
|
5
|
-
import { sanitizeSessionId, scoreSignalsAgainstWorktrees, sessionIdFromFileName,
|
|
8
|
+
import { sanitizeSessionId, scoreSignalsAgainstWorktrees, sessionIdFromFileName, shortHash, } from "./attribution-core.js";
|
|
6
9
|
/**
|
|
7
10
|
* Deterministic Codex session JSONL -> repo/worktree attribution.
|
|
8
11
|
*
|
|
@@ -18,12 +21,24 @@ import { sanitizeSessionId, scoreSignalsAgainstWorktrees, sessionIdFromFileName,
|
|
|
18
21
|
export { sanitizeSessionId, sessionIdFromFileName };
|
|
19
22
|
export const CODEX_ATTRIBUTION_DEFAULT_SINCE_MINUTES = 24 * 60;
|
|
20
23
|
export const CODEX_ATTRIBUTION_DEFAULT_SESSION_LIMIT = 50;
|
|
24
|
+
export const CODEX_ATTRIBUTION_BACKFILL_SINCE_MINUTES = 14 * 24 * 60;
|
|
25
|
+
export const CODEX_ATTRIBUTION_BACKFILL_SESSION_LIMIT = 500;
|
|
26
|
+
// Compatibility export only. Codex attribution streams full files for metadata
|
|
27
|
+
// and no longer rejects sessions by file size; raw evidence collection enforces
|
|
28
|
+
// upload budgets and content guards later.
|
|
21
29
|
export const CODEX_SESSION_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
30
|
+
const CODEX_ATTRIBUTION_MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
31
|
+
export function defaultCodexSessionDirs(homeDir) {
|
|
32
|
+
return [
|
|
33
|
+
path.join(homeDir, ".codex", "sessions"),
|
|
34
|
+
path.join(homeDir, ".codex", "archived_sessions"),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
22
37
|
export async function scanAndAttributeCodexSessions(options) {
|
|
23
38
|
const sinceMinutes = options.sinceMinutes ?? CODEX_ATTRIBUTION_DEFAULT_SINCE_MINUTES;
|
|
24
39
|
const limit = options.limit ?? CODEX_ATTRIBUTION_DEFAULT_SESSION_LIMIT;
|
|
25
40
|
const cutoffMs = options.now.getTime() - sinceMinutes * 60 * 1000;
|
|
26
|
-
const discovery = await discoverCodexJsonlFiles(options
|
|
41
|
+
const discovery = await discoverCodexJsonlFiles(codexSessionDirsFromOptions(options), cutoffMs);
|
|
27
42
|
const files = discovery.files.slice(0, limit);
|
|
28
43
|
const results = [];
|
|
29
44
|
for (const file of files) {
|
|
@@ -36,7 +51,7 @@ export async function scanAndAttributeCodexSessions(options) {
|
|
|
36
51
|
since_minutes: sinceMinutes,
|
|
37
52
|
session_limit: limit,
|
|
38
53
|
session_limit_applied: discovery.files.length > limit,
|
|
39
|
-
max_file_bytes:
|
|
54
|
+
max_file_bytes: 0,
|
|
40
55
|
directory_read_failed_count: discovery.directoryReadFailedCount,
|
|
41
56
|
stat_failed_count: discovery.statFailedCount,
|
|
42
57
|
secret_path_skipped_count: discovery.secretPathSkippedCount,
|
|
@@ -57,7 +72,8 @@ async function discoverCodexJsonlFiles(dir, cutoffMs) {
|
|
|
57
72
|
let directoryReadFailedCount = 0;
|
|
58
73
|
let statFailedCount = 0;
|
|
59
74
|
let secretPathSkippedCount = 0;
|
|
60
|
-
const stack = [dir];
|
|
75
|
+
const stack = Array.isArray(dir) ? [...dir] : [dir];
|
|
76
|
+
const seenFiles = new Set();
|
|
61
77
|
while (stack.length > 0) {
|
|
62
78
|
const current = stack.pop();
|
|
63
79
|
if (!current)
|
|
@@ -100,7 +116,11 @@ async function discoverCodexJsonlFiles(dir, cutoffMs) {
|
|
|
100
116
|
continue;
|
|
101
117
|
}
|
|
102
118
|
if (stat.mtimeMs >= cutoffMs) {
|
|
103
|
-
|
|
119
|
+
const dedupeKey = await fs.realpath(full).catch(() => path.resolve(full));
|
|
120
|
+
if (!seenFiles.has(dedupeKey)) {
|
|
121
|
+
seenFiles.add(dedupeKey);
|
|
122
|
+
out.push({ file: full, mtimeMs: stat.mtimeMs, byteSize: stat.size });
|
|
123
|
+
}
|
|
104
124
|
}
|
|
105
125
|
}
|
|
106
126
|
}
|
|
@@ -112,6 +132,22 @@ async function discoverCodexJsonlFiles(dir, cutoffMs) {
|
|
|
112
132
|
secretPathSkippedCount,
|
|
113
133
|
};
|
|
114
134
|
}
|
|
135
|
+
function codexSessionDirsFromOptions(options) {
|
|
136
|
+
const dirs = options.sessionsDirs ?? (options.sessionsDir ? [options.sessionsDir] : []);
|
|
137
|
+
return dedupePaths(dirs);
|
|
138
|
+
}
|
|
139
|
+
function dedupePaths(values) {
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const value of values) {
|
|
143
|
+
const resolved = path.resolve(value);
|
|
144
|
+
if (seen.has(resolved))
|
|
145
|
+
continue;
|
|
146
|
+
seen.add(resolved);
|
|
147
|
+
out.push(resolved);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
115
151
|
async function attributeOneSession(file, worktrees) {
|
|
116
152
|
const fileName = path.basename(file.file);
|
|
117
153
|
const base = {
|
|
@@ -128,23 +164,16 @@ async function attributeOneSession(file, worktrees) {
|
|
|
128
164
|
if (isSecretLikePath(fileName)) {
|
|
129
165
|
return skippedResult(base, "secret_like_file_name");
|
|
130
166
|
}
|
|
131
|
-
|
|
132
|
-
return skippedResult(base, "file_too_large");
|
|
133
|
-
}
|
|
134
|
-
let raw;
|
|
167
|
+
let read;
|
|
135
168
|
try {
|
|
136
|
-
|
|
169
|
+
read = await readCodexMetadataSignals(file.file);
|
|
137
170
|
}
|
|
138
171
|
catch {
|
|
139
172
|
return skippedResult(base, "file_read_failed");
|
|
140
173
|
}
|
|
141
|
-
|
|
142
|
-
base.
|
|
143
|
-
|
|
144
|
-
if (containsSecretLikeContent(content)) {
|
|
145
|
-
return skippedResult(base, "secret_like_content_guard");
|
|
146
|
-
}
|
|
147
|
-
const signals = extractCodexSessionSignals(content);
|
|
174
|
+
base.content_hash_sha256 = read.contentHashSha256;
|
|
175
|
+
base.byte_size = read.byteSize;
|
|
176
|
+
const signals = read.signals;
|
|
148
177
|
const metaSessionId = sanitizeSessionId(signals.session_ids[0]);
|
|
149
178
|
if (metaSessionId) {
|
|
150
179
|
base.codex_session_id = metaSessionId;
|
|
@@ -174,72 +203,132 @@ async function attributeOneSession(file, worktrees) {
|
|
|
174
203
|
}, worktrees);
|
|
175
204
|
return { ...base, ...outcome };
|
|
176
205
|
}
|
|
206
|
+
async function readCodexMetadataSignals(filePath) {
|
|
207
|
+
const hash = crypto.createHash("sha256");
|
|
208
|
+
const decoder = new StringDecoder("utf8");
|
|
209
|
+
const state = makeSignalExtractionState();
|
|
210
|
+
const stream = createReadStream(filePath);
|
|
211
|
+
let byteSize = 0;
|
|
212
|
+
let lineBuffer = "";
|
|
213
|
+
let discardingOversizedLine = false;
|
|
214
|
+
for await (const chunk of stream) {
|
|
215
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
216
|
+
byteSize += buffer.byteLength;
|
|
217
|
+
hash.update(buffer);
|
|
218
|
+
const text = decoder.write(buffer);
|
|
219
|
+
const segments = text.split("\n");
|
|
220
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
221
|
+
const segment = segments[index] ?? "";
|
|
222
|
+
const lineEnded = index < segments.length - 1;
|
|
223
|
+
if (discardingOversizedLine) {
|
|
224
|
+
if (lineEnded)
|
|
225
|
+
discardingOversizedLine = false;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
lineBuffer += segment;
|
|
229
|
+
if (Buffer.byteLength(lineBuffer, "utf8") > CODEX_ATTRIBUTION_MAX_LINE_BYTES) {
|
|
230
|
+
state.lineCount += 1;
|
|
231
|
+
state.parseErrorCount += 1;
|
|
232
|
+
lineBuffer = "";
|
|
233
|
+
discardingOversizedLine = !lineEnded;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (lineEnded) {
|
|
237
|
+
absorbCodexSessionLine(state, lineBuffer);
|
|
238
|
+
lineBuffer = "";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const rest = decoder.end();
|
|
243
|
+
if (rest)
|
|
244
|
+
lineBuffer += rest;
|
|
245
|
+
if (lineBuffer.trim())
|
|
246
|
+
absorbCodexSessionLine(state, lineBuffer);
|
|
247
|
+
return {
|
|
248
|
+
signals: codexSignalsFromState(state),
|
|
249
|
+
contentHashSha256: hash.digest("hex"),
|
|
250
|
+
byteSize,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
177
253
|
export function extractCodexSessionSignals(content) {
|
|
178
|
-
const
|
|
179
|
-
const cwds = new Set();
|
|
180
|
-
const workspaceRoots = new Set();
|
|
181
|
-
const branches = new Set();
|
|
182
|
-
const commitHashes = new Set();
|
|
183
|
-
const repositoryUrls = new Set();
|
|
184
|
-
let lineCount = 0;
|
|
185
|
-
let parseErrorCount = 0;
|
|
254
|
+
const state = makeSignalExtractionState();
|
|
186
255
|
for (const line of content.split("\n")) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
256
|
+
absorbCodexSessionLine(state, line);
|
|
257
|
+
}
|
|
258
|
+
return codexSignalsFromState(state);
|
|
259
|
+
}
|
|
260
|
+
function makeSignalExtractionState() {
|
|
261
|
+
return {
|
|
262
|
+
sessionIds: new Set(),
|
|
263
|
+
cwds: new Set(),
|
|
264
|
+
workspaceRoots: new Set(),
|
|
265
|
+
branches: new Set(),
|
|
266
|
+
commitHashes: new Set(),
|
|
267
|
+
repositoryUrls: new Set(),
|
|
268
|
+
lineCount: 0,
|
|
269
|
+
parseErrorCount: 0,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function absorbCodexSessionLine(state, line) {
|
|
273
|
+
if (!line.trim())
|
|
274
|
+
return;
|
|
275
|
+
state.lineCount += 1;
|
|
276
|
+
let record;
|
|
277
|
+
try {
|
|
278
|
+
record = JSON.parse(line);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
state.parseErrorCount += 1;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (!record || typeof record !== "object")
|
|
285
|
+
return;
|
|
286
|
+
const type = record.type;
|
|
287
|
+
if (type !== "session_meta" && type !== "turn_context")
|
|
288
|
+
return;
|
|
289
|
+
const payload = record.payload;
|
|
290
|
+
if (!payload || typeof payload !== "object")
|
|
291
|
+
return;
|
|
292
|
+
const payloadRecord = payload;
|
|
293
|
+
if (type === "session_meta") {
|
|
294
|
+
addString(state.sessionIds, payloadRecord["id"]);
|
|
295
|
+
addString(state.cwds, payloadRecord["cwd"]);
|
|
296
|
+
const git = payloadRecord["git"];
|
|
297
|
+
if (git && typeof git === "object") {
|
|
298
|
+
const gitRecord = git;
|
|
299
|
+
addString(state.branches, gitRecord["branch"] ?? gitRecord["current_branch"]);
|
|
300
|
+
addString(state.commitHashes, gitRecord["commit_hash"]);
|
|
301
|
+
const repositoryUrl = gitRecord["repository_url"];
|
|
302
|
+
if (typeof repositoryUrl === "string" && repositoryUrl.trim()) {
|
|
303
|
+
state.repositoryUrls.add(normalizeGitOrigin(repositoryUrl));
|
|
217
304
|
}
|
|
218
305
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
306
|
+
}
|
|
307
|
+
else if (type === "turn_context") {
|
|
308
|
+
addString(state.cwds, payloadRecord["cwd"]);
|
|
309
|
+
const roots = payloadRecord["workspace_roots"];
|
|
310
|
+
if (Array.isArray(roots)) {
|
|
311
|
+
for (const root of roots) {
|
|
312
|
+
if (typeof root === "string") {
|
|
313
|
+
addString(state.workspaceRoots, root);
|
|
314
|
+
}
|
|
315
|
+
else if (root && typeof root === "object") {
|
|
316
|
+
addString(state.workspaceRoots, root["path"]);
|
|
230
317
|
}
|
|
231
318
|
}
|
|
232
319
|
}
|
|
233
320
|
}
|
|
321
|
+
}
|
|
322
|
+
function codexSignalsFromState(state) {
|
|
234
323
|
return {
|
|
235
|
-
session_ids: [...sessionIds],
|
|
236
|
-
cwds: [...cwds],
|
|
237
|
-
workspace_roots: [...workspaceRoots],
|
|
238
|
-
branches: [...branches],
|
|
239
|
-
commit_hashes: [...commitHashes],
|
|
240
|
-
repository_urls: [...repositoryUrls],
|
|
241
|
-
line_count: lineCount,
|
|
242
|
-
parse_error_count: parseErrorCount,
|
|
324
|
+
session_ids: [...state.sessionIds],
|
|
325
|
+
cwds: [...state.cwds],
|
|
326
|
+
workspace_roots: [...state.workspaceRoots],
|
|
327
|
+
branches: [...state.branches],
|
|
328
|
+
commit_hashes: [...state.commitHashes],
|
|
329
|
+
repository_urls: [...state.repositoryUrls],
|
|
330
|
+
line_count: state.lineCount,
|
|
331
|
+
parse_error_count: state.parseErrorCount,
|
|
243
332
|
};
|
|
244
333
|
}
|
|
245
334
|
function skippedResult(base, reason) {
|
|
@@ -25,6 +25,7 @@ export async function runLocalSourceCollectors(options) {
|
|
|
25
25
|
stateDir: options.rawEvidenceStateDir,
|
|
26
26
|
repoRoot: options.repoRoot,
|
|
27
27
|
sessionsDir: options.rawEvidenceSessionsDir,
|
|
28
|
+
sessionsDirs: options.rawEvidenceSessionsDirs,
|
|
28
29
|
includeCodexJsonl: options.rawEvidenceIncludeCodexJsonl,
|
|
29
30
|
includeClaudeJsonl: options.rawEvidenceIncludeClaudeJsonl,
|
|
30
31
|
codexSessionFiles: options.rawEvidenceCodexSessionFiles,
|
|
@@ -6,6 +6,7 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { makeSourceAdapterIdentity, } from "./common.js";
|
|
8
8
|
import { collectAgentImageEvidenceFromJsonlFile, } from "./agent-image-evidence.js";
|
|
9
|
+
import { defaultCodexSessionDirs, } from "./codex-attribution.js";
|
|
9
10
|
const DEFAULT_SINCE_MINUTES = 24 * 60;
|
|
10
11
|
const DEFAULT_SESSION_LIMIT = 50;
|
|
11
12
|
const MAX_GIT_DIFF_BYTES = 2 * 1024 * 1024;
|
|
@@ -104,6 +105,7 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
104
105
|
await collectCodexJsonlFiles(collection, {
|
|
105
106
|
codexSessionFiles: options.codexSessionFiles,
|
|
106
107
|
sessionsDir: options.sessionsDir,
|
|
108
|
+
sessionsDirs: options.sessionsDirs,
|
|
107
109
|
sinceMinutes,
|
|
108
110
|
limit: sessionLimit,
|
|
109
111
|
});
|
|
@@ -311,11 +313,6 @@ function recordCodexAttributionCompleteness(collection, scan) {
|
|
|
311
313
|
limit: scan.session_limit,
|
|
312
314
|
observed: scan.discovered_file_count,
|
|
313
315
|
applied: scan.session_limit_applied,
|
|
314
|
-
}, {
|
|
315
|
-
source: "codex_attribution",
|
|
316
|
-
cap_type: "max_file_bytes",
|
|
317
|
-
limit: scan.max_file_bytes,
|
|
318
|
-
applied: scan.results.some((result) => result.reason === "file_too_large"),
|
|
319
316
|
});
|
|
320
317
|
recordSkipCount(collection, "codex_attribution", "session_limit_overflow", Math.max(0, scan.discovered_file_count - scan.scanned_file_count));
|
|
321
318
|
recordSkipCount(collection, "codex_attribution", "directory_read_failed", scan.directory_read_failed_count);
|
|
@@ -391,7 +388,10 @@ async function collectCodexJsonlFiles(collection, options) {
|
|
|
391
388
|
filePath: file.local_path,
|
|
392
389
|
codexSessionId: file.codex_session_id,
|
|
393
390
|
}))
|
|
394
|
-
: (await walkJsonlFiles(options.
|
|
391
|
+
: (await walkJsonlFiles(options.sessionsDirs ??
|
|
392
|
+
(options.sessionsDir
|
|
393
|
+
? [options.sessionsDir]
|
|
394
|
+
: defaultCodexSessionDirs(os.homedir())), collection.context.now.getTime() - options.sinceMinutes * 60 * 1000))
|
|
395
395
|
.map((filePath) => ({ filePath, codexSessionId: null }));
|
|
396
396
|
collection.caps.push({
|
|
397
397
|
source: "codex_jsonl",
|
|
@@ -573,10 +573,9 @@ async function collectOneAgentImageFile(collection, options) {
|
|
|
573
573
|
}
|
|
574
574
|
/**
|
|
575
575
|
* Reads, secret-guards, content-addresses, budget-checks, and copies one
|
|
576
|
-
* attributed transcript into the pack.
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
* "optimize" a layer away.
|
|
576
|
+
* attributed transcript into the pack. Attribution only reads metadata records;
|
|
577
|
+
* this collection layer and the server commit layer are the two content guards
|
|
578
|
+
* that decide whether transcript bytes can become durable evidence.
|
|
580
579
|
*/
|
|
581
580
|
async function collectOneEvidenceFile(collection, options) {
|
|
582
581
|
const fileName = path.basename(options.filePath);
|
|
@@ -1075,7 +1074,8 @@ function makeEvidenceCompleteness(collection, options) {
|
|
|
1075
1074
|
}
|
|
1076
1075
|
async function walkJsonlFiles(dir, cutoffMs) {
|
|
1077
1076
|
const out = [];
|
|
1078
|
-
const stack = [dir];
|
|
1077
|
+
const stack = Array.isArray(dir) ? [...dir] : [dir];
|
|
1078
|
+
const seen = new Set();
|
|
1079
1079
|
while (stack.length > 0) {
|
|
1080
1080
|
const current = stack.pop();
|
|
1081
1081
|
if (!current || isSecretLikePath(current))
|
|
@@ -1098,8 +1098,13 @@ async function walkJsonlFiles(dir, cutoffMs) {
|
|
|
1098
1098
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
|
|
1099
1099
|
continue;
|
|
1100
1100
|
const stat = await fs.stat(full);
|
|
1101
|
-
if (stat.mtimeMs >= cutoffMs)
|
|
1102
|
-
|
|
1101
|
+
if (stat.mtimeMs >= cutoffMs) {
|
|
1102
|
+
const dedupeKey = await fs.realpath(full).catch(() => path.resolve(full));
|
|
1103
|
+
if (!seen.has(dedupeKey)) {
|
|
1104
|
+
seen.add(dedupeKey);
|
|
1105
|
+
out.push({ file: full, mtimeMs: stat.mtimeMs });
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1103
1108
|
}
|
|
1104
1109
|
}
|
|
1105
1110
|
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -12,6 +12,9 @@ export function parseLocalArgs(argv) {
|
|
|
12
12
|
switch (command) {
|
|
13
13
|
case "onboard":
|
|
14
14
|
return parseOnboardArgs(argv.slice(1));
|
|
15
|
+
case "update":
|
|
16
|
+
case "upgrade":
|
|
17
|
+
return parseUpdateArgs(command, argv.slice(1));
|
|
15
18
|
case "install":
|
|
16
19
|
return parseInstallArgs(argv.slice(1));
|
|
17
20
|
case "login":
|
|
@@ -33,11 +36,13 @@ export function parseLocalArgs(argv) {
|
|
|
33
36
|
return parseAutostartArgs(argv.slice(1));
|
|
34
37
|
case "agent-rules":
|
|
35
38
|
return parseAgentRulesArgs(argv.slice(1));
|
|
39
|
+
case "release":
|
|
40
|
+
return parseReleaseArgs(argv.slice(1));
|
|
36
41
|
default:
|
|
37
42
|
throw new Error(`Unknown local command: ${command ?? ""}`);
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
|
-
function
|
|
45
|
+
function parseOnboardLikeArgs(args, command) {
|
|
41
46
|
const values = parseNamedArgs(args, {
|
|
42
47
|
allowedFlags: [
|
|
43
48
|
"--home",
|
|
@@ -69,13 +74,13 @@ function parseOnboardArgs(args) {
|
|
|
69
74
|
"--max-repos",
|
|
70
75
|
],
|
|
71
76
|
});
|
|
72
|
-
assertNoPositionals(values.positionals,
|
|
77
|
+
assertNoPositionals(values.positionals, command);
|
|
73
78
|
return {
|
|
74
|
-
kind: "onboard",
|
|
75
79
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
76
80
|
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
77
81
|
collectionRoots: optionalNonEmptyList(workRootFlagValues(values)),
|
|
78
82
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
83
|
+
dashboardUrlExplicit: values.flags.has("--dashboard-url"),
|
|
79
84
|
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
80
85
|
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
81
86
|
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
@@ -87,6 +92,16 @@ function parseOnboardArgs(args) {
|
|
|
87
92
|
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
88
93
|
};
|
|
89
94
|
}
|
|
95
|
+
function parseOnboardArgs(args) {
|
|
96
|
+
return { kind: "onboard", ...parseOnboardLikeArgs(args, "onboard") };
|
|
97
|
+
}
|
|
98
|
+
function parseUpdateArgs(alias, args) {
|
|
99
|
+
return {
|
|
100
|
+
kind: "update",
|
|
101
|
+
alias,
|
|
102
|
+
...parseOnboardLikeArgs(args, alias),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
90
105
|
function parseInstallArgs(args) {
|
|
91
106
|
const values = parseNamedArgs(args, {
|
|
92
107
|
allowedFlags: [
|
|
@@ -115,6 +130,30 @@ function parseInstallArgs(args) {
|
|
|
115
130
|
json: values.booleans.has("--json"),
|
|
116
131
|
};
|
|
117
132
|
}
|
|
133
|
+
function parseReleaseArgs(args) {
|
|
134
|
+
const values = parseNamedArgs(args, {
|
|
135
|
+
allowedFlags: [
|
|
136
|
+
"--dry-run",
|
|
137
|
+
"--tag",
|
|
138
|
+
"--access",
|
|
139
|
+
"--otp",
|
|
140
|
+
"--skip-checks",
|
|
141
|
+
],
|
|
142
|
+
valueFlags: ["--tag", "--access", "--otp"],
|
|
143
|
+
});
|
|
144
|
+
assertNoPositionals(values.positionals, "release");
|
|
145
|
+
const releaseArgs = [];
|
|
146
|
+
if (values.booleans.has("--dry-run"))
|
|
147
|
+
releaseArgs.push("--dry-run");
|
|
148
|
+
if (values.booleans.has("--skip-checks"))
|
|
149
|
+
releaseArgs.push("--skip-checks");
|
|
150
|
+
for (const flag of ["--tag", "--access", "--otp"]) {
|
|
151
|
+
const value = values.flags.get(flag);
|
|
152
|
+
if (value !== undefined)
|
|
153
|
+
releaseArgs.push(flag, value);
|
|
154
|
+
}
|
|
155
|
+
return { kind: "release", args: releaseArgs };
|
|
156
|
+
}
|
|
118
157
|
function parseLoginArgs(args) {
|
|
119
158
|
const values = parseNamedArgs(args, {
|
|
120
159
|
allowedFlags: [
|
package/dist/commands/local.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { createCollectorServer } from "../server.js";
|
|
@@ -6,7 +7,7 @@ import { inspectAgentRules, installAgentRules, uninstallAgentRules, } from "../a
|
|
|
6
7
|
import { parseLocalArgs, normalizeUrl } from "./local-args.js";
|
|
7
8
|
import { autostartStatus, installAutostartAgent, uninstallAutostartAgent, } from "../autostart.js";
|
|
8
9
|
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
|
|
9
|
-
import { scanAndAttributeCodexSessions } from "../adapters/codex-attribution.js";
|
|
10
|
+
import { CODEX_ATTRIBUTION_BACKFILL_SESSION_LIMIT, CODEX_ATTRIBUTION_BACKFILL_SINCE_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
10
11
|
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
11
12
|
import { acquireSyncLock } from "../sync-lock.js";
|
|
12
13
|
import { discoverGitWorktrees } from "../repo-identity.js";
|
|
@@ -14,6 +15,8 @@ import { runAttributedWorktreeSync, } from "./session-sync.js";
|
|
|
14
15
|
import { COLLECTION_ROOT_REQUIRED, resolveOnboardingRoots, } from "../onboarding-roots.js";
|
|
15
16
|
export const rootCommandNames = new Set([
|
|
16
17
|
"onboard",
|
|
18
|
+
"update",
|
|
19
|
+
"upgrade",
|
|
17
20
|
"install",
|
|
18
21
|
"login",
|
|
19
22
|
"pair",
|
|
@@ -25,6 +28,7 @@ export const rootCommandNames = new Set([
|
|
|
25
28
|
"serve",
|
|
26
29
|
"autostart",
|
|
27
30
|
"agent-rules",
|
|
31
|
+
"release",
|
|
28
32
|
]);
|
|
29
33
|
export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
30
34
|
if (isLocalHelpRequest(argv)) {
|
|
@@ -47,6 +51,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
47
51
|
return await runInstall(command, io);
|
|
48
52
|
case "onboard":
|
|
49
53
|
return await runOnboard(command, io);
|
|
54
|
+
case "update":
|
|
55
|
+
return await runUpdate(command, io);
|
|
50
56
|
case "login":
|
|
51
57
|
return await runLogin(command, io);
|
|
52
58
|
case "logout":
|
|
@@ -65,6 +71,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
65
71
|
return await runAutostart(command, io);
|
|
66
72
|
case "agent-rules":
|
|
67
73
|
return await runAgentRules(command, io);
|
|
74
|
+
case "release":
|
|
75
|
+
return await runRelease(command, io);
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
catch (error) {
|
|
@@ -77,6 +85,8 @@ export function localCommandHelp(command) {
|
|
|
77
85
|
return localSubcommandHelp(command);
|
|
78
86
|
return [
|
|
79
87
|
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
88
|
+
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
89
|
+
" cockpit upgrade [same flags as update]",
|
|
80
90
|
" cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
81
91
|
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
82
92
|
" cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
@@ -88,6 +98,7 @@ export function localCommandHelp(command) {
|
|
|
88
98
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
89
99
|
" cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
90
100
|
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
101
|
+
" cockpit release [--dry-run] [--skip-checks] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
91
102
|
"",
|
|
92
103
|
`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.`,
|
|
93
104
|
].join("\n");
|
|
@@ -118,6 +129,25 @@ function localSubcommandHelp(command) {
|
|
|
118
129
|
"Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
|
|
119
130
|
],
|
|
120
131
|
],
|
|
132
|
+
[
|
|
133
|
+
"update",
|
|
134
|
+
[
|
|
135
|
+
"Usage: cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
136
|
+
"",
|
|
137
|
+
"Updates the global public CLI from npm, then reruns `cockpit onboard`",
|
|
138
|
+
"with the same setup flags so pairing, saved roots, agent rules,",
|
|
139
|
+
"autostart, and the initial sync are refreshed in one command.",
|
|
140
|
+
"`cockpit upgrade` is an alias.",
|
|
141
|
+
],
|
|
142
|
+
],
|
|
143
|
+
[
|
|
144
|
+
"upgrade",
|
|
145
|
+
[
|
|
146
|
+
"Usage: cockpit upgrade [same flags as cockpit update]",
|
|
147
|
+
"",
|
|
148
|
+
"Alias for `cockpit update`.",
|
|
149
|
+
],
|
|
150
|
+
],
|
|
121
151
|
[
|
|
122
152
|
"login",
|
|
123
153
|
[
|
|
@@ -222,6 +252,17 @@ function localSubcommandHelp(command) {
|
|
|
222
252
|
"Action defaults to `install`.",
|
|
223
253
|
],
|
|
224
254
|
],
|
|
255
|
+
[
|
|
256
|
+
"release",
|
|
257
|
+
[
|
|
258
|
+
"Usage: cockpit release [--dry-run] [--skip-checks] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
259
|
+
"",
|
|
260
|
+
"Maintainer-only helper. Run from inside the bli-cockpit repo checkout.",
|
|
261
|
+
"Requires a clean `main` branch and runs `git pull --ff-only` before publishing.",
|
|
262
|
+
"Delegates to `npm run publish:public -- ...` so public packages are",
|
|
263
|
+
"built, checked, and published in the safe telemetry-core then CLI order.",
|
|
264
|
+
],
|
|
265
|
+
],
|
|
225
266
|
]);
|
|
226
267
|
return (helpByCommand.get(command) ?? [localCommandHelp()]).join("\n");
|
|
227
268
|
}
|
|
@@ -244,6 +285,186 @@ async function runInstall(command, io) {
|
|
|
244
285
|
writeLine(io.stdout, "Next: run `cockpit login`, then `cockpit start` inside the repo; add `--ticket <id>` only when ticket work begins.");
|
|
245
286
|
return 0;
|
|
246
287
|
}
|
|
288
|
+
async function runUpdate(command, io) {
|
|
289
|
+
const exec = io.exec ?? defaultExec();
|
|
290
|
+
const installArgs = [
|
|
291
|
+
"install",
|
|
292
|
+
"-g",
|
|
293
|
+
"@bli-cockpit/cli@latest",
|
|
294
|
+
"--prefer-online",
|
|
295
|
+
];
|
|
296
|
+
if (!command.json) {
|
|
297
|
+
writeLine(io.stdout, "Updating Cockpit CLI from npm...");
|
|
298
|
+
}
|
|
299
|
+
const install = await exec("npm", installArgs);
|
|
300
|
+
writeExecOutput(io, install, { stdout: !command.json, stderr: true });
|
|
301
|
+
if (install.code !== 0) {
|
|
302
|
+
if (command.json) {
|
|
303
|
+
writeLine(io.stdout, JSON.stringify({
|
|
304
|
+
status: "blocked",
|
|
305
|
+
step: "npm_install",
|
|
306
|
+
command: `npm ${installArgs.join(" ")}`,
|
|
307
|
+
exit_code: install.code,
|
|
308
|
+
}, null, 2));
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
writeLine(io.stderr, "BLOCKED: npm install failed; Cockpit CLI was not refreshed.");
|
|
312
|
+
}
|
|
313
|
+
return install.code || 1;
|
|
314
|
+
}
|
|
315
|
+
if (!command.json) {
|
|
316
|
+
writeLine(io.stdout, "Cockpit CLI updated. Rechecking onboarding...");
|
|
317
|
+
}
|
|
318
|
+
const onboard = await exec("cockpit", [
|
|
319
|
+
"onboard",
|
|
320
|
+
...updateOnboardArgs(command),
|
|
321
|
+
]);
|
|
322
|
+
writeExecOutput(io, onboard, { stdout: true, stderr: true });
|
|
323
|
+
return onboard.code;
|
|
324
|
+
}
|
|
325
|
+
async function runRelease(command, io) {
|
|
326
|
+
const releaseRoot = await findPublicReleaseRoot(process.cwd());
|
|
327
|
+
if (!releaseRoot) {
|
|
328
|
+
writeLine(io.stderr, "cockpit release must be run inside the bli-cockpit repo checkout (missing publish:public script).");
|
|
329
|
+
return 1;
|
|
330
|
+
}
|
|
331
|
+
const exec = io.exec ?? defaultExec();
|
|
332
|
+
const gitReady = await prepareReleaseMainBranch(releaseRoot, exec, io);
|
|
333
|
+
if (!gitReady)
|
|
334
|
+
return 1;
|
|
335
|
+
writeLine(io.stdout, "Running Cockpit public package release...");
|
|
336
|
+
const npmArgs = ["--prefix", releaseRoot, "run", "publish:public"];
|
|
337
|
+
if (command.args.length > 0)
|
|
338
|
+
npmArgs.push("--", ...command.args);
|
|
339
|
+
const releaseExec = io.interactiveExec ?? defaultInteractiveExec();
|
|
340
|
+
const result = await releaseExec("npm", npmArgs);
|
|
341
|
+
writeExecOutput(io, result, { stdout: true, stderr: true });
|
|
342
|
+
return result.code;
|
|
343
|
+
}
|
|
344
|
+
async function prepareReleaseMainBranch(releaseRoot, exec, io) {
|
|
345
|
+
const branch = await exec("git", [
|
|
346
|
+
"-C",
|
|
347
|
+
releaseRoot,
|
|
348
|
+
"rev-parse",
|
|
349
|
+
"--abbrev-ref",
|
|
350
|
+
"HEAD",
|
|
351
|
+
]);
|
|
352
|
+
writeExecOutput(io, branch, { stdout: false, stderr: true });
|
|
353
|
+
if (branch.code !== 0) {
|
|
354
|
+
writeLine(io.stderr, "BLOCKED: cockpit release could not read the current git branch.");
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
const currentBranch = branch.stdout.trim();
|
|
358
|
+
if (currentBranch !== "main") {
|
|
359
|
+
writeLine(io.stderr, `BLOCKED: cockpit release only publishes from main. Current branch is ${currentBranch || "unknown"}.`);
|
|
360
|
+
writeLine(io.stderr, "Merge the release changes, switch to main, then rerun `cockpit release`.");
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
const status = await exec("git", [
|
|
364
|
+
"-C",
|
|
365
|
+
releaseRoot,
|
|
366
|
+
"status",
|
|
367
|
+
"--porcelain",
|
|
368
|
+
]);
|
|
369
|
+
writeExecOutput(io, status, { stdout: false, stderr: true });
|
|
370
|
+
if (status.code !== 0) {
|
|
371
|
+
writeLine(io.stderr, "BLOCKED: cockpit release could not inspect git status.");
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
if (status.stdout.trim()) {
|
|
375
|
+
writeLine(io.stderr, "BLOCKED: cockpit release requires a clean main checkout.");
|
|
376
|
+
writeLine(io.stderr, "Commit or discard local changes, then rerun `cockpit release`.");
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
writeLine(io.stdout, "Syncing main with git pull --ff-only...");
|
|
380
|
+
const pull = await exec("git", ["-C", releaseRoot, "pull", "--ff-only"]);
|
|
381
|
+
writeExecOutput(io, pull, { stdout: true, stderr: true });
|
|
382
|
+
if (pull.code !== 0) {
|
|
383
|
+
writeLine(io.stderr, "BLOCKED: git pull --ff-only failed; main is not safely current.");
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
async function findPublicReleaseRoot(startDir) {
|
|
389
|
+
let current = path.resolve(startDir);
|
|
390
|
+
while (true) {
|
|
391
|
+
const packageJsonPath = path.join(current, "package.json");
|
|
392
|
+
try {
|
|
393
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
394
|
+
if (packageJson.scripts?.["publish:public"] !== undefined)
|
|
395
|
+
return current;
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// Keep walking: nested packages may be missing package.json or have one
|
|
399
|
+
// without the release script.
|
|
400
|
+
}
|
|
401
|
+
const parent = path.dirname(current);
|
|
402
|
+
if (parent === current)
|
|
403
|
+
return null;
|
|
404
|
+
current = parent;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function updateOnboardArgs(command) {
|
|
408
|
+
const args = [];
|
|
409
|
+
if (command.homeDir)
|
|
410
|
+
args.push("--home", command.homeDir);
|
|
411
|
+
for (const root of updateCollectionRoots(command)) {
|
|
412
|
+
args.push("--workspace", root);
|
|
413
|
+
}
|
|
414
|
+
if (command.dashboardUrlExplicit) {
|
|
415
|
+
args.push("--dashboard-url", command.dashboardUrl);
|
|
416
|
+
}
|
|
417
|
+
if (command.claimedOwnerEmail)
|
|
418
|
+
args.push("--email", command.claimedOwnerEmail);
|
|
419
|
+
if (command.deviceName)
|
|
420
|
+
args.push("--device-name", command.deviceName);
|
|
421
|
+
if (command.activeTicketId)
|
|
422
|
+
args.push("--ticket", command.activeTicketId);
|
|
423
|
+
if (command.branch)
|
|
424
|
+
args.push("--branch", command.branch);
|
|
425
|
+
if (command.pollIntervalMs !== undefined) {
|
|
426
|
+
args.push("--poll-interval-ms", String(command.pollIntervalMs));
|
|
427
|
+
}
|
|
428
|
+
if (command.timeoutMs !== undefined) {
|
|
429
|
+
args.push("--timeout-ms", String(command.timeoutMs));
|
|
430
|
+
}
|
|
431
|
+
if (command.maxDepth !== undefined)
|
|
432
|
+
args.push("--max-depth", String(command.maxDepth));
|
|
433
|
+
if (command.maxRepos !== undefined)
|
|
434
|
+
args.push("--max-repos", String(command.maxRepos));
|
|
435
|
+
if (command.json)
|
|
436
|
+
args.push("--json");
|
|
437
|
+
return args;
|
|
438
|
+
}
|
|
439
|
+
function updateCollectionRoots(command) {
|
|
440
|
+
const roots = command.collectionRoots?.length
|
|
441
|
+
? command.collectionRoots
|
|
442
|
+
: command.repoRoot
|
|
443
|
+
? [command.repoRoot]
|
|
444
|
+
: [];
|
|
445
|
+
const seen = new Set();
|
|
446
|
+
const deduped = [];
|
|
447
|
+
for (const root of roots) {
|
|
448
|
+
if (seen.has(root))
|
|
449
|
+
continue;
|
|
450
|
+
seen.add(root);
|
|
451
|
+
deduped.push(root);
|
|
452
|
+
}
|
|
453
|
+
return deduped;
|
|
454
|
+
}
|
|
455
|
+
function writeExecOutput(io, result, options) {
|
|
456
|
+
if (options.stdout)
|
|
457
|
+
writeRaw(io.stdout, result.stdout);
|
|
458
|
+
if (options.stderr)
|
|
459
|
+
writeRaw(io.stderr, result.stderr);
|
|
460
|
+
}
|
|
461
|
+
function writeRaw(stream, text) {
|
|
462
|
+
if (!text)
|
|
463
|
+
return;
|
|
464
|
+
stream.write(text);
|
|
465
|
+
if (!text.endsWith("\n"))
|
|
466
|
+
stream.write("\n");
|
|
467
|
+
}
|
|
247
468
|
function isInteractiveStdin(io) {
|
|
248
469
|
return Boolean(io.stdin.isTTY);
|
|
249
470
|
}
|
|
@@ -1143,9 +1364,11 @@ async function runSessions(command, io) {
|
|
|
1143
1364
|
const wantClaude = command.source !== "codex";
|
|
1144
1365
|
const codex = wantCodex
|
|
1145
1366
|
? await scanAndAttributeCodexSessions({
|
|
1146
|
-
|
|
1367
|
+
sessionsDirs: defaultCodexSessionDirs(homeDir),
|
|
1147
1368
|
worktrees,
|
|
1148
1369
|
now,
|
|
1370
|
+
sinceMinutes: CODEX_ATTRIBUTION_BACKFILL_SINCE_MINUTES,
|
|
1371
|
+
limit: CODEX_ATTRIBUTION_BACKFILL_SESSION_LIMIT,
|
|
1149
1372
|
})
|
|
1150
1373
|
: null;
|
|
1151
1374
|
const claude = wantClaude
|
|
@@ -1341,6 +1564,17 @@ function defaultExec() {
|
|
|
1341
1564
|
});
|
|
1342
1565
|
});
|
|
1343
1566
|
}
|
|
1567
|
+
function defaultInteractiveExec() {
|
|
1568
|
+
return (cmd, args) => new Promise((resolve) => {
|
|
1569
|
+
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
1570
|
+
child.on("error", (error) => {
|
|
1571
|
+
resolve({ code: 1, stdout: "", stderr: errorMessage(error) });
|
|
1572
|
+
});
|
|
1573
|
+
child.on("close", (code) => {
|
|
1574
|
+
resolve({ code: code ?? 1, stdout: "", stderr: "" });
|
|
1575
|
+
});
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1344
1578
|
function defaultIo() {
|
|
1345
1579
|
if (!globalThis.fetch) {
|
|
1346
1580
|
throw new Error("global fetch is unavailable; use Node.js 20 or newer.");
|
|
@@ -1352,6 +1586,7 @@ function defaultIo() {
|
|
|
1352
1586
|
env: process.env,
|
|
1353
1587
|
fetch: globalThis.fetch.bind(globalThis),
|
|
1354
1588
|
exec: defaultExec(),
|
|
1589
|
+
interactiveExec: defaultInteractiveExec(),
|
|
1355
1590
|
};
|
|
1356
1591
|
}
|
|
1357
1592
|
function writeLine(stream, text) {
|
|
@@ -22,13 +22,15 @@ function cockpitHelp() {
|
|
|
22
22
|
"Usage:",
|
|
23
23
|
localCommandHelp(),
|
|
24
24
|
"",
|
|
25
|
-
"Install
|
|
25
|
+
"Install: `npm install -g @bli-cockpit/cli@latest`.",
|
|
26
|
+
"Update: run `cockpit update` to refresh the global CLI and rerun onboarding checks.",
|
|
26
27
|
"Intern path: run `cockpit onboard`; it confirms a `/BLI` collection root before syncing.",
|
|
27
28
|
"Headless/reused laptop path: `cockpit onboard --email <email> --workspace ~/BLI`.",
|
|
28
|
-
"Already onboarded:
|
|
29
|
+
"Already onboarded: run `cockpit update` from anywhere to refresh pairing, roots, agent rules, autostart, and sync.",
|
|
29
30
|
"Agent setup: `cockpit onboard` refreshes AGENTS.md/CLAUDE.md rules; use `cockpit agent-rules install --workspace ~/BLI` for repair.",
|
|
30
31
|
"Dashboard URL is optional for normal production use; pass `--dashboard-url` only for staging/custom dashboards or forced re-pairing.",
|
|
31
32
|
"Manual collector path: `install`, `login`, `start [--ticket <id>] [--topic <label>] [--intent <intent>] [--phase <phase>]`, `sync`, `status`, `agent-rules`.",
|
|
33
|
+
"Maintainer release path: merge to main first, then run `cockpit release --dry-run` and `cockpit release` from a clean main checkout.",
|
|
32
34
|
].join("\n");
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -7,7 +7,7 @@ import os from "node:os";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { getCollectorRuntimePaths, startLocalWorkContext, readLocalCollectorConfig } from "../local-state.js";
|
|
9
9
|
import { LocalUploadBlockedError, postCodexSessionReport, syncLocalAmbientEnvelope } from "../upload.js";
|
|
10
|
-
import { scanAndAttributeCodexSessions } from "../adapters/codex-attribution.js";
|
|
10
|
+
import { CODEX_ATTRIBUTION_BACKFILL_SESSION_LIMIT, CODEX_ATTRIBUTION_BACKFILL_SINCE_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
11
11
|
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
12
12
|
import { RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET } from "../adapters/raw-evidence.js";
|
|
13
13
|
import { CLAUDE_CURSOR_FILENAME, countStaleSessions, emptyRawEvidenceCursorState, readRawEvidenceCursor, recordSessionObservation, writeRawEvidenceCursor } from "../cursors/raw-evidence-cursor.js";
|
|
@@ -30,9 +30,11 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
30
30
|
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
31
31
|
const claudeEnabled = await isClaudeCollectionEnabled(paths);
|
|
32
32
|
const codexAttribution = await scanAndAttributeCodexSessions({
|
|
33
|
-
|
|
33
|
+
sessionsDirs: defaultCodexSessionDirs(homeDir),
|
|
34
34
|
worktrees: options.worktrees,
|
|
35
35
|
now,
|
|
36
|
+
sinceMinutes: CODEX_ATTRIBUTION_BACKFILL_SINCE_MINUTES,
|
|
37
|
+
limit: CODEX_ATTRIBUTION_BACKFILL_SESSION_LIMIT,
|
|
36
38
|
});
|
|
37
39
|
// First run (D B.4 §8): without a Claude cursor yet, widen the window to 14
|
|
38
40
|
// days so the first sync captures retroactive history instead of only 24h.
|
package/dist/upload.js
CHANGED
|
@@ -2,6 +2,7 @@ import { AgentImageArtifactReportRequestSchema, EvidenceCompletenessPayloadSchem
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, readLocalWorkContextForRepo, } from "./local-state.js";
|
|
4
4
|
import { runLocalSourceCollectors } from "./adapters/local-sources.js";
|
|
5
|
+
import { defaultCodexSessionDirs, } from "./adapters/codex-attribution.js";
|
|
5
6
|
import { uploadRawEvidenceFilesChunked, } from "./evidence-upload-client.js";
|
|
6
7
|
import { markObjectCommitted, readRawEvidenceCursor, writeRawEvidenceCursor, } from "./cursors/raw-evidence-cursor.js";
|
|
7
8
|
import { recordUploadBlocked, recordUploadFailure, recordUploadSuccess, } from "./spool/local-spool.js";
|
|
@@ -57,7 +58,7 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
57
58
|
workContextId: uploadContext.work_context_id,
|
|
58
59
|
activeWorkContext: activeContext,
|
|
59
60
|
rawEvidenceStateDir: paths.state_dir,
|
|
60
|
-
|
|
61
|
+
rawEvidenceSessionsDirs: defaultCodexSessionDirs(paths.home_dir),
|
|
61
62
|
claudeProjectsDir: path.join(paths.home_dir, ".claude", "projects"),
|
|
62
63
|
rawEvidenceIncludeCodexJsonl: options.rawEvidenceIncludeCodexJsonl,
|
|
63
64
|
rawEvidenceIncludeClaudeJsonl: options.rawEvidenceIncludeClaudeJsonl,
|