@bli-cockpit/cli 0.2.54 → 0.2.55
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 +247 -0
- package/dist/adapters/attribution-core-paths.js +182 -0
- package/dist/adapters/attribution-core-score.js +159 -0
- package/dist/adapters/attribution-core-types.js +13 -0
- package/dist/adapters/attribution-core.js +13 -565
- package/dist/adapters/claude-attribution-discovery.js +186 -0
- package/dist/adapters/claude-attribution-score.js +204 -0
- package/dist/adapters/claude-attribution-signals.js +180 -0
- package/dist/adapters/claude-attribution-types.js +25 -0
- package/dist/adapters/claude-attribution.js +14 -569
- package/dist/commands/doctor-access.js +129 -0
- package/dist/commands/doctor-pipeline.js +326 -0
- package/dist/commands/doctor-registration.js +105 -0
- package/dist/commands/doctor-report.js +111 -0
- package/dist/commands/doctor-update.js +120 -0
- package/dist/commands/doctor.js +8 -753
- package/dist/commands/heartbeat.js +8 -0
- package/dist/commands/jarvis-contracts.js +8 -0
- package/dist/commands/jarvis-render.js +413 -0
- package/dist/commands/jarvis-turn.js +305 -0
- package/dist/commands/jarvis.js +23 -698
- package/dist/commands/local-args-collector-setup.js +250 -0
- package/dist/commands/local-args-collector-status.js +227 -0
- package/dist/commands/local-args-collector-work.js +175 -0
- package/dist/commands/local-args-collector.js +19 -624
- package/dist/commands/local-args-tower-admin.js +456 -0
- package/dist/commands/local-args-tower-chat.js +194 -0
- package/dist/commands/local-args-tower-pages.js +314 -0
- package/dist/commands/local-args-tower.js +13 -880
- package/dist/commands/local-help.js +10 -2
- package/dist/commands/onboard-completion.js +136 -0
- package/dist/commands/onboard-flows.js +165 -0
- package/dist/commands/onboard-setup.js +102 -0
- package/dist/commands/onboard.js +5 -392
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-counters.js +55 -0
- package/dist/commands/session-sync-health.js +8 -1
- package/dist/commands/session-sync-plan.js +47 -7
- package/dist/commands/session-sync-scan.js +4 -4
- package/dist/commands/session-sync.js +6 -0
- package/dist/commands/settings-render.js +27 -0
- package/dist/commands/sync-followups.js +5 -1
- package/dist/commands/sync.js +5 -1
- package/dist/commands/team-device-reasons.js +16 -0
- package/dist/commands/team.js +87 -7
- package/dist/evidence-upload-client.js +14 -763
- package/dist/evidence-upload-object.js +181 -0
- package/dist/evidence-upload-plan.js +233 -0
- package/dist/evidence-upload-terminal.js +309 -0
- package/dist/evidence-upload-transport.js +104 -0
- package/dist/spool/local-spool-io.js +122 -0
- package/dist/spool/local-spool-mutations.js +174 -0
- package/dist/spool/local-spool-parse.js +143 -0
- package/dist/spool/local-spool-types.js +22 -0
- package/dist/spool/local-spool.js +20 -426
- package/dist/upload-evidence-delivery-offer.js +144 -0
- package/dist/upload-evidence-delivery-reconcile.js +134 -0
- package/dist/upload-evidence-delivery-summary.js +205 -0
- package/dist/upload-evidence-delivery.js +12 -482
- package/package.json +3 -3
|
@@ -102,6 +102,12 @@ export function buildCollectorHeartbeat(options) {
|
|
|
102
102
|
...(typeof options.facts.sessionsOutsideRoot === "number"
|
|
103
103
|
? { sessions_outside_root: options.facts.sessionsOutsideRoot }
|
|
104
104
|
: {}),
|
|
105
|
+
...(typeof options.facts.sessionsNewThisTick === "number"
|
|
106
|
+
? { sessions_new_this_tick: options.facts.sessionsNewThisTick }
|
|
107
|
+
: {}),
|
|
108
|
+
...(typeof options.facts.sessionsPendingUpload === "number"
|
|
109
|
+
? { sessions_pending_upload: options.facts.sessionsPendingUpload }
|
|
110
|
+
: {}),
|
|
105
111
|
};
|
|
106
112
|
}
|
|
107
113
|
/**
|
|
@@ -176,6 +182,8 @@ export async function sendCollectorHeartbeatBestEffort(options) {
|
|
|
176
182
|
sync_reason: heartbeat.last_sync_reason ?? null,
|
|
177
183
|
sessions_observed: heartbeat.sessions_observed ?? null,
|
|
178
184
|
sessions_outside_root: heartbeat.sessions_outside_root ?? null,
|
|
185
|
+
sessions_new_this_tick: heartbeat.sessions_new_this_tick ?? null,
|
|
186
|
+
sessions_pending_upload: heartbeat.sessions_pending_upload ?? null,
|
|
179
187
|
}));
|
|
180
188
|
return true;
|
|
181
189
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parsed `cockpit jarvis` command, and the reply shapes it reads off
|
|
3
|
+
* `/api/jarvis/cli` and `--threads`/`--history`. Every reply field is loose
|
|
4
|
+
* and optional on purpose — this package has no dependency on
|
|
5
|
+
* `apps/dashboard`, so a shape drifting a field should degrade to "say
|
|
6
|
+
* nothing about it" here, never a crash.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything around a turn that is not the network call itself: getting a
|
|
3
|
+
* turn ready to send (the question, and the attachment multipart body),
|
|
4
|
+
* reporting what it did (the failure sentence, the fallback-model receipt,
|
|
5
|
+
* the latency split), and drawing the answer in a terminal — live, settled,
|
|
6
|
+
* and its "how I got there" trace — since a terminal cannot unprint.
|
|
7
|
+
*/
|
|
8
|
+
import { colorEnabled, dim, isInteractiveStdin, readPipedText, writeFragment, writeLine } from "./cli-io.js";
|
|
9
|
+
import { attachedFileRefusalSentence, } from "./jarvis-attachment.js";
|
|
10
|
+
import { turnTimingFields } from "../tower-stream.js";
|
|
11
|
+
/**
|
|
12
|
+
* The dashboard route caps a turn at 120s (`maxDuration = 120`). The client
|
|
13
|
+
* waits slightly longer so the server's own named failure wins the race
|
|
14
|
+
* whenever it manages to send one; past that, the terminal names the timeout
|
|
15
|
+
* itself rather than sitting there.
|
|
16
|
+
*/
|
|
17
|
+
export const TURN_DEADLINE_MS = 125_000;
|
|
18
|
+
export async function resolveOneShotPrompt(command, io) {
|
|
19
|
+
if (command.prompt)
|
|
20
|
+
return validatePrompt(command.prompt);
|
|
21
|
+
if (isInteractiveStdin(io))
|
|
22
|
+
return null;
|
|
23
|
+
const piped = await readPipedText(io.stdin, { maxChars: 4000, overflowMessage: "JARVIS questions are limited to 4000 characters." });
|
|
24
|
+
return validatePrompt(piped);
|
|
25
|
+
}
|
|
26
|
+
export function validatePrompt(raw) {
|
|
27
|
+
const prompt = raw.trim();
|
|
28
|
+
if (!prompt)
|
|
29
|
+
throw new Error("JARVIS needs a non-empty question.");
|
|
30
|
+
if (prompt.length > 4000) {
|
|
31
|
+
throw new Error("JARVIS questions are limited to 4000 characters.");
|
|
32
|
+
}
|
|
33
|
+
return prompt;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The multipart body `/api/jarvis/cli` reads when a file is attached
|
|
37
|
+
* (BLI-3414) — same field names the request handler parses, mirroring the
|
|
38
|
+
* JSON body's fields plus one `image` file field.
|
|
39
|
+
*/
|
|
40
|
+
export function buildAttachmentForm(command, prompt, attachment) {
|
|
41
|
+
const form = new FormData();
|
|
42
|
+
form.set("question", prompt);
|
|
43
|
+
form.set("thread", command.thread);
|
|
44
|
+
if (command.subject)
|
|
45
|
+
form.set("subject", command.subject);
|
|
46
|
+
if (command.model)
|
|
47
|
+
form.set("model", command.model);
|
|
48
|
+
if (command.date)
|
|
49
|
+
form.set("date", command.date);
|
|
50
|
+
form.set("image", new File([attachment.bytes], attachment.fileName, { type: attachment.mimeType }));
|
|
51
|
+
return form;
|
|
52
|
+
}
|
|
53
|
+
/** A refusal caught locally, before any request went out — never a stack trace. */
|
|
54
|
+
export function writeAttachmentRefusal(command, io, refusal, filePath) {
|
|
55
|
+
const message = attachedFileRefusalSentence(refusal, filePath);
|
|
56
|
+
if (command.json) {
|
|
57
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: refusal, detail: message }));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
writeLine(io.stderr, `JARVIS could not attach that file: ${message}`);
|
|
61
|
+
}
|
|
62
|
+
writeLine(io.stderr, `[jarvis cli] attachment refused ${JSON.stringify({ reason: refusal })}`);
|
|
63
|
+
}
|
|
64
|
+
export function writeFailure(command, io, reason, detail) {
|
|
65
|
+
if (command.json) {
|
|
66
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: reason, detail }));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
writeLine(io.stderr, `JARVIS could not answer: ${detail}`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One line, only when the answer did not come from what was requested — a
|
|
73
|
+
* fallback, or an explicit `--model` that landed on a different model
|
|
74
|
+
* (BLI-3381). Silent otherwise, matching the web panel's own fallback notice.
|
|
75
|
+
*/
|
|
76
|
+
export function writeModelReceipt(io, model) {
|
|
77
|
+
if (!model)
|
|
78
|
+
return;
|
|
79
|
+
const requested = typeof model.requestedModel === "string" ? model.requestedModel : null;
|
|
80
|
+
const answered = namedModel(model.model);
|
|
81
|
+
const mismatched = requested !== null && answered !== null && requested !== answered;
|
|
82
|
+
if (!model.fallback && !mismatched)
|
|
83
|
+
return;
|
|
84
|
+
// BLI-3467: never the word "unavailable" — this side cannot know why, and
|
|
85
|
+
// on 2026-09-01 the real cause was a healthy provider refusing our own tool
|
|
86
|
+
// schema. Say what the receipt actually reports.
|
|
87
|
+
writeLine(io.stdout, `Model: ${answered ?? "an unknown model"} answered instead of ${requested ?? "the requested model"}`);
|
|
88
|
+
}
|
|
89
|
+
/** One millisecond span off the wire, or null for anything that is not a number. */
|
|
90
|
+
function latencyMs(value) {
|
|
91
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
92
|
+
}
|
|
93
|
+
/** The latency split for `--json`, always the same three keys so a consumer can rely on them. */
|
|
94
|
+
export function latencyFields(latency) {
|
|
95
|
+
return {
|
|
96
|
+
wireMs: latencyMs(latency?.wireMs),
|
|
97
|
+
prepMs: latencyMs(latency?.prepMs),
|
|
98
|
+
modelTtftMs: latencyMs(latency?.modelTtftMs),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** The same three spans as log fields, snake_case like everything else on that line. */
|
|
102
|
+
export function latencyLogFields(latency) {
|
|
103
|
+
const split = latencyFields(latency);
|
|
104
|
+
return {
|
|
105
|
+
wire_ms: split.wireMs,
|
|
106
|
+
prep_ms: split.prepMs,
|
|
107
|
+
model_ttft_ms: split.modelTtftMs,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The receipt's `model` field, unless it is the server's own "I could not tell"
|
|
112
|
+
* sentinel (BLI-3582).
|
|
113
|
+
*
|
|
114
|
+
* `modelReceipt` on the dashboard writes the literal string `"unknown"` when
|
|
115
|
+
* the run reported no model, and this side used to read that as a model NAME —
|
|
116
|
+
* so a turn whose model was simply unnamed printed
|
|
117
|
+
* `Model: unknown answered instead of gpt-5.6-terra`, a fallback notice for a
|
|
118
|
+
* fallback that never happened. Every streamed turn hit it. The dashboard fix
|
|
119
|
+
* makes the streamed path name the model again; this one makes the sentinel
|
|
120
|
+
* unable to invent a fallback on any dashboard version, old or new.
|
|
121
|
+
*/
|
|
122
|
+
function namedModel(value) {
|
|
123
|
+
if (typeof value !== "string")
|
|
124
|
+
return null;
|
|
125
|
+
const trimmed = value.trim();
|
|
126
|
+
if (!trimmed || trimmed.toLowerCase() === "unknown")
|
|
127
|
+
return null;
|
|
128
|
+
return trimmed;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The terminal's own spans as log fields (BLI-3591).
|
|
132
|
+
*
|
|
133
|
+
* Deliberately snake_case beside the dashboard's three, and deliberately
|
|
134
|
+
* distinct names: `model_ttft_ms` is the dashboard's clock, `first_token_ms`
|
|
135
|
+
* is this one, and confusing them is how a latency line stops being
|
|
136
|
+
* answerable.
|
|
137
|
+
*/
|
|
138
|
+
export function clientTimingFields(boot, turn) {
|
|
139
|
+
return {
|
|
140
|
+
boot_ms: boot.bootMs,
|
|
141
|
+
session_ms: boot.sessionMs,
|
|
142
|
+
prompt_ms: boot.promptMs,
|
|
143
|
+
pre_request_ms: turn.preRequestMs,
|
|
144
|
+
responded_ms: turn.respondedMs,
|
|
145
|
+
...turnTimingFields(turn.timing),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A citation line's link, as the dashboard mints it (BLI-3570):
|
|
150
|
+
* `Source: BLI-1234 — Judge outage <https://linear.app/…>`. The words are the
|
|
151
|
+
* receipt; the link is not part of the sentence, so the terminal prints it
|
|
152
|
+
* underneath, dim, rather than dragging a 118-character URL through the answer.
|
|
153
|
+
*/
|
|
154
|
+
const CITATION_LINK = /^(\s*(?:Source|Receipt)s?:.*?)\s*<(https?:\/\/[^>\s]+)>\s*$/i;
|
|
155
|
+
/**
|
|
156
|
+
* The answer, one line at a time, so a receipt's link gets its own dim line.
|
|
157
|
+
* Every other line is printed exactly as it arrived — this never rewrites the
|
|
158
|
+
* words, only where the link sits.
|
|
159
|
+
*/
|
|
160
|
+
export function writeReply(io, prefix, reply) {
|
|
161
|
+
const styled = colorEnabled(io);
|
|
162
|
+
const lines = reply.split("\n");
|
|
163
|
+
lines.forEach((line, index) => {
|
|
164
|
+
const head = index === 0 ? prefix : "";
|
|
165
|
+
const citation = line.match(CITATION_LINK);
|
|
166
|
+
if (!citation) {
|
|
167
|
+
writeLine(io.stdout, `${head}${line}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
writeLine(io.stdout, `${head}${citation[1]}`);
|
|
171
|
+
writeLine(io.stdout, dim(` ${citation[2]}`, styled));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The same link rule, applied to text that CONTINUES a line already on screen
|
|
176
|
+
* (BLI-3517 + BLI-3570).
|
|
177
|
+
*
|
|
178
|
+
* When the answer streamed, the citation block arrives after the last token,
|
|
179
|
+
* so `settle` has a tail to print rather than a whole reply. The first fragment
|
|
180
|
+
* is deliberately never tested against `CITATION_LINK`: it is the rest of a
|
|
181
|
+
* sentence the person is already reading, not a line of its own, and a regex
|
|
182
|
+
* anchored at `^` would be matching against a boundary that is not there.
|
|
183
|
+
* Every COMPLETE line after it gets the dim link treatment as usual.
|
|
184
|
+
*
|
|
185
|
+
* A citation that was already streamed inline cannot be split — a terminal
|
|
186
|
+
* cannot unprint — and that is the honest limit of this, not a bug to chase.
|
|
187
|
+
*/
|
|
188
|
+
export function writeReplyContinuation(io, tail) {
|
|
189
|
+
const styled = colorEnabled(io);
|
|
190
|
+
const [first = "", ...rest] = tail.split("\n");
|
|
191
|
+
writeFragment(io.stdout, first);
|
|
192
|
+
for (const line of rest) {
|
|
193
|
+
writeLine(io.stdout, "");
|
|
194
|
+
const citation = line.match(CITATION_LINK);
|
|
195
|
+
if (!citation) {
|
|
196
|
+
writeFragment(io.stdout, line);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
writeFragment(io.stdout, citation[1]);
|
|
200
|
+
writeLine(io.stdout, "");
|
|
201
|
+
writeFragment(io.stdout, dim(` ${citation[2]}`, styled));
|
|
202
|
+
}
|
|
203
|
+
writeLine(io.stdout, "");
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* What the terminal says when the grounding gate changed an answer it had
|
|
207
|
+
* already printed. The browser's own mark says the same thing in one word
|
|
208
|
+
* (`ANSWER_REVISED_NOTE`, `apps/dashboard/src/lib/webchat/tool-trace.ts`);
|
|
209
|
+
* this package cannot import from the dashboard, so the sentence is written
|
|
210
|
+
* here in the same register rather than shared through a dependency that does
|
|
211
|
+
* not exist.
|
|
212
|
+
*/
|
|
213
|
+
const ANSWER_REVISED_LINE = "revised: I checked that against its sources and changed what they did not back up";
|
|
214
|
+
/**
|
|
215
|
+
* The three lines a correction's rewrite prints (BLI-3567).
|
|
216
|
+
*
|
|
217
|
+
* The browsers replace the paragraph where it stands. A terminal cannot —
|
|
218
|
+
* stdout is a river — so it says the same three things in sequence instead:
|
|
219
|
+
* that a rewrite is under way, what the paragraph now says, and that the
|
|
220
|
+
* printed one above it did not change. The last of those is the honest half:
|
|
221
|
+
* without it a person would have two versions on screen and no idea which one
|
|
222
|
+
* JARVIS believes.
|
|
223
|
+
*
|
|
224
|
+
* `ANSWER_UPDATING_LINE` is the terminal's copy of the browsers'
|
|
225
|
+
* `ANSWER_UPDATING_LABEL` (`apps/dashboard/src/lib/webchat/tool-trace.ts`),
|
|
226
|
+
* word for word. This package cannot import from the dashboard, so it is
|
|
227
|
+
* written out here rather than shared through a dependency that does not exist
|
|
228
|
+
* — the same arrangement `ANSWER_REVISED_LINE` above already has.
|
|
229
|
+
*/
|
|
230
|
+
export const ANSWER_UPDATING_LINE = "(updating...)";
|
|
231
|
+
const ANSWER_CORRECTED_LINE = "revised after your correction";
|
|
232
|
+
const ANSWER_CORRECTED_NOTE_LINE = "The paragraph printed above is unchanged — a terminal cannot rewrite what it already " +
|
|
233
|
+
"printed. This is what it says now.";
|
|
234
|
+
const ANSWER_REWRITE_UNVERIFIED_LINE = "Your correction is recorded. I could not verify a rewrite of that paragraph against its " +
|
|
235
|
+
"sources, so it stays as it was.";
|
|
236
|
+
/**
|
|
237
|
+
* The answer as it is written, in a terminal (BLI-3517).
|
|
238
|
+
*
|
|
239
|
+
* A terminal cannot unprint. That single fact decides everything here:
|
|
240
|
+
*
|
|
241
|
+
* - The words stream out under the usual `jarvis> ` prefix, printed the moment
|
|
242
|
+
* they arrive rather than after the whole turn (a measured 4-16 s of nothing
|
|
243
|
+
* before this ticket).
|
|
244
|
+
* - A `reset` — the server taking back a draft the model wrote before deciding
|
|
245
|
+
* to call a tool — cannot erase what is on screen, so it SAYS so on its own
|
|
246
|
+
* line and the replacement follows. Never a retracted sentence left standing
|
|
247
|
+
* with nothing marking it.
|
|
248
|
+
* - The settled reply is the authority. The grounding gate runs on the whole
|
|
249
|
+
* text once the stream ends, so the answer can grow (citations, a fallback
|
|
250
|
+
* notice) or change. A pure continuation is printed as the remainder; a
|
|
251
|
+
* genuine change is reprinted whole under a line saying it was revised.
|
|
252
|
+
* - `--json` never streams a fragment: that contract is exactly one object on
|
|
253
|
+
* stdout, and every method here is a no-op for it.
|
|
254
|
+
*/
|
|
255
|
+
export function createLiveAnswer(command, io) {
|
|
256
|
+
const styled = colorEnabled(io);
|
|
257
|
+
const quiet = command.json === true;
|
|
258
|
+
let printed = "";
|
|
259
|
+
let opened = false;
|
|
260
|
+
const open = (subject) => {
|
|
261
|
+
if (opened)
|
|
262
|
+
return;
|
|
263
|
+
opened = true;
|
|
264
|
+
writeFragment(io.stdout, `jarvis${subject}> `);
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
token(event) {
|
|
268
|
+
if (quiet)
|
|
269
|
+
return;
|
|
270
|
+
const text = event.text ?? "";
|
|
271
|
+
if (event.reset) {
|
|
272
|
+
if (printed) {
|
|
273
|
+
writeLine(io.stdout, "");
|
|
274
|
+
writeLine(io.stdout, dim(" — that draft was replaced —", styled));
|
|
275
|
+
opened = false;
|
|
276
|
+
}
|
|
277
|
+
printed = "";
|
|
278
|
+
}
|
|
279
|
+
if (!text)
|
|
280
|
+
return;
|
|
281
|
+
open("");
|
|
282
|
+
printed += text;
|
|
283
|
+
writeFragment(io.stdout, text);
|
|
284
|
+
},
|
|
285
|
+
interrupt() {
|
|
286
|
+
if (quiet || !opened)
|
|
287
|
+
return;
|
|
288
|
+
writeLine(io.stdout, "");
|
|
289
|
+
opened = false;
|
|
290
|
+
},
|
|
291
|
+
settle(reply, subject, revised) {
|
|
292
|
+
if (quiet)
|
|
293
|
+
return;
|
|
294
|
+
if (!printed) {
|
|
295
|
+
// Nothing streamed: a non-streaming dashboard, `--no-stream`, or a
|
|
296
|
+
// turn whose words never arrived. Exactly the pre-ticket line, dim
|
|
297
|
+
// citation link and all (BLI-3570).
|
|
298
|
+
writeReply(io, `jarvis${subject}> `, reply);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (reply === printed) {
|
|
302
|
+
if (opened)
|
|
303
|
+
writeLine(io.stdout, "");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (reply.startsWith(printed)) {
|
|
307
|
+
// The gate only ADDED — citations going back under the answer, a
|
|
308
|
+
// fallback notice. Print the tail and leave the words alone; the
|
|
309
|
+
// citation block is exactly the thing BLI-3570 puts its link under, and
|
|
310
|
+
// it is arriving here rather than in the stream.
|
|
311
|
+
if (!opened)
|
|
312
|
+
writeFragment(io.stdout, `jarvis${subject}> `);
|
|
313
|
+
writeReplyContinuation(io, reply.slice(printed.length));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (opened)
|
|
317
|
+
writeLine(io.stdout, "");
|
|
318
|
+
writeLine(io.stdout, dim(revised ? ` — ${ANSWER_REVISED_LINE} —` : " — corrected —", styled));
|
|
319
|
+
writeReply(io, `jarvis${subject}> `, reply);
|
|
320
|
+
},
|
|
321
|
+
abandon() {
|
|
322
|
+
// The turn died with words already on screen. End the line so the
|
|
323
|
+
// failure sentence does not run on from a half-written answer.
|
|
324
|
+
if (!quiet && opened)
|
|
325
|
+
writeLine(io.stdout, "");
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Print what a correction did to the answer above, once the turn has settled.
|
|
331
|
+
*
|
|
332
|
+
* `--json` prints nothing: that contract is exactly one object on stdout, and
|
|
333
|
+
* the revision rides it as a field. Every branch either prints or is a
|
|
334
|
+
* deliberate no-op with a stderr line behind it in `sendOneTurn`.
|
|
335
|
+
*/
|
|
336
|
+
export function writeRevision(command, io, revision) {
|
|
337
|
+
if (command.json)
|
|
338
|
+
return;
|
|
339
|
+
const styled = colorEnabled(io);
|
|
340
|
+
if (revision.status === "unverified") {
|
|
341
|
+
writeLine(io.stdout, "");
|
|
342
|
+
writeLine(io.stdout, dim(` — ${ANSWER_REWRITE_UNVERIFIED_LINE}`, styled));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (revision.status !== "revised")
|
|
346
|
+
return;
|
|
347
|
+
const paragraphs = Array.isArray(revision.paragraphs) ? revision.paragraphs : [];
|
|
348
|
+
const written = paragraphs.filter((one) => typeof one === "object" && one !== null && typeof one.text === "string");
|
|
349
|
+
if (written.length === 0)
|
|
350
|
+
return;
|
|
351
|
+
writeLine(io.stdout, "");
|
|
352
|
+
writeLine(io.stdout, dim(` — ${ANSWER_CORRECTED_LINE} —`, styled));
|
|
353
|
+
for (const paragraph of written)
|
|
354
|
+
writeLine(io.stdout, paragraph.text);
|
|
355
|
+
writeLine(io.stdout, dim(` ${ANSWER_CORRECTED_NOTE_LINE}`, styled));
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* One dim line per tool the turn called (BLI-3381): what it was, how long it
|
|
359
|
+
* took, and — for a failure — the reason, matching what the web thinking
|
|
360
|
+
* trace shows (`describeToolTraceEvent` on the server side built these
|
|
361
|
+
* labels; this only renders them). Silent when no tool ran.
|
|
362
|
+
*/
|
|
363
|
+
export function writeTraceBlock(io, trace) {
|
|
364
|
+
if (!trace || trace.length === 0)
|
|
365
|
+
return;
|
|
366
|
+
for (const step of trace)
|
|
367
|
+
writeTraceLine(io, step);
|
|
368
|
+
}
|
|
369
|
+
/** The one dim trace line, drawn identically live (BLI-3457) and after the fact. */
|
|
370
|
+
export function writeTraceLine(io, step) {
|
|
371
|
+
const elapsed = typeof step.elapsedMs === "number" ? ` (${step.elapsedMs}ms)` : "";
|
|
372
|
+
const failure = step.status === "failed" ? ` — failed: ${step.detail ?? "no reason given"}` : "";
|
|
373
|
+
writeLine(io.stdout, dim(` ⏺ ${step.label}${elapsed}${failure}`, colorEnabled(io)));
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Draws one streamed tool call, and says whether it drew anything.
|
|
377
|
+
*
|
|
378
|
+
* Only settled steps print: a `running` event is the same tool arriving a
|
|
379
|
+
* second time, and printing both would double every line in a terminal that
|
|
380
|
+
* cannot rewrite the one above it.
|
|
381
|
+
*/
|
|
382
|
+
export function writeActivityLine(io, event) {
|
|
383
|
+
if (event.status !== "done" && event.status !== "failed")
|
|
384
|
+
return false;
|
|
385
|
+
const step = activityToStep(event);
|
|
386
|
+
if (!step)
|
|
387
|
+
return false;
|
|
388
|
+
writeTraceLine(io, step);
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
/** The settled steps of a streamed turn, in the shape `--json` already promises. */
|
|
392
|
+
export function activityToTrace(activity) {
|
|
393
|
+
const steps = [];
|
|
394
|
+
for (const event of activity) {
|
|
395
|
+
if (event.status !== "done" && event.status !== "failed")
|
|
396
|
+
continue;
|
|
397
|
+
const step = activityToStep(event);
|
|
398
|
+
if (step)
|
|
399
|
+
steps.push(step);
|
|
400
|
+
}
|
|
401
|
+
return steps;
|
|
402
|
+
}
|
|
403
|
+
function activityToStep(event) {
|
|
404
|
+
if (event.status !== "done" && event.status !== "failed")
|
|
405
|
+
return null;
|
|
406
|
+
return {
|
|
407
|
+
tool: event.tool ?? "unknown_tool",
|
|
408
|
+
label: event.label ?? event.tool ?? "A tool ran",
|
|
409
|
+
status: event.status,
|
|
410
|
+
...(typeof event.elapsedMs === "number" ? { elapsedMs: event.elapsedMs } : {}),
|
|
411
|
+
...(event.detail ? { detail: event.detail } : {}),
|
|
412
|
+
};
|
|
413
|
+
}
|