@bli-cockpit/cli 0.2.237 → 0.2.238
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/adapters/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-overlay.js +73 -30
- package/dist/commands/cal.js +36 -23
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +68 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +52 -50
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +55 -45
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-work.js +52 -33
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +6 -2
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +91 -67
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +2 -2
|
@@ -19,115 +19,92 @@ export async function uploadNotes(command, door) {
|
|
|
19
19
|
const results = [];
|
|
20
20
|
let worstExit = 0;
|
|
21
21
|
for (const filePath of paths) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!read.ok) {
|
|
26
|
-
const sentence = noteFileRefusalSentence(read.refusal, filePath);
|
|
27
|
-
writeLine(door.io.stderr, `${TAG} file refused ${JSON.stringify({ reason: read.refusal, detail: read.detail })}`);
|
|
28
|
-
if (!door.json)
|
|
29
|
-
writeLine(door.io.stderr, sentence);
|
|
30
|
-
results.push({ path: filePath, ok: false, reason: read.refusal });
|
|
31
|
-
worstExit = 1;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (!read.direct && read.bytes.byteLength >= NOTE_SLOW_UPLOAD_BYTES) {
|
|
35
|
-
// The route reads the whole note with one model call and may take
|
|
36
|
-
// minutes. A still cursor reads as a hang, so say what is happening.
|
|
37
|
-
writeLine(door.io.stderr, `Reading ${read.fileName} (${Math.round(read.bytes.byteLength / 1024)} KB). This can take a couple of minutes.`);
|
|
38
|
-
}
|
|
39
|
-
// A whole-meeting recording (over 4 MB) goes straight to storage and is
|
|
40
|
-
// transcribed in the background (BLI-12963); everything else keeps the
|
|
41
|
-
// upload route.
|
|
42
|
-
let answer;
|
|
43
|
-
if (read.direct) {
|
|
44
|
-
const direct = await uploadAudioDirect(door, {
|
|
45
|
-
filePath, fileName: read.fileName, size: read.size, exclude: command.exclude, folder: command.folder,
|
|
46
|
-
});
|
|
47
|
-
answer = direct.ok ? { ok: true, body: direct.body } : { ok: false, reason: direct.reason, detail: direct.detail };
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
const form = new FormData();
|
|
51
|
-
form.set("file", new File([new Uint8Array(read.bytes)], read.fileName, { type: noteAudioMime(read.fileName) ?? "" }));
|
|
52
|
-
if (command.exclude)
|
|
53
|
-
form.set("exclusions", command.exclude);
|
|
54
|
-
if (command.folder)
|
|
55
|
-
form.set("folder_path", command.folder);
|
|
56
|
-
answer = await ask(door, {
|
|
57
|
-
path: "/api/notes/upload",
|
|
58
|
-
method: "POST",
|
|
59
|
-
label: "notes upload",
|
|
60
|
-
timeoutMs: UPLOAD_DEADLINE_MS,
|
|
61
|
-
body: form,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
if (!answer.ok) {
|
|
65
|
-
writeLine(door.io.stderr, `${TAG} upload failed ${JSON.stringify({ reason: answer.reason })}`);
|
|
66
|
-
if (!door.json)
|
|
67
|
-
writeLine(door.io.stderr, `${read.fileName}: ${answer.detail}`);
|
|
68
|
-
results.push({ path: filePath, ok: false, reason: answer.reason });
|
|
22
|
+
const uploaded = await uploadOneNote(command, door, filePath);
|
|
23
|
+
results.push(uploaded.result);
|
|
24
|
+
if (uploaded.failed)
|
|
69
25
|
worstExit = 1;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const body = answer.body;
|
|
73
|
-
if (command.wait && body.noteId && body.transcription?.status === "pending") {
|
|
74
|
-
body.transcription = await waitForTranscription(door, body.noteId, read.direct);
|
|
75
|
-
}
|
|
76
|
-
if (body.transcription?.status === "failed")
|
|
77
|
-
worstExit = 1;
|
|
78
|
-
if (!door.json && body.transcription)
|
|
79
|
-
writeLine(door.io.stdout, `${body.noteId}: transcription ${body.transcription.status}${body.transcription.reason ? ": " + body.transcription.reason : ""}`);
|
|
80
|
-
results.push({ path: filePath, ok: body.stored === true && body.transcription?.status !== "failed", body });
|
|
81
|
-
if (body.stored !== true)
|
|
82
|
-
worstExit = 1;
|
|
83
|
-
if (!door.json)
|
|
84
|
-
sayUpload(door, body);
|
|
85
|
-
writeLine(door.io.stderr, `${TAG} upload answered ${JSON.stringify({
|
|
86
|
-
stored: body.stored === true,
|
|
87
|
-
note_id: body.noteId ?? null,
|
|
88
|
-
scope: body.scope ?? null,
|
|
89
|
-
byte_size: read.size,
|
|
90
|
-
direct: read.direct,
|
|
91
|
-
extension: read.extension,
|
|
92
|
-
})}`);
|
|
93
26
|
}
|
|
94
27
|
if (door.json) {
|
|
95
28
|
emit(door, { ok: worstExit === 0, uploaded: results.length, results });
|
|
96
29
|
}
|
|
97
30
|
return worstExit;
|
|
98
31
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (!decoded.ok) {
|
|
110
|
-
writeLine(door.io.stderr, `${TAG} file refused ${JSON.stringify({ reason: decoded.reason })}`);
|
|
111
|
-
return fail(door, decoded.reason, "That file is not text this command can decode (it may be a binary or an unusual encoding). Nothing was sent.");
|
|
112
|
-
}
|
|
113
|
-
text = decoded.text;
|
|
32
|
+
async function uploadOneNote(command, door, filePath) {
|
|
33
|
+
// Read and screen locally BEFORE any network call — a refusal here never
|
|
34
|
+
// reaches the dashboard, same discipline as `cockpit jarvis --image`.
|
|
35
|
+
const read = await readNoteFile(filePath, { allowDirectAudio: true });
|
|
36
|
+
if (!read.ok) {
|
|
37
|
+
const sentence = noteFileRefusalSentence(read.refusal, filePath);
|
|
38
|
+
writeLine(door.io.stderr, `${TAG} file refused ${JSON.stringify({ reason: read.refusal, detail: read.detail })}`);
|
|
39
|
+
if (!door.json)
|
|
40
|
+
writeLine(door.io.stderr, sentence);
|
|
41
|
+
return { result: { path: filePath, ok: false, reason: read.refusal }, failed: true };
|
|
114
42
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
43
|
+
if (!read.direct && read.bytes.byteLength >= NOTE_SLOW_UPLOAD_BYTES) {
|
|
44
|
+
// The route reads the whole note with one model call and may take
|
|
45
|
+
// minutes. A still cursor reads as a hang, so say what is happening.
|
|
46
|
+
writeLine(door.io.stderr, `Reading ${read.fileName} (${Math.round(read.bytes.byteLength / 1024)} KB). This can take a couple of minutes.`);
|
|
47
|
+
}
|
|
48
|
+
const answer = await sendNoteFile(command, door, filePath, read);
|
|
49
|
+
if (!answer.ok) {
|
|
50
|
+
writeLine(door.io.stderr, `${TAG} upload failed ${JSON.stringify({ reason: answer.reason })}`);
|
|
51
|
+
if (!door.json)
|
|
52
|
+
writeLine(door.io.stderr, `${read.fileName}: ${answer.detail}`);
|
|
53
|
+
return { result: { path: filePath, ok: false, reason: answer.reason }, failed: true };
|
|
54
|
+
}
|
|
55
|
+
const body = answer.body;
|
|
56
|
+
if (command.wait && body.noteId && body.transcription?.status === "pending") {
|
|
57
|
+
body.transcription = await waitForTranscription(door, body.noteId, read.direct);
|
|
58
|
+
}
|
|
59
|
+
let failed = body.transcription?.status === "failed";
|
|
60
|
+
if (!door.json && body.transcription)
|
|
61
|
+
writeLine(door.io.stdout, `${body.noteId}: transcription ${body.transcription.status}${body.transcription.reason ? ": " + body.transcription.reason : ""}`);
|
|
62
|
+
const result = { path: filePath, ok: body.stored === true && body.transcription?.status !== "failed", body };
|
|
63
|
+
if (body.stored !== true)
|
|
64
|
+
failed = true;
|
|
65
|
+
if (!door.json)
|
|
66
|
+
sayUpload(door, body);
|
|
67
|
+
writeLine(door.io.stderr, `${TAG} upload answered ${JSON.stringify({
|
|
68
|
+
stored: body.stored === true,
|
|
69
|
+
note_id: body.noteId ?? null,
|
|
70
|
+
scope: body.scope ?? null,
|
|
71
|
+
byte_size: read.size,
|
|
72
|
+
direct: read.direct,
|
|
73
|
+
extension: read.extension,
|
|
74
|
+
})}`);
|
|
75
|
+
return { result, failed };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A whole-meeting recording (over 4 MB) goes straight to storage and is
|
|
79
|
+
* transcribed in the background (BLI-12963); everything else keeps the
|
|
80
|
+
* upload route.
|
|
81
|
+
*/
|
|
82
|
+
async function sendNoteFile(command, door, filePath, read) {
|
|
83
|
+
if (read.direct) {
|
|
84
|
+
const direct = await uploadAudioDirect(door, {
|
|
85
|
+
filePath, fileName: read.fileName, size: read.size, exclude: command.exclude, folder: command.folder,
|
|
86
|
+
});
|
|
87
|
+
return direct.ok ? { ok: true, body: direct.body } : { ok: false, reason: direct.reason, detail: direct.detail };
|
|
130
88
|
}
|
|
89
|
+
const form = new FormData();
|
|
90
|
+
form.set("file", new File([new Uint8Array(read.bytes)], read.fileName, { type: noteAudioMime(read.fileName) ?? "" }));
|
|
91
|
+
if (command.exclude)
|
|
92
|
+
form.set("exclusions", command.exclude);
|
|
93
|
+
if (command.folder)
|
|
94
|
+
form.set("folder_path", command.folder);
|
|
95
|
+
return await ask(door, {
|
|
96
|
+
path: "/api/notes/upload",
|
|
97
|
+
method: "POST",
|
|
98
|
+
label: "notes upload",
|
|
99
|
+
timeoutMs: UPLOAD_DEADLINE_MS,
|
|
100
|
+
body: form,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export async function pasteNote(command, door) {
|
|
104
|
+
const pasted = command.filePath ? await readPasteFromFile(door, command.filePath) : await readPasteFromStdin(door);
|
|
105
|
+
if ("exitCode" in pasted)
|
|
106
|
+
return pasted.exitCode;
|
|
107
|
+
const text = pasted.text;
|
|
131
108
|
if (text.trim() === "") {
|
|
132
109
|
return fail(door, "empty_paste", "There was nothing to paste. Nothing was sent.");
|
|
133
110
|
}
|
|
@@ -171,6 +148,44 @@ export async function pasteNote(command, door) {
|
|
|
171
148
|
sayUpload(door, body);
|
|
172
149
|
return body.stored === true ? 0 : 1;
|
|
173
150
|
}
|
|
151
|
+
/** The note text from `--file`, or the exit code of the refusal already written. */
|
|
152
|
+
async function readPasteFromFile(door, filePath) {
|
|
153
|
+
const read = await readNoteFile(filePath);
|
|
154
|
+
if (!read.ok) {
|
|
155
|
+
const sentence = noteFileRefusalSentence(read.refusal, filePath);
|
|
156
|
+
writeLine(door.io.stderr, `${TAG} file refused ${JSON.stringify({ reason: read.refusal, detail: read.detail })}`);
|
|
157
|
+
return { exitCode: fail(door, read.refusal, sentence) };
|
|
158
|
+
}
|
|
159
|
+
const decoded = decodeTextBytes(read.bytes);
|
|
160
|
+
if (!decoded.ok) {
|
|
161
|
+
writeLine(door.io.stderr, `${TAG} file refused ${JSON.stringify({ reason: decoded.reason })}`);
|
|
162
|
+
return {
|
|
163
|
+
exitCode: fail(door, decoded.reason, "That file is not text this command can decode (it may be a binary or an unusual encoding). Nothing was sent."),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { text: decoded.text };
|
|
167
|
+
}
|
|
168
|
+
/** The note text piped on stdin, or the exit code of the refusal already written. */
|
|
169
|
+
async function readPasteFromStdin(door) {
|
|
170
|
+
if (isInteractiveStdin(door.io)) {
|
|
171
|
+
return {
|
|
172
|
+
exitCode: fail(door, "nothing_piped", "Pass --file <path> (safest on Windows), or pipe the note in "
|
|
173
|
+
+ "(`pbpaste | cockpit notes paste` on macOS; on Windows use PowerShell 7 — "
|
|
174
|
+
+ "Windows PowerShell 5.1 turns non-ASCII into `?` on pipes)."),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
return {
|
|
179
|
+
text: await readPipedText(door.io.stdin, {
|
|
180
|
+
maxChars: PASTE_MAX_CHARS,
|
|
181
|
+
overflowMessage: `A pasted note is limited to ${PASTE_MAX_CHARS} characters. Save it to a file and use --file instead.`,
|
|
182
|
+
}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return { exitCode: fail(door, "paste_too_long", errorText(error)) };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
174
189
|
export async function shareNote(command, door) {
|
|
175
190
|
const wantsToShare = command.action === "share";
|
|
176
191
|
// Only sharing asks. Taking a note back narrows who can read it, and nobody
|
package/dist/commands/notes.js
CHANGED
|
@@ -57,6 +57,38 @@ export { showGranolaStatus } from "./notes-granola-status.js";
|
|
|
57
57
|
export { showTranscriptionStats } from "./notes-transcription-stats.js";
|
|
58
58
|
export { listRecordings, showRecording } from "./notes-recordings.js";
|
|
59
59
|
export { ask, emit, fail, sayUpload, sayScope, errorText, TAG, } from "./notes-door.js";
|
|
60
|
+
/** Which function answers each `cockpit notes <verb>`; typed over every action so a new one cannot be forgotten. */
|
|
61
|
+
const NOTES_VERBS = {
|
|
62
|
+
folders: runFolderCommand,
|
|
63
|
+
mkdir: runFolderCommand,
|
|
64
|
+
rmdir: runFolderCommand,
|
|
65
|
+
rename: runFolderCommand,
|
|
66
|
+
list: listNotes,
|
|
67
|
+
shelves: listShelves,
|
|
68
|
+
show: showNote,
|
|
69
|
+
shelf: showShelf,
|
|
70
|
+
upload: uploadNotes,
|
|
71
|
+
paste: pasteNote,
|
|
72
|
+
share: shareNote,
|
|
73
|
+
unshare: shareNote,
|
|
74
|
+
summarize: summarizeNote,
|
|
75
|
+
move: moveNote,
|
|
76
|
+
place: placeNote,
|
|
77
|
+
// BLI-4394. `--show-webhook <id>` turns the list into one account's
|
|
78
|
+
// webhook URL, because that is the question a person asks WHILE looking
|
|
79
|
+
// at the list rather than a verb of its own.
|
|
80
|
+
accounts: (command, door) => (command.showWebhook ? showNotesWebhook(command, door) : listNotesAccounts(command, door)),
|
|
81
|
+
connect: connectNotetaker,
|
|
82
|
+
detach: detachNotetaker,
|
|
83
|
+
sync: syncNotetaker,
|
|
84
|
+
"sync-status": syncStatusNotetakers,
|
|
85
|
+
recordings: listRecordings,
|
|
86
|
+
recording: showRecording,
|
|
87
|
+
webhook: armNotesWebhook,
|
|
88
|
+
"bot-upcoming": showBotUpcoming,
|
|
89
|
+
"granola-status": showGranolaStatus,
|
|
90
|
+
"transcription-stats": showTranscriptionStats,
|
|
91
|
+
};
|
|
60
92
|
export async function runNotes(command, io) {
|
|
61
93
|
const session = await loadPairedSession("notes", command.homeDir);
|
|
62
94
|
const door = {
|
|
@@ -65,59 +97,5 @@ export async function runNotes(command, io) {
|
|
|
65
97
|
io,
|
|
66
98
|
json: command.json,
|
|
67
99
|
};
|
|
68
|
-
|
|
69
|
-
case "folders":
|
|
70
|
-
case "mkdir":
|
|
71
|
-
case "rmdir":
|
|
72
|
-
case "rename":
|
|
73
|
-
return runFolderCommand(command, door);
|
|
74
|
-
case "list":
|
|
75
|
-
return listNotes(command, door);
|
|
76
|
-
case "shelves":
|
|
77
|
-
return listShelves(command, door);
|
|
78
|
-
case "show":
|
|
79
|
-
return showNote(command, door);
|
|
80
|
-
case "shelf":
|
|
81
|
-
return showShelf(command, door);
|
|
82
|
-
case "upload":
|
|
83
|
-
return uploadNotes(command, door);
|
|
84
|
-
case "paste":
|
|
85
|
-
return pasteNote(command, door);
|
|
86
|
-
case "share":
|
|
87
|
-
case "unshare":
|
|
88
|
-
return shareNote(command, door);
|
|
89
|
-
case "summarize":
|
|
90
|
-
return summarizeNote(command, door);
|
|
91
|
-
case "move":
|
|
92
|
-
return moveNote(command, door);
|
|
93
|
-
case "place":
|
|
94
|
-
return placeNote(command, door);
|
|
95
|
-
case "accounts":
|
|
96
|
-
// BLI-4394. `--show-webhook <id>` turns the list into one account's
|
|
97
|
-
// webhook URL, because that is the question a person asks WHILE looking
|
|
98
|
-
// at the list rather than a verb of its own.
|
|
99
|
-
return command.showWebhook
|
|
100
|
-
? showNotesWebhook(command, door)
|
|
101
|
-
: listNotesAccounts(command, door);
|
|
102
|
-
case "connect":
|
|
103
|
-
return connectNotetaker(command, door);
|
|
104
|
-
case "detach":
|
|
105
|
-
return detachNotetaker(command, door);
|
|
106
|
-
case "sync":
|
|
107
|
-
return syncNotetaker(command, door);
|
|
108
|
-
case "sync-status":
|
|
109
|
-
return syncStatusNotetakers(command, door);
|
|
110
|
-
case "recordings":
|
|
111
|
-
return listRecordings(command, door);
|
|
112
|
-
case "recording":
|
|
113
|
-
return showRecording(command, door);
|
|
114
|
-
case "webhook":
|
|
115
|
-
return armNotesWebhook(command, door);
|
|
116
|
-
case "bot-upcoming":
|
|
117
|
-
return showBotUpcoming(command, door);
|
|
118
|
-
case "granola-status":
|
|
119
|
-
return showGranolaStatus(command, door);
|
|
120
|
-
case "transcription-stats":
|
|
121
|
-
return showTranscriptionStats(command, door);
|
|
122
|
-
}
|
|
100
|
+
return NOTES_VERBS[command.action](command, door);
|
|
123
101
|
}
|
|
@@ -8,7 +8,7 @@ import { runAttributedWorktreeSync, } from "./session-sync.js";
|
|
|
8
8
|
import { inspectLocalCollectorStatus, startLocalWorkContext, } from "../local-state.js";
|
|
9
9
|
/** The parent-folder arm: sync every discovered worktree, then backfill and finish setup once. */
|
|
10
10
|
export async function runOnboardMultiRepoFlow(ctx) {
|
|
11
|
-
const { command, resolvedCommand, collectionRoots,
|
|
11
|
+
const { command, resolvedCommand, collectionRoots, installEvents, io, worktrees, } = ctx;
|
|
12
12
|
const multi = await runMultiRepoOnboard(resolvedCommand, io, worktrees);
|
|
13
13
|
addInstallEvent(installEvents, "work_context", "ok");
|
|
14
14
|
addInstallEvent(installEvents, "sync", multi.ok ? "ok" : "fail", multi.ok ? undefined : "sync_blocked");
|
|
@@ -23,16 +23,7 @@ export async function runOnboardMultiRepoFlow(ctx) {
|
|
|
23
23
|
? undefined
|
|
24
24
|
: backfill.failureReason ?? "backfill_incomplete");
|
|
25
25
|
if (!completedBackfill) {
|
|
26
|
-
|
|
27
|
-
json: command.json,
|
|
28
|
-
base: onboardJsonPayload("blocked", resolvedCommand, install, pair, null, null, rootsResult),
|
|
29
|
-
extra: {
|
|
30
|
-
mode: "multi_repo",
|
|
31
|
-
repos: multi.results,
|
|
32
|
-
codex_sessions: multi.codex_sessions,
|
|
33
|
-
},
|
|
34
|
-
backfill,
|
|
35
|
-
});
|
|
26
|
+
reportMultiRepoBackfillBlocked(ctx, multi, backfill);
|
|
36
27
|
return finishOnboardRun(command, installEvents, io, 1);
|
|
37
28
|
}
|
|
38
29
|
const multiSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
|
|
@@ -43,9 +34,48 @@ export async function runOnboardMultiRepoFlow(ctx) {
|
|
|
43
34
|
});
|
|
44
35
|
return finishOnboardRun(command, installEvents, io, multiSetup.ok ? 0 : 1);
|
|
45
36
|
}
|
|
37
|
+
function reportMultiRepoBackfillBlocked(ctx, multi, backfill) {
|
|
38
|
+
const { command, resolvedCommand, rootsResult, install, pair, io } = ctx;
|
|
39
|
+
writeOnboardBackfillBlockedResult(io, {
|
|
40
|
+
json: command.json,
|
|
41
|
+
base: onboardJsonPayload("blocked", resolvedCommand, install, pair, null, null, rootsResult),
|
|
42
|
+
extra: {
|
|
43
|
+
mode: "multi_repo",
|
|
44
|
+
repos: multi.results,
|
|
45
|
+
codex_sessions: multi.codex_sessions,
|
|
46
|
+
},
|
|
47
|
+
backfill,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
46
50
|
/** The single-repo arm: start the work context, sync it, backfill, then finish setup once. */
|
|
47
51
|
export async function runOnboardSingleRepoFlow(ctx, single) {
|
|
48
|
-
const { command, resolvedCommand,
|
|
52
|
+
const { command, resolvedCommand, rootsResult, install, pair, installEvents, io } = ctx;
|
|
53
|
+
const { run, sync, status, worktreeRoot } = await startContextAndSync(ctx, single);
|
|
54
|
+
if (!run.ok || sync.status !== "uploaded") {
|
|
55
|
+
addInstallEvent(installEvents, "sync", "fail", "sync_blocked");
|
|
56
|
+
reportOnboardSyncBlocked(ctx, { run, sync, status, worktreeRoot });
|
|
57
|
+
return finishOnboardRun(command, installEvents, io, 1);
|
|
58
|
+
}
|
|
59
|
+
addInstallEvent(installEvents, "sync", "ok");
|
|
60
|
+
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
61
|
+
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
62
|
+
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
63
|
+
? undefined
|
|
64
|
+
: backfill.failureReason ?? "backfill_incomplete");
|
|
65
|
+
if (!completedBackfill) {
|
|
66
|
+
writeOnboardBackfillBlockedResult(io, {
|
|
67
|
+
json: command.json,
|
|
68
|
+
base: onboardJsonPayload("blocked", resolvedCommand, install, pair, sync, status, rootsResult),
|
|
69
|
+
extra: { codex_sessions: run.summary },
|
|
70
|
+
backfill,
|
|
71
|
+
});
|
|
72
|
+
return finishOnboardRun(command, installEvents, io, 1);
|
|
73
|
+
}
|
|
74
|
+
return await finishSingleRepoSetup(ctx, { run, sync, status, completedBackfill });
|
|
75
|
+
}
|
|
76
|
+
/** Work context started, the one worktree synced, and the collector's own reading of the repo. */
|
|
77
|
+
async function startContextAndSync(ctx, single) {
|
|
78
|
+
const { command, installEvents, io, worktrees } = ctx;
|
|
49
79
|
const worktreeRoot = worktrees[0]?.repo_root ?? single.primaryRoot;
|
|
50
80
|
const context = await startLocalWorkContext({
|
|
51
81
|
homeDir: command.homeDir,
|
|
@@ -74,26 +104,12 @@ export async function runOnboardSingleRepoFlow(ctx, single) {
|
|
|
74
104
|
repoRoot: worktreeRoot,
|
|
75
105
|
branch: command.branch,
|
|
76
106
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
84
|
-
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
85
|
-
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
86
|
-
? undefined
|
|
87
|
-
: backfill.failureReason ?? "backfill_incomplete");
|
|
88
|
-
if (!completedBackfill) {
|
|
89
|
-
writeOnboardBackfillBlockedResult(io, {
|
|
90
|
-
json: command.json,
|
|
91
|
-
base: onboardJsonPayload("blocked", resolvedCommand, install, pair, sync, status, rootsResult),
|
|
92
|
-
extra: { codex_sessions: run.summary },
|
|
93
|
-
backfill,
|
|
94
|
-
});
|
|
95
|
-
return finishOnboardRun(command, installEvents, io, 1);
|
|
96
|
-
}
|
|
107
|
+
return { run, sync, status, worktreeRoot };
|
|
108
|
+
}
|
|
109
|
+
/** Backfill is complete: finish setup once, and say it as JSON or as the narrative. */
|
|
110
|
+
async function finishSingleRepoSetup(ctx, facts) {
|
|
111
|
+
const { command, resolvedCommand, collectionRoots, pair, installEvents, io } = ctx;
|
|
112
|
+
const { run, sync, status, completedBackfill } = facts;
|
|
97
113
|
if (command.json) {
|
|
98
114
|
const jsonSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
|
|
99
115
|
writeOnboardSingleRepoResultJson(ctx, {
|
|
@@ -141,25 +157,27 @@ async function runMultiRepoOnboard(command, io, worktrees) {
|
|
|
141
157
|
fetchImpl: io.fetch,
|
|
142
158
|
});
|
|
143
159
|
const results = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
|
|
160
|
+
if (!command.json)
|
|
161
|
+
writeMultiRepoSyncReport(io, command, run, results);
|
|
162
|
+
return { ok: run.ok, results, codex_sessions: run.summary };
|
|
163
|
+
}
|
|
164
|
+
function writeMultiRepoSyncReport(io, command, run, results) {
|
|
144
165
|
const collectionRunStatus = attributedSyncRunStatus(run);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
writeLine(uploaded ? io.stdout : io.stderr, `${uploaded ? "SYNCED" : "BLOCKED"} ${row["repo_label"]}/${row["worktree_label"]} (${row["branch"]}) head:${shortSha(outcome.sync.head_sha ?? outcome.worktree.head_sha)} ${outcome.sync.status} objects:${outcome.sync.raw_evidence_uploaded_object_count} chunks:${outcome.sync.raw_evidence_uploaded_chunk_count} reused:${outcome.sync.raw_evidence_reused_count} failed:${outcome.sync.raw_evidence_failed_count} sessions:${row["attributed_session_count"]}${row["failure_reason"] ? ` reason:${row["failure_reason"]}` : ""}`);
|
|
152
|
-
}
|
|
153
|
-
writeLine(io.stdout, "4/5 Parent worktree sync complete.");
|
|
154
|
-
writeLine(io.stdout, `Uploaded: ${results.filter((row) => row["upload_status"] === "uploaded").length}/${results.length}`);
|
|
155
|
-
writeAgentSessionSummary(io, run.summary);
|
|
156
|
-
if (run.ok) {
|
|
157
|
-
writeLine(io.stdout, "Live sync receipts complete; verifying all-history Codex and Claude backfill.");
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
writeLine(io.stderr, `BLOCKED: Tower collection is ${collectionRunStatus}; retry until every eligible session has a durable receipt or explicit terminal reason.`);
|
|
161
|
-
}
|
|
162
|
-
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
166
|
+
for (const [index, outcome] of run.outcomes.entries()) {
|
|
167
|
+
const row = results[index];
|
|
168
|
+
if (!row)
|
|
169
|
+
continue;
|
|
170
|
+
const uploaded = outcome.sync.status === "uploaded";
|
|
171
|
+
writeLine(uploaded ? io.stdout : io.stderr, `${uploaded ? "SYNCED" : "BLOCKED"} ${row["repo_label"]}/${row["worktree_label"]} (${row["branch"]}) head:${shortSha(outcome.sync.head_sha ?? outcome.worktree.head_sha)} ${outcome.sync.status} objects:${outcome.sync.raw_evidence_uploaded_object_count} chunks:${outcome.sync.raw_evidence_uploaded_chunk_count} reused:${outcome.sync.raw_evidence_reused_count} failed:${outcome.sync.raw_evidence_failed_count} sessions:${row["attributed_session_count"]}${row["failure_reason"] ? ` reason:${row["failure_reason"]}` : ""}`);
|
|
163
172
|
}
|
|
164
|
-
|
|
173
|
+
writeLine(io.stdout, "4/5 Parent worktree sync complete.");
|
|
174
|
+
writeLine(io.stdout, `Uploaded: ${results.filter((row) => row["upload_status"] === "uploaded").length}/${results.length}`);
|
|
175
|
+
writeAgentSessionSummary(io, run.summary);
|
|
176
|
+
if (run.ok) {
|
|
177
|
+
writeLine(io.stdout, "Live sync receipts complete; verifying all-history Codex and Claude backfill.");
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
writeLine(io.stderr, `BLOCKED: Tower collection is ${collectionRunStatus}; retry until every eligible session has a durable receipt or explicit terminal reason.`);
|
|
181
|
+
}
|
|
182
|
+
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
165
183
|
}
|
package/dist/commands/onboard.js
CHANGED
|
@@ -16,9 +16,7 @@ export async function runOnboard(command, io) {
|
|
|
16
16
|
let install = null;
|
|
17
17
|
let pair = null;
|
|
18
18
|
let rootsResult = null;
|
|
19
|
-
//
|
|
20
|
-
// mutated through this holder rather than returned — a return only happens
|
|
21
|
-
// on the success path, and the failure path needs the value too.
|
|
19
|
+
// Both failure paths need the last sync value, even when a flow throws.
|
|
22
20
|
const syncHolder = {
|
|
23
21
|
current: null,
|
|
24
22
|
};
|
|
@@ -30,12 +28,7 @@ export async function runOnboard(command, io) {
|
|
|
30
28
|
const claimedOwnerEmail = await resolveOnboardEmail(roots.resolvedCommand, roots.collectionRoots, roots.existingConfig, io);
|
|
31
29
|
install = await installOnboardConfig(command, roots.resolvedRoots, installEvents, io);
|
|
32
30
|
pair = await pairForOnboarding(command, { primaryRoot: roots.primaryRoot, claimedOwnerEmail }, installEvents, io);
|
|
33
|
-
const worktrees = await
|
|
34
|
-
maxDepth: command.maxDepth,
|
|
35
|
-
maxRepos: command.maxRepos,
|
|
36
|
-
homeDir: command.homeDir,
|
|
37
|
-
allowEmpty: true,
|
|
38
|
-
}, io);
|
|
31
|
+
const worktrees = await discoverOnboardWorktrees(command, roots, io);
|
|
39
32
|
const ctx = {
|
|
40
33
|
command,
|
|
41
34
|
resolvedCommand: roots.resolvedCommand,
|
|
@@ -59,4 +52,13 @@ export async function runOnboard(command, io) {
|
|
|
59
52
|
catch (error) {
|
|
60
53
|
return await handleOnboardFailure(error, command, rootsResult, install, pair, syncHolder.current, installEvents, io);
|
|
61
54
|
}
|
|
55
|
+
}
|
|
56
|
+
async function discoverOnboardWorktrees(command, roots, io) {
|
|
57
|
+
const worktrees = await discoverCommandWorktrees(roots.collectionRoots, {
|
|
58
|
+
maxDepth: command.maxDepth,
|
|
59
|
+
maxRepos: command.maxRepos,
|
|
60
|
+
homeDir: command.homeDir,
|
|
61
|
+
allowEmpty: true,
|
|
62
|
+
}, io);
|
|
63
|
+
return worktrees;
|
|
62
64
|
}
|
|
@@ -49,6 +49,16 @@ export function renderMemoryUsage(memory, dim) {
|
|
|
49
49
|
return leftOrphan - rightOrphan;
|
|
50
50
|
return agentSaves(right.counts) - agentSaves(left.counts);
|
|
51
51
|
});
|
|
52
|
+
appendMemoryPeople(lines, ranked, days, nameWidth, dim);
|
|
53
|
+
if (memory.truncated) {
|
|
54
|
+
lines.push(dim(" this read stopped at its page cap, so every number above is a FLOOR — it can understate use and never invent it"));
|
|
55
|
+
}
|
|
56
|
+
if (memory.searches?.counted === false) {
|
|
57
|
+
lines.push(dim(` searches are not counted (${memory.searches.reason ?? "no ledger"}): nothing in Tower records one, so this is saves only`));
|
|
58
|
+
}
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
function appendMemoryPeople(lines, ranked, days, nameWidth, dim) {
|
|
52
62
|
for (const person of ranked) {
|
|
53
63
|
const byDay = new Map((person.days ?? []).map((entry) => [entry.day ?? "", entry]));
|
|
54
64
|
const cells = days
|
|
@@ -66,13 +76,6 @@ export function renderMemoryUsage(memory, dim) {
|
|
|
66
76
|
`${String(counts.import ?? 0).padStart(5)}`;
|
|
67
77
|
lines.push(agentSaves(counts) > 0 ? row : dim(row));
|
|
68
78
|
}
|
|
69
|
-
if (memory.truncated) {
|
|
70
|
-
lines.push(dim(" this read stopped at its page cap, so every number above is a FLOOR — it can understate use and never invent it"));
|
|
71
|
-
}
|
|
72
|
-
if (memory.searches?.counted === false) {
|
|
73
|
-
lines.push(dim(` searches are not counted (${memory.searches.reason ?? "no ledger"}): nothing in Tower records one, so this is saves only`));
|
|
74
|
-
}
|
|
75
|
-
return lines;
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
81
|
* The hook half: one line per machine, the server's own words.
|
|
@@ -102,36 +102,7 @@ options = {}) {
|
|
|
102
102
|
.join(" · ");
|
|
103
103
|
lines.push(`PIPELINES ${rows.length} job${rows.length === 1 ? "" : "s"}${summary ? ` · ${summary}` : ""}`);
|
|
104
104
|
lines.push("");
|
|
105
|
-
|
|
106
|
-
for (const row of rows) {
|
|
107
|
-
const verdict = verdictWord(row.verdict).padEnd(6);
|
|
108
|
-
const id = (row.id ?? "?").padEnd(idWidth);
|
|
109
|
-
// BLI-3723: a row that has no age BY DESIGN says `n/a`, never `—`. The two
|
|
110
|
-
// look alike and mean opposite things — `—` is "the table is empty".
|
|
111
|
-
const age = (row.ageAbsence ? "n/a" : ageWord(row.ageHours)).padStart(7);
|
|
112
|
-
// BLI-3722: name the device this age belongs to right in the row — a bare
|
|
113
|
-
// `STALE collector-fleet 11h` was already misread once (BLI-3699) as "the
|
|
114
|
-
// fleet", not "one machine in it", and the name is otherwise buried in
|
|
115
|
-
// `detail` below.
|
|
116
|
-
// BLI-4057 generalised it from `stale` alone: `memory-hooks` names the
|
|
117
|
-
// machine whose hooks stopped and reads `failing`, because nothing about
|
|
118
|
-
// it is late. A row that took the trouble to name a device is naming it
|
|
119
|
-
// for the reader, whatever word it printed beside it.
|
|
120
|
-
const staleName = row.staleDeviceName ? ` ${dim(`(${row.staleDeviceName})`)}` : "";
|
|
121
|
-
lines.push(`${verdict} ${id} ${age}${staleName} ${dim(intervalWord(row))}`);
|
|
122
|
-
if (row.verdict !== "healthy") {
|
|
123
|
-
// The absence of an age is explained BEFORE the detail, because it is
|
|
124
|
-
// the first thing a reader's eye stopped on.
|
|
125
|
-
if (row.ageAbsence)
|
|
126
|
-
lines.push(dim(` no age: ${row.ageAbsence}`));
|
|
127
|
-
if (row.detail)
|
|
128
|
-
lines.push(dim(` ${row.detail}`));
|
|
129
|
-
if (row.caveat)
|
|
130
|
-
lines.push(dim(` note: ${row.caveat}`));
|
|
131
|
-
if (row.configFile)
|
|
132
|
-
lines.push(dim(` scheduled by ${row.configFile} (${row.cron ?? "?"})`));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
105
|
+
appendPipelineRows(lines, rows, dim);
|
|
135
106
|
if (payload.fleet)
|
|
136
107
|
lines.push(...renderFleet(payload.fleet, dim));
|
|
137
108
|
if (payload.coverage) {
|
|
@@ -163,6 +134,38 @@ options = {}) {
|
|
|
163
134
|
}
|
|
164
135
|
return lines;
|
|
165
136
|
}
|
|
137
|
+
function appendPipelineRows(lines, rows, dim) {
|
|
138
|
+
const idWidth = Math.max(4, ...rows.map((row) => (row.id ?? "").length));
|
|
139
|
+
for (const row of rows) {
|
|
140
|
+
const verdict = verdictWord(row.verdict).padEnd(6);
|
|
141
|
+
const id = (row.id ?? "?").padEnd(idWidth);
|
|
142
|
+
// BLI-3723: a row that has no age BY DESIGN says `n/a`, never `—`. The two
|
|
143
|
+
// look alike and mean opposite things — `—` is "the table is empty".
|
|
144
|
+
const age = (row.ageAbsence ? "n/a" : ageWord(row.ageHours)).padStart(7);
|
|
145
|
+
// BLI-3722: name the device this age belongs to right in the row — a bare
|
|
146
|
+
// `STALE collector-fleet 11h` was already misread once (BLI-3699) as "the
|
|
147
|
+
// fleet", not "one machine in it", and the name is otherwise buried in
|
|
148
|
+
// `detail` below.
|
|
149
|
+
// BLI-4057 generalised it from `stale` alone: `memory-hooks` names the
|
|
150
|
+
// machine whose hooks stopped and reads `failing`, because nothing about
|
|
151
|
+
// it is late. A row that took the trouble to name a device is naming it
|
|
152
|
+
// for the reader, whatever word it printed beside it.
|
|
153
|
+
const staleName = row.staleDeviceName ? ` ${dim(`(${row.staleDeviceName})`)}` : "";
|
|
154
|
+
lines.push(`${verdict} ${id} ${age}${staleName} ${dim(intervalWord(row))}`);
|
|
155
|
+
if (row.verdict !== "healthy") {
|
|
156
|
+
// The absence of an age is explained BEFORE the detail, because it is
|
|
157
|
+
// the first thing a reader's eye stopped on.
|
|
158
|
+
if (row.ageAbsence)
|
|
159
|
+
lines.push(dim(` no age: ${row.ageAbsence}`));
|
|
160
|
+
if (row.detail)
|
|
161
|
+
lines.push(dim(` ${row.detail}`));
|
|
162
|
+
if (row.caveat)
|
|
163
|
+
lines.push(dim(` note: ${row.caveat}`));
|
|
164
|
+
if (row.configFile)
|
|
165
|
+
lines.push(dim(` scheduled by ${row.configFile} (${row.cron ?? "?"})`));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
166
169
|
/**
|
|
167
170
|
* The laptops. Red first, because a person reading this at 9am should not have
|
|
168
171
|
* to scroll past nine healthy machines to find the dead one.
|
package/dist/commands/ops.js
CHANGED
|
@@ -170,6 +170,9 @@ function writeHumanOpsStatus(command, io, payload, memory, models, turns, toolRo
|
|
|
170
170
|
for (const line of memory.experience.lines)
|
|
171
171
|
writeLine(io.stdout, ` ${line}`);
|
|
172
172
|
}
|
|
173
|
+
writeOptionalOpsSections(io, memory, models, turns, toolRouter);
|
|
174
|
+
}
|
|
175
|
+
function writeOptionalOpsSections(io, memory, models, turns, toolRouter) {
|
|
173
176
|
if (models) {
|
|
174
177
|
writeLine(io.stdout, "");
|
|
175
178
|
if (models.lines.length > 0) {
|