@bli-cockpit/cli 0.2.119 → 0.2.122
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/commands/analyze.js +74 -54
- package/dist/commands/brief-rewrite.js +164 -101
- package/dist/commands/brief.js +38 -13
- package/dist/commands/careers.js +81 -9
- package/dist/commands/correct.js +38 -21
- package/dist/commands/docs.js +13 -10
- package/dist/commands/editor.js +59 -30
- package/dist/commands/install-receipts.js +106 -91
- package/dist/commands/local-args-tower-admin.js +4 -0
- package/dist/commands/local-args-tower-cal.js +28 -3
- package/dist/commands/local-args-tower-careers.js +56 -9
- package/dist/commands/local-args-tower-chat.js +55 -28
- package/dist/commands/local-args-tower-docs-msg.js +39 -8
- package/dist/commands/local-args-tower-mail.js +27 -1
- package/dist/commands/local-args-tower-models.js +14 -17
- package/dist/commands/local-args-tower-work.js +37 -6
- package/dist/commands/local-help-commands-tower.js +15 -1
- package/dist/commands/local-help-commands.js +2 -1
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/mcp-stdio-probe.js +92 -73
- package/dist/commands/memory-hook-performance.js +135 -101
- package/dist/commands/memory-install-claude.js +15 -14
- package/dist/commands/memory-install-codex.js +10 -6
- package/dist/commands/memory-install-config.js +5 -4
- package/dist/commands/memory-install-contract.js +56 -10
- package/dist/commands/memory-install-report.js +16 -11
- package/dist/commands/memory-install-skills.js +11 -11
- package/dist/commands/memory-log.js +22 -5
- package/dist/commands/msg.js +11 -5
- package/dist/commands/onboard-setup.js +16 -1
- package/dist/commands/ops-sections.js +89 -0
- package/dist/commands/ops.js +117 -120
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/scout.js +90 -68
- package/dist/commands/session-sync-failures.js +19 -13
- package/dist/commands/session-sync-record.js +53 -52
- package/dist/commands/session-sync-upload.js +15 -11
- package/dist/commands/sessions.js +61 -51
- package/dist/commands/slack.js +90 -61
- package/dist/commands/status.js +53 -41
- package/dist/commands/workbook.js +23 -20
- package/package.json +2 -2
|
@@ -86,17 +86,7 @@ async function syncOneWorktree(options) {
|
|
|
86
86
|
const attributedSyntheticTarget = options.isDiscoveredWorktree
|
|
87
87
|
? null
|
|
88
88
|
: worktree;
|
|
89
|
-
const
|
|
90
|
-
homeDir: run.homeDir,
|
|
91
|
-
repoRoot: worktree.repo_root,
|
|
92
|
-
activeTicketId: run.activeTicketId,
|
|
93
|
-
operatorId: run.operatorId,
|
|
94
|
-
sessionId: run.sessionId,
|
|
95
|
-
...(attributedSyntheticTarget ? {} : { branch: run.branch }),
|
|
96
|
-
};
|
|
97
|
-
const ensureContext = () => attributedSyntheticTarget
|
|
98
|
-
? startLocalWorkContextForAttributedTarget(contextOptions, attributedSyntheticTarget)
|
|
99
|
-
: startLocalWorkContext(contextOptions);
|
|
89
|
+
const ensureContext = () => startWorktreeContext(options, attributedSyntheticTarget);
|
|
100
90
|
let context = null;
|
|
101
91
|
if (run.startContexts || attributedSyntheticTarget) {
|
|
102
92
|
context = await ensureContext();
|
|
@@ -118,6 +108,20 @@ async function syncOneWorktree(options) {
|
|
|
118
108
|
}
|
|
119
109
|
return { worktree, context, sync };
|
|
120
110
|
}
|
|
111
|
+
function startWorktreeContext(options, attributedSyntheticTarget) {
|
|
112
|
+
const { run, worktree } = options;
|
|
113
|
+
const contextOptions = {
|
|
114
|
+
homeDir: run.homeDir,
|
|
115
|
+
repoRoot: worktree.repo_root,
|
|
116
|
+
activeTicketId: run.activeTicketId,
|
|
117
|
+
operatorId: run.operatorId,
|
|
118
|
+
sessionId: run.sessionId,
|
|
119
|
+
...(attributedSyntheticTarget ? {} : { branch: run.branch }),
|
|
120
|
+
};
|
|
121
|
+
return attributedSyntheticTarget
|
|
122
|
+
? startLocalWorkContextForAttributedTarget(contextOptions, attributedSyntheticTarget)
|
|
123
|
+
: startLocalWorkContext(contextOptions);
|
|
124
|
+
}
|
|
121
125
|
/**
|
|
122
126
|
* Assemble what one worktree is about to upload: its sibling worktree
|
|
123
127
|
* inventory, its Codex mains, its Claude mains and sidecars, and its share of
|
|
@@ -21,9 +21,45 @@ export async function runSessions(command, io) {
|
|
|
21
21
|
const homeDir = command.homeDir ?? os.homedir();
|
|
22
22
|
const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos, homeDir: command.homeDir }, io);
|
|
23
23
|
const window = await sessionsScanWindow(command, now);
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
24
|
+
const { codex, claude } = await scanSessionSources(command, homeDir, worktrees, now, window);
|
|
25
|
+
const { codexRows, claudeRows } = sessionRows(codex, claude);
|
|
26
|
+
// BLI-3483: the scanners have always counted the folders and files they
|
|
27
|
+
// could not read, and this command has always thrown those counts away
|
|
28
|
+
// except for one field in `--json`. So "No sessions observed" was printed on
|
|
29
|
+
// a machine where the scan had been blocked from looking — the operator's
|
|
30
|
+
// one diagnostic tool answering the question with the failure removed.
|
|
31
|
+
const readFailures = countReadFailures(codex, claude);
|
|
32
|
+
if (command.json) {
|
|
33
|
+
return writeSessionJson(io, window, readFailures, codex, claude, codexRows, claudeRows);
|
|
34
|
+
}
|
|
35
|
+
return writeSessionText(io, window, readFailures, codexRows, claudeRows);
|
|
36
|
+
}
|
|
37
|
+
function writeSessionText(io, window, readFailures, codexRows, claudeRows) {
|
|
38
|
+
writeLine(io.stdout, "Tower sessions (read-only attribution)");
|
|
39
|
+
writeLine(io.stdout, `window: ${sessionsWindowLine(window)}`);
|
|
40
|
+
for (const row of codexRows) {
|
|
41
|
+
writeLine(io.stdout, sessionRowLine(row));
|
|
42
|
+
}
|
|
43
|
+
for (const row of claudeRows) {
|
|
44
|
+
writeLine(io.stdout, sessionRowLine(row));
|
|
45
|
+
for (const sidecar of row.sidecar_skips) {
|
|
46
|
+
writeLine(io.stdout, ` sidecar ${sidecar.file_name}: ${sidecar.reason}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (codexRows.length === 0 && claudeRows.length === 0) {
|
|
50
|
+
writeLine(io.stdout, readFailures.total > 0
|
|
51
|
+
? `No sessions listed, but ${readFailures.total} read failure(s) mean the scan could not see everywhere — this is not proof there were none.`
|
|
52
|
+
: "No sessions observed in the scan window.");
|
|
53
|
+
}
|
|
54
|
+
if (readFailures.total > 0) {
|
|
55
|
+
for (const line of readFailureLines(readFailures)) {
|
|
56
|
+
writeLine(io.stdout, line);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
async function scanSessionSources(command, homeDir, worktrees, now, window) {
|
|
62
|
+
const codex = command.source !== "claude"
|
|
27
63
|
? await scanAndAttributeCodexSessions({
|
|
28
64
|
sessionsDirs: defaultCodexSessionDirs(homeDir),
|
|
29
65
|
worktrees,
|
|
@@ -32,7 +68,7 @@ export async function runSessions(command, io) {
|
|
|
32
68
|
limit: window.limit,
|
|
33
69
|
})
|
|
34
70
|
: null;
|
|
35
|
-
const claude =
|
|
71
|
+
const claude = command.source !== "codex"
|
|
36
72
|
? await scanAndAttributeClaudeSessions({
|
|
37
73
|
projectsDir: path.join(homeDir, ".claude", "projects"),
|
|
38
74
|
worktrees,
|
|
@@ -41,6 +77,26 @@ export async function runSessions(command, io) {
|
|
|
41
77
|
limit: window.mode === "default" ? undefined : window.limit,
|
|
42
78
|
})
|
|
43
79
|
: null;
|
|
80
|
+
return { codex, claude };
|
|
81
|
+
}
|
|
82
|
+
function writeSessionJson(io, window, readFailures, codex, claude, codexRows, claudeRows) {
|
|
83
|
+
writeLine(io.stdout, JSON.stringify({
|
|
84
|
+
window,
|
|
85
|
+
read_failures: readFailures,
|
|
86
|
+
...(codex ? { codex: { counts: codex.counts, sessions: codexRows } } : {}),
|
|
87
|
+
...(claude
|
|
88
|
+
? {
|
|
89
|
+
claude: {
|
|
90
|
+
counts: claude.counts,
|
|
91
|
+
project_dirs_skipped: claude.project_dirs_skipped,
|
|
92
|
+
sessions: claudeRows,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
}, null, 2));
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
function sessionRows(codex, claude) {
|
|
44
100
|
// Safe output contract (B.4 §6): id, state, reason, scores, signals, sidecar
|
|
45
101
|
// skip reasons, plus the repo_label basename. Branch is intentionally omitted
|
|
46
102
|
// — branch names can carry operator-authored task/customer text.
|
|
@@ -71,53 +127,7 @@ export async function runSessions(command, io) {
|
|
|
71
127
|
reason: sidecar.skipped_reason,
|
|
72
128
|
})),
|
|
73
129
|
}));
|
|
74
|
-
|
|
75
|
-
// could not read, and this command has always thrown those counts away
|
|
76
|
-
// except for one field in `--json`. So "No sessions observed" was printed on
|
|
77
|
-
// a machine where the scan had been blocked from looking — the operator's
|
|
78
|
-
// one diagnostic tool answering the question with the failure removed.
|
|
79
|
-
const readFailures = countReadFailures(codex, claude);
|
|
80
|
-
if (command.json) {
|
|
81
|
-
writeLine(io.stdout, JSON.stringify({
|
|
82
|
-
window,
|
|
83
|
-
read_failures: readFailures,
|
|
84
|
-
...(codex
|
|
85
|
-
? { codex: { counts: codex.counts, sessions: codexRows } }
|
|
86
|
-
: {}),
|
|
87
|
-
...(claude
|
|
88
|
-
? {
|
|
89
|
-
claude: {
|
|
90
|
-
counts: claude.counts,
|
|
91
|
-
project_dirs_skipped: claude.project_dirs_skipped,
|
|
92
|
-
sessions: claudeRows,
|
|
93
|
-
},
|
|
94
|
-
}
|
|
95
|
-
: {}),
|
|
96
|
-
}, null, 2));
|
|
97
|
-
return 0;
|
|
98
|
-
}
|
|
99
|
-
writeLine(io.stdout, "Tower sessions (read-only attribution)");
|
|
100
|
-
writeLine(io.stdout, `window: ${sessionsWindowLine(window)}`);
|
|
101
|
-
for (const row of codexRows) {
|
|
102
|
-
writeLine(io.stdout, sessionRowLine(row));
|
|
103
|
-
}
|
|
104
|
-
for (const row of claudeRows) {
|
|
105
|
-
writeLine(io.stdout, sessionRowLine(row));
|
|
106
|
-
for (const sidecar of row.sidecar_skips) {
|
|
107
|
-
writeLine(io.stdout, ` sidecar ${sidecar.file_name}: ${sidecar.reason}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (codexRows.length === 0 && claudeRows.length === 0) {
|
|
111
|
-
writeLine(io.stdout, readFailures.total > 0
|
|
112
|
-
? `No sessions listed, but ${readFailures.total} read failure(s) mean the scan could not see everywhere — this is not proof there were none.`
|
|
113
|
-
: "No sessions observed in the scan window.");
|
|
114
|
-
}
|
|
115
|
-
if (readFailures.total > 0) {
|
|
116
|
-
for (const line of readFailureLines(readFailures)) {
|
|
117
|
-
writeLine(io.stdout, line);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return 0;
|
|
130
|
+
return { codexRows, claudeRows };
|
|
121
131
|
}
|
|
122
132
|
/**
|
|
123
133
|
* Everything the two scanners looked at and could not read. Counts only — the
|
package/dist/commands/slack.js
CHANGED
|
@@ -46,15 +46,23 @@ async function runCoverageAction(command, io, tower) {
|
|
|
46
46
|
const workspaces = Array.isArray(coverage["workspaces"])
|
|
47
47
|
? coverage["workspaces"]
|
|
48
48
|
: [];
|
|
49
|
+
writeCoverageReply(command, io, body, coverage, workspaces);
|
|
50
|
+
logCoverageRead(command, io, workspaces);
|
|
51
|
+
// A stale cursor is a finding, not a failure of this command: exit 0 so a
|
|
52
|
+
// person reading coverage is not told their own read broke.
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
function writeCoverageReply(command, io, body, coverage, workspaces) {
|
|
49
56
|
if (command.json) {
|
|
50
57
|
writeLine(io.stdout, JSON.stringify(body));
|
|
58
|
+
return;
|
|
51
59
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
writeLine(io.stdout, line);
|
|
56
|
-
}
|
|
60
|
+
const styled = colorEnabled(io);
|
|
61
|
+
for (const line of coverageLines(coverage, workspaces, command.staleOnly === true, (text) => dim(text, styled))) {
|
|
62
|
+
writeLine(io.stdout, line);
|
|
57
63
|
}
|
|
64
|
+
}
|
|
65
|
+
function logCoverageRead(command, io, workspaces) {
|
|
58
66
|
const staleTotal = workspaces.reduce((sum, row) => sum + numberOf(row["staleChannelsTotal"]), 0);
|
|
59
67
|
writeLine(io.stderr, `[slack cli] coverage read ${JSON.stringify({
|
|
60
68
|
workspace: command.workspace ?? "all",
|
|
@@ -64,75 +72,74 @@ async function runCoverageAction(command, io, tower) {
|
|
|
64
72
|
open_skips: workspaces.reduce((sum, row) => sum + numberOf(row["openSkipsTotal"]), 0),
|
|
65
73
|
stale_only: command.staleOnly === true,
|
|
66
74
|
})}`);
|
|
67
|
-
// A stale cursor is a finding, not a failure of this command: exit 0 so a
|
|
68
|
-
// person reading coverage is not told their own read broke.
|
|
69
|
-
return 0;
|
|
70
75
|
}
|
|
71
76
|
function coverageLines(coverage, workspaces, staleOnly, paint) {
|
|
72
77
|
const lines = [];
|
|
73
78
|
const staleAfter = numberOf(coverage["staleAfterHours"]);
|
|
74
79
|
for (const workspace of workspaces) {
|
|
75
|
-
|
|
76
|
-
const known = numberOf(workspace["channelsKnown"]);
|
|
77
|
-
const covered = numberOf(workspace["covered"]);
|
|
78
|
-
lines.push(`${key.toUpperCase()} ${covered} of ${known} channels readable`);
|
|
79
|
-
if (!staleOnly) {
|
|
80
|
-
const byVisibility = Array.isArray(workspace["byVisibility"])
|
|
81
|
-
? workspace["byVisibility"]
|
|
82
|
-
: [];
|
|
83
|
-
for (const entry of byVisibility) {
|
|
84
|
-
lines.push(paint(` ${String(numberOf(entry["count"])).padStart(4)} ${String(entry["state"] ?? "?")} — ${String(entry["meaning"] ?? "?")}`));
|
|
85
|
-
}
|
|
86
|
-
lines.push(paint(` newest cursor: ${String(workspace["newestCursorIso"] ?? "never")}`));
|
|
87
|
-
}
|
|
88
|
-
const stale = Array.isArray(workspace["staleChannels"])
|
|
89
|
-
? workspace["staleChannels"]
|
|
90
|
-
: [];
|
|
91
|
-
const staleTotal = numberOf(workspace["staleChannelsTotal"]);
|
|
92
|
-
if (staleTotal === 0) {
|
|
93
|
-
lines.push(paint(` no channel is over ${staleAfter}h since its last sync`));
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
lines.push(` ${staleTotal} channel${staleTotal === 1 ? "" : "s"} over ${staleAfter}h since last sync:`);
|
|
97
|
-
for (const entry of stale) {
|
|
98
|
-
lines.push(paint(` ${String(entry["name"] ?? "?")} last ${String(entry["lastSyncedAt"] ?? "never")}`));
|
|
99
|
-
}
|
|
100
|
-
if (stale.length < staleTotal)
|
|
101
|
-
lines.push(paint(` …and ${staleTotal - stale.length} more`));
|
|
102
|
-
}
|
|
103
|
-
const skips = Array.isArray(workspace["openSkips"])
|
|
104
|
-
? workspace["openSkips"]
|
|
105
|
-
: [];
|
|
106
|
-
if (skips.length > 0) {
|
|
107
|
-
lines.push(` open ingest skips:`);
|
|
108
|
-
for (const skip of skips) {
|
|
109
|
-
lines.push(paint(` ${String(numberOf(skip["count"])).padStart(4)} ${String(skip["reason"] ?? "?")}`));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
lines.push("");
|
|
80
|
+
writeWorkspaceCoverageLines(lines, workspace, staleAfter, staleOnly, paint);
|
|
113
81
|
}
|
|
82
|
+
writeCoverageFooter(lines, coverage, paint);
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
function writeWorkspaceCoverageLines(lines, workspace, staleAfter, staleOnly, paint) {
|
|
86
|
+
const key = String(workspace["workspace"] ?? "?");
|
|
87
|
+
const known = numberOf(workspace["channelsKnown"]);
|
|
88
|
+
const covered = numberOf(workspace["covered"]);
|
|
89
|
+
lines.push(`${key.toUpperCase()} ${covered} of ${known} channels readable`);
|
|
90
|
+
writeVisibilityLines(lines, workspace, staleOnly, paint);
|
|
91
|
+
writeStaleChannelLines(lines, workspace, staleAfter, paint);
|
|
92
|
+
writeOpenSkipLines(lines, workspace, paint);
|
|
93
|
+
lines.push("");
|
|
94
|
+
}
|
|
95
|
+
function writeCoverageFooter(lines, coverage, paint) {
|
|
114
96
|
const orphanChannels = numberOf(coverage["orphanChannels"]);
|
|
115
97
|
if (orphanChannels > 0) {
|
|
116
98
|
lines.push(paint(`${orphanChannels} channel(s) belong to no known workspace and are not counted above.`));
|
|
117
99
|
}
|
|
118
100
|
if (typeof coverage["note"] === "string")
|
|
119
101
|
lines.push(paint(coverage["note"]));
|
|
120
|
-
|
|
102
|
+
}
|
|
103
|
+
function writeVisibilityLines(lines, workspace, staleOnly, paint) {
|
|
104
|
+
if (staleOnly)
|
|
105
|
+
return;
|
|
106
|
+
const byVisibility = Array.isArray(workspace["byVisibility"])
|
|
107
|
+
? workspace["byVisibility"]
|
|
108
|
+
: [];
|
|
109
|
+
for (const entry of byVisibility) {
|
|
110
|
+
lines.push(paint(` ${String(numberOf(entry["count"])).padStart(4)} ${String(entry["state"] ?? "?")} — ${String(entry["meaning"] ?? "?")}`));
|
|
111
|
+
}
|
|
112
|
+
lines.push(paint(` newest cursor: ${String(workspace["newestCursorIso"] ?? "never")}`));
|
|
113
|
+
}
|
|
114
|
+
function writeStaleChannelLines(lines, workspace, staleAfter, paint) {
|
|
115
|
+
const stale = Array.isArray(workspace["staleChannels"])
|
|
116
|
+
? workspace["staleChannels"]
|
|
117
|
+
: [];
|
|
118
|
+
const staleTotal = numberOf(workspace["staleChannelsTotal"]);
|
|
119
|
+
if (staleTotal === 0) {
|
|
120
|
+
lines.push(paint(` no channel is over ${staleAfter}h since its last sync`));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
lines.push(` ${staleTotal} channel${staleTotal === 1 ? "" : "s"} over ${staleAfter}h since last sync:`);
|
|
124
|
+
for (const entry of stale) {
|
|
125
|
+
lines.push(paint(` ${String(entry["name"] ?? "?")} last ${String(entry["lastSyncedAt"] ?? "never")}`));
|
|
126
|
+
}
|
|
127
|
+
if (stale.length < staleTotal)
|
|
128
|
+
lines.push(paint(` …and ${staleTotal - stale.length} more`));
|
|
129
|
+
}
|
|
130
|
+
function writeOpenSkipLines(lines, workspace, paint) {
|
|
131
|
+
const skips = Array.isArray(workspace["openSkips"])
|
|
132
|
+
? workspace["openSkips"]
|
|
133
|
+
: [];
|
|
134
|
+
if (skips.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
lines.push(` open ingest skips:`);
|
|
137
|
+
for (const skip of skips) {
|
|
138
|
+
lines.push(paint(` ${String(numberOf(skip["count"])).padStart(4)} ${String(skip["reason"] ?? "?")}`));
|
|
139
|
+
}
|
|
121
140
|
}
|
|
122
141
|
async function runReadAction(command, io, tower) {
|
|
123
|
-
const body =
|
|
124
|
-
if (command.person)
|
|
125
|
-
body["person"] = command.person;
|
|
126
|
-
if (command.channel)
|
|
127
|
-
body["channel"] = command.channel;
|
|
128
|
-
if (command.query)
|
|
129
|
-
body["query"] = command.query;
|
|
130
|
-
if (command.since)
|
|
131
|
-
body["since"] = command.since;
|
|
132
|
-
if (command.until)
|
|
133
|
-
body["until"] = command.until;
|
|
134
|
-
if (command.limit !== undefined)
|
|
135
|
-
body["limit"] = command.limit;
|
|
142
|
+
const body = buildReadBody(command);
|
|
136
143
|
const result = await callTower(tower, {
|
|
137
144
|
path: "/api/ops/slack/read",
|
|
138
145
|
method: "POST",
|
|
@@ -156,6 +163,27 @@ async function runReadAction(command, io, tower) {
|
|
|
156
163
|
const messages = Array.isArray(read["messages"])
|
|
157
164
|
? read["messages"]
|
|
158
165
|
: [];
|
|
166
|
+
writeReadReply(command, io, payload, read, messages);
|
|
167
|
+
logReadResult(command, io, read, messages);
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
function buildReadBody(command) {
|
|
171
|
+
const body = {};
|
|
172
|
+
if (command.person)
|
|
173
|
+
body["person"] = command.person;
|
|
174
|
+
if (command.channel)
|
|
175
|
+
body["channel"] = command.channel;
|
|
176
|
+
if (command.query)
|
|
177
|
+
body["query"] = command.query;
|
|
178
|
+
if (command.since)
|
|
179
|
+
body["since"] = command.since;
|
|
180
|
+
if (command.until)
|
|
181
|
+
body["until"] = command.until;
|
|
182
|
+
if (command.limit !== undefined)
|
|
183
|
+
body["limit"] = command.limit;
|
|
184
|
+
return body;
|
|
185
|
+
}
|
|
186
|
+
function writeReadReply(command, io, payload, read, messages) {
|
|
159
187
|
if (command.json) {
|
|
160
188
|
writeLine(io.stdout, JSON.stringify(payload));
|
|
161
189
|
}
|
|
@@ -179,6 +207,8 @@ async function runReadAction(command, io, tower) {
|
|
|
179
207
|
writeLine(io.stdout, dim(read["note"], styled));
|
|
180
208
|
}
|
|
181
209
|
}
|
|
210
|
+
}
|
|
211
|
+
function logReadResult(command, io, read, messages) {
|
|
182
212
|
const coverage = asRecord(read["coverage"]);
|
|
183
213
|
writeLine(io.stderr, `[slack cli] read ${JSON.stringify({
|
|
184
214
|
status: read["status"] ?? null,
|
|
@@ -191,7 +221,6 @@ async function runReadAction(command, io, tower) {
|
|
|
191
221
|
has_channel: Boolean(command.channel),
|
|
192
222
|
has_query: Boolean(command.query),
|
|
193
223
|
})}`);
|
|
194
|
-
return 0;
|
|
195
224
|
}
|
|
196
225
|
function numberOf(value) {
|
|
197
226
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
package/dist/commands/status.js
CHANGED
|
@@ -29,6 +29,14 @@ import { getCollectorRuntimePaths, inspectLocalCollectorStatus, readLocalCollect
|
|
|
29
29
|
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
30
30
|
export async function runStatus(command, io) {
|
|
31
31
|
const backfillCursor = await inspectBackfillCursor(command.homeDir);
|
|
32
|
+
const setupReceipt = await readMachineSetupReceipt(command.homeDir, io);
|
|
33
|
+
const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos, homeDir: command.homeDir }, io);
|
|
34
|
+
if (worktrees.length > 1) {
|
|
35
|
+
return writeMultiRepoStatus(command, io, backfillCursor, setupReceipt, worktrees);
|
|
36
|
+
}
|
|
37
|
+
return writeSingleRepoStatus(command, io, backfillCursor, setupReceipt);
|
|
38
|
+
}
|
|
39
|
+
async function readMachineSetupReceipt(homeDir, io) {
|
|
32
40
|
// BLI-3731: "is this machine actually connected?" belongs beside "is it
|
|
33
41
|
// collecting?", and it is read back off the host rather than remembered.
|
|
34
42
|
// Refreshing here also keeps the heartbeat's cache warm without the 15-minute
|
|
@@ -38,40 +46,43 @@ export async function runStatus(command, io) {
|
|
|
38
46
|
// about the MACHINE, so it cannot depend on whether the folder you happen to
|
|
39
47
|
// stand in holds one repo or six — and the multi-repo branch, which every
|
|
40
48
|
// agent machine takes, used to return before the receipt was ever built.
|
|
41
|
-
|
|
42
|
-
...(
|
|
49
|
+
return refreshSetupReceipt(io, {
|
|
50
|
+
...(homeDir ? { homeDir } : {}),
|
|
43
51
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// about this machine, so both sit at the top of the payload rather
|
|
60
|
-
// than once per repo row. Null means this run could not read the
|
|
61
|
-
// host — `refreshSetupReceipt` named the reason on stderr — never
|
|
62
|
-
// "this machine is not connected".
|
|
63
|
-
setup_receipt: setupReceipt?.receipt ?? null,
|
|
64
|
-
}, null, 2));
|
|
65
|
-
return 0;
|
|
66
|
-
}
|
|
67
|
-
writeLine(io.stdout, "Tower status — every repo below this folder");
|
|
68
|
-
writeLine(io.stdout, `Old sessions: ${backfillCursorLine(backfillCursor)}`);
|
|
69
|
-
for (const status of statuses) {
|
|
70
|
-
writeLine(io.stdout, `- ${status.repo_label ?? status.repo}/${status.worktree_label ?? "worktree"} · ${status.branch} · head:${shortSha(status.head_sha)} · ${status.upload_state}`);
|
|
71
|
-
}
|
|
72
|
-
writeConnectedBlock(io, setupReceipt);
|
|
73
|
-
return 0;
|
|
52
|
+
}
|
|
53
|
+
async function writeMultiRepoStatus(command, io, backfillCursor, setupReceipt, worktrees) {
|
|
54
|
+
const statuses = await Promise.all(worktrees.map(async (worktree) => ({
|
|
55
|
+
...(await inspectLocalCollectorStatus({
|
|
56
|
+
homeDir: command.homeDir,
|
|
57
|
+
repoRoot: worktree.repo_root,
|
|
58
|
+
})),
|
|
59
|
+
head_sha: worktree.head_sha,
|
|
60
|
+
})));
|
|
61
|
+
if (command.json)
|
|
62
|
+
return writeMultiRepoJson(io, backfillCursor, setupReceipt, statuses);
|
|
63
|
+
writeLine(io.stdout, "Tower status — every repo below this folder");
|
|
64
|
+
writeLine(io.stdout, `Old sessions: ${backfillCursorLine(backfillCursor)}`);
|
|
65
|
+
for (const status of statuses) {
|
|
66
|
+
writeLine(io.stdout, `- ${status.repo_label ?? status.repo}/${status.worktree_label ?? "worktree"} · ${status.branch} · head:${shortSha(status.head_sha)} · ${status.upload_state}`);
|
|
74
67
|
}
|
|
68
|
+
writeConnectedBlock(io, setupReceipt);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
function writeMultiRepoJson(io, backfillCursor, setupReceipt, statuses) {
|
|
72
|
+
writeLine(io.stdout, JSON.stringify({
|
|
73
|
+
mode: "multi_repo",
|
|
74
|
+
statuses,
|
|
75
|
+
backfill_cursor: backfillCursor,
|
|
76
|
+
// Beside `backfill_cursor`, and for the same reason: both are facts
|
|
77
|
+
// about this machine, so both sit at the top of the payload rather
|
|
78
|
+
// than once per repo row. Null means this run could not read the
|
|
79
|
+
// host — `refreshSetupReceipt` named the reason on stderr — never
|
|
80
|
+
// "this machine is not connected".
|
|
81
|
+
setup_receipt: setupReceipt?.receipt ?? null,
|
|
82
|
+
}, null, 2));
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
async function writeSingleRepoStatus(command, io, backfillCursor, setupReceipt) {
|
|
75
86
|
// The staging walk is opt-in (BLI-3797) and this is the surface that pays for
|
|
76
87
|
// it: one person asking one question. The multi-repo branch above deliberately
|
|
77
88
|
// does not — the backlog is a MACHINE fact, so N repos would walk one tree N
|
|
@@ -80,14 +91,16 @@ export async function runStatus(command, io) {
|
|
|
80
91
|
...command,
|
|
81
92
|
includeStagingBacklog: true,
|
|
82
93
|
});
|
|
83
|
-
if (command.json)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
if (command.json)
|
|
95
|
+
return writeSingleRepoJson(io, status, backfillCursor, setupReceipt);
|
|
96
|
+
writeSingleRepoLines(io, status, backfillCursor, setupReceipt);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
function writeSingleRepoJson(io, status, backfillCursor, setupReceipt) {
|
|
100
|
+
writeLine(io.stdout, JSON.stringify({ ...status, backfill_cursor: backfillCursor, setup_receipt: setupReceipt?.receipt ?? null }, null, 2));
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
function writeSingleRepoLines(io, status, backfillCursor, setupReceipt) {
|
|
91
104
|
writeLine(io.stdout, "Tower status");
|
|
92
105
|
writeLine(io.stdout, `Installed: ${status.installed ? "yes" : "no"}`);
|
|
93
106
|
writeLine(io.stdout, `Signed in: ${signedInLine(status.session_state)}`);
|
|
@@ -117,7 +130,6 @@ export async function runStatus(command, io) {
|
|
|
117
130
|
for (const detail of status.details)
|
|
118
131
|
writeLine(io.stdout, `- ${detail}`);
|
|
119
132
|
writeConnectedBlock(io, setupReceipt);
|
|
120
|
-
return 0;
|
|
121
133
|
}
|
|
122
134
|
/**
|
|
123
135
|
* The receipt block, in the words `cockpit doctor` prints, from the one reader
|
|
@@ -100,26 +100,9 @@ function logIndexRead(io, projects) {
|
|
|
100
100
|
function writeDoc(command, io, payload, width) {
|
|
101
101
|
const markdown = payload.markdown ?? "";
|
|
102
102
|
const sections = payload.sections ?? [];
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
if (slice === null) {
|
|
107
|
-
const known = sections.map((section) => section.id ?? "").filter(Boolean);
|
|
108
|
-
const sentence = `No section "${command.section}" in this document (${known.join(", ") || "none"}).`;
|
|
109
|
-
writeLine(io.stderr, `[workbook cli] section missed ${JSON.stringify({
|
|
110
|
-
reason: "workbook_section_not_found",
|
|
111
|
-
sections: sections.length,
|
|
112
|
-
})}`);
|
|
113
|
-
if (command.json) {
|
|
114
|
-
writeLine(io.stdout, JSON.stringify({ ok: false, error: "workbook_section_not_found", detail: sentence }));
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
writeLine(io.stderr, sentence);
|
|
118
|
-
}
|
|
119
|
-
return 1;
|
|
120
|
-
}
|
|
121
|
-
selected = slice;
|
|
122
|
-
}
|
|
103
|
+
const selected = selectDocumentSection(command, io, markdown, sections);
|
|
104
|
+
if (selected === null)
|
|
105
|
+
return 1;
|
|
123
106
|
if (command.json) {
|
|
124
107
|
writeLine(io.stdout, JSON.stringify({
|
|
125
108
|
ok: true,
|
|
@@ -150,6 +133,26 @@ function writeDoc(command, io, payload, width) {
|
|
|
150
133
|
})}`);
|
|
151
134
|
return 0;
|
|
152
135
|
}
|
|
136
|
+
function selectDocumentSection(command, io, markdown, sections) {
|
|
137
|
+
if (!command.section)
|
|
138
|
+
return markdown;
|
|
139
|
+
const slice = sectionSlice(markdown, sections, command.section);
|
|
140
|
+
if (slice !== null)
|
|
141
|
+
return slice;
|
|
142
|
+
const known = sections.map((section) => section.id ?? "").filter(Boolean);
|
|
143
|
+
const sentence = `No section "${command.section}" in this document (${known.join(", ") || "none"}).`;
|
|
144
|
+
writeLine(io.stderr, `[workbook cli] section missed ${JSON.stringify({
|
|
145
|
+
reason: "workbook_section_not_found",
|
|
146
|
+
sections: sections.length,
|
|
147
|
+
})}`);
|
|
148
|
+
if (command.json) {
|
|
149
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "workbook_section_not_found", detail: sentence }));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
writeLine(io.stderr, sentence);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
153
156
|
// ----------------------------------------------------------------- small parts
|
|
154
157
|
/**
|
|
155
158
|
* The wrap column: an explicit `--width` wins, otherwise the terminal's own
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.122",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@bli-cockpit/memory-mcp": "0.1.32",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.49",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.48"
|
|
33
33
|
}
|
|
34
34
|
}
|