@bli-cockpit/cli 0.2.54 → 0.2.56

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.
Files changed (60) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +247 -0
  2. package/dist/adapters/attribution-core-paths.js +182 -0
  3. package/dist/adapters/attribution-core-score.js +159 -0
  4. package/dist/adapters/attribution-core-types.js +13 -0
  5. package/dist/adapters/attribution-core.js +13 -565
  6. package/dist/adapters/claude-attribution-discovery.js +186 -0
  7. package/dist/adapters/claude-attribution-score.js +204 -0
  8. package/dist/adapters/claude-attribution-signals.js +180 -0
  9. package/dist/adapters/claude-attribution-types.js +25 -0
  10. package/dist/adapters/claude-attribution.js +14 -569
  11. package/dist/commands/doctor-access.js +129 -0
  12. package/dist/commands/doctor-pipeline.js +326 -0
  13. package/dist/commands/doctor-registration.js +105 -0
  14. package/dist/commands/doctor-report.js +111 -0
  15. package/dist/commands/doctor-update.js +120 -0
  16. package/dist/commands/doctor.js +8 -753
  17. package/dist/commands/heartbeat.js +8 -0
  18. package/dist/commands/jarvis-contracts.js +8 -0
  19. package/dist/commands/jarvis-render.js +413 -0
  20. package/dist/commands/jarvis-turn.js +305 -0
  21. package/dist/commands/jarvis.js +23 -698
  22. package/dist/commands/local-args-collector-setup.js +250 -0
  23. package/dist/commands/local-args-collector-status.js +227 -0
  24. package/dist/commands/local-args-collector-work.js +175 -0
  25. package/dist/commands/local-args-collector.js +19 -624
  26. package/dist/commands/local-args-tower-admin.js +456 -0
  27. package/dist/commands/local-args-tower-chat.js +194 -0
  28. package/dist/commands/local-args-tower-pages.js +314 -0
  29. package/dist/commands/local-args-tower.js +13 -880
  30. package/dist/commands/local-help.js +10 -2
  31. package/dist/commands/onboard-completion.js +136 -0
  32. package/dist/commands/onboard-flows.js +165 -0
  33. package/dist/commands/onboard-setup.js +102 -0
  34. package/dist/commands/onboard.js +5 -392
  35. package/dist/commands/public-root.js +1 -1
  36. package/dist/commands/session-sync-counters.js +55 -0
  37. package/dist/commands/session-sync-health.js +8 -1
  38. package/dist/commands/session-sync-plan.js +47 -7
  39. package/dist/commands/session-sync-scan.js +4 -4
  40. package/dist/commands/session-sync.js +6 -0
  41. package/dist/commands/settings-render.js +27 -0
  42. package/dist/commands/sync-followups.js +5 -1
  43. package/dist/commands/sync.js +5 -1
  44. package/dist/commands/team-device-reasons.js +16 -0
  45. package/dist/commands/team.js +87 -7
  46. package/dist/evidence-upload-client.js +14 -763
  47. package/dist/evidence-upload-object.js +181 -0
  48. package/dist/evidence-upload-plan.js +233 -0
  49. package/dist/evidence-upload-terminal.js +309 -0
  50. package/dist/evidence-upload-transport.js +104 -0
  51. package/dist/spool/local-spool-io.js +122 -0
  52. package/dist/spool/local-spool-mutations.js +174 -0
  53. package/dist/spool/local-spool-parse.js +143 -0
  54. package/dist/spool/local-spool-types.js +22 -0
  55. package/dist/spool/local-spool.js +20 -426
  56. package/dist/upload-evidence-delivery-offer.js +144 -0
  57. package/dist/upload-evidence-delivery-reconcile.js +134 -0
  58. package/dist/upload-evidence-delivery-summary.js +205 -0
  59. package/dist/upload-evidence-delivery.js +12 -482
  60. package/package.json +3 -3
@@ -0,0 +1,305 @@
1
+ /**
2
+ * The turn engine, including `jarvis-turn`'s own turn: `sendOneTurn` (attach,
3
+ * request, stream, settle, print, log) and `readHistory` (`--threads` /
4
+ * `--thread <name> --history` — the same class of act with no model call,
5
+ * reading back what was already said). Everything each needs before the
6
+ * request or to draw the outcome lives in the render sibling —
7
+ * `jarvis-render.ts` — imported one direction only.
8
+ */
9
+ import { colorEnabled, dim, writeLine } from "./cli-io.js";
10
+ import { readAttachedImage } from "./jarvis-attachment.js";
11
+ import { rememberTurnTrace } from "./jarvis-trace.js";
12
+ import { towerFailureDetail, towerJsonRequest, towerRequest } from "../tower-client.js";
13
+ import { readTowerTurn, streamFailureDetail } from "../tower-stream.js";
14
+ import { activityToTrace, ANSWER_UPDATING_LINE, buildAttachmentForm, clientTimingFields, createLiveAnswer, latencyFields, latencyLogFields, TURN_DEADLINE_MS, writeActivityLine, writeAttachmentRefusal, writeFailure, writeModelReceipt, writeRevision, writeTraceBlock, } from "./jarvis-render.js";
15
+ export async function sendOneTurn(context, prompt, io) {
16
+ const startedAt = Date.now();
17
+ // BLI-3414: an attached file is read and locally screened (exists,
18
+ // readable, a supported extension, under the byte ceiling) BEFORE any
19
+ // network call — a refusal here never reaches the dashboard and is
20
+ // terminal, same discipline as the panel's own attached-image gate.
21
+ let attachment = null;
22
+ if (context.command.imagePath) {
23
+ const read = await readAttachedImage(context.command.imagePath);
24
+ if (!read.ok) {
25
+ writeAttachmentRefusal(context.command, io, read.refusal, context.command.imagePath);
26
+ return { exitCode: 1, reply: null };
27
+ }
28
+ attachment = read;
29
+ }
30
+ // BLI-3457: streaming is the default. A dashboard that has not shipped the
31
+ // NDJSON half yet answers `application/json`, which the reader takes as a
32
+ // single final event and names `stream_not_available` — so the terminal
33
+ // works against both server versions with no flag.
34
+ const wantsStream = context.command.stream !== false;
35
+ const log = (line) => writeLine(io.stderr, line);
36
+ // BLI-3591: everything this turn did before the question left the machine.
37
+ // There is no self-update probe, no floor check and no settings fetch on
38
+ // this path — the ONLY awaits between the command starting and the POST are
39
+ // the paired-session read, the piped-prompt read and an attached image, and
40
+ // all three are named. If this number is ever large, the step that made it
41
+ // large is on the same line.
42
+ const preRequestMs = Date.now() - startedAt;
43
+ const requested = await towerRequest({
44
+ dashboardUrl: context.dashboardUrl,
45
+ path: "/api/jarvis/cli",
46
+ deviceToken: context.deviceToken,
47
+ fetch: io.fetch,
48
+ label: "jarvis",
49
+ timeoutMs: TURN_DEADLINE_MS,
50
+ headers: wantsStream ? { accept: "application/x-ndjson" } : {},
51
+ body: attachment
52
+ ? buildAttachmentForm(context.command, prompt, attachment)
53
+ : {
54
+ question: prompt,
55
+ thread: context.command.thread,
56
+ subject: context.command.subject,
57
+ model: context.command.model,
58
+ // BLI-3484: which day's page this turn is about. Sent verbatim — the
59
+ // dashboard decides what counts as a date and whose day it is.
60
+ date: context.command.date,
61
+ // BLI-3567: the answer this session last printed, so a correction can
62
+ // rewrite the paragraph it contradicts. Absent on a one-shot turn and
63
+ // on the first turn of a session, which is how the dashboard knows
64
+ // there is nothing above to revise.
65
+ previousAnswer: context.previous?.answer,
66
+ previousQuestion: context.previous?.question,
67
+ },
68
+ log,
69
+ });
70
+ if (!requested.ok) {
71
+ writeFailure(context.command, io, requested.reason, towerFailureDetail(requested.reason, requested.detail));
72
+ return { exitCode: 1, reply: null };
73
+ }
74
+ // Live trace lines go to a person as they land, never to a `--json`
75
+ // consumer: that contract is exactly one object on stdout, so the events are
76
+ // buffered and folded into the final payload instead. The same rule governs
77
+ // the answer's own words (BLI-3517): `--json` stays exactly one object.
78
+ let liveTraceLines = 0;
79
+ const live = createLiveAnswer(context.command, io);
80
+ const turn = await readTowerTurn(requested.response, {
81
+ startedAt,
82
+ log,
83
+ onActivity: (event) => {
84
+ if (context.command.json)
85
+ return;
86
+ // A trace row must never land in the middle of a half-written sentence.
87
+ live.interrupt();
88
+ if (writeActivityLine(io, event))
89
+ liveTraceLines += 1;
90
+ },
91
+ onToken: (event) => live.token(event),
92
+ // BLI-3567: the paragraph being rewritten says so while it happens. The
93
+ // terminal cannot mark the paragraph itself — it printed it turns ago — so
94
+ // the pending state is one dim line, in the same words the browsers use.
95
+ onRevision: (event) => {
96
+ if (context.command.json)
97
+ return;
98
+ if (event.revision?.status !== "pending")
99
+ return;
100
+ live.interrupt();
101
+ writeLine(io.stdout, dim(` ${ANSWER_UPDATING_LINE}`, colorEnabled(io)));
102
+ },
103
+ onNote: (reason, detail) => {
104
+ log(`[jarvis cli] stream note ${JSON.stringify({
105
+ reason,
106
+ ...(detail && reason !== "ndjson_line_unparseable" ? { detail } : {}),
107
+ })}`);
108
+ },
109
+ });
110
+ if (!turn.ok) {
111
+ live.abandon();
112
+ writeFailure(context.command, io, turn.reason, streamFailureDetail(turn.reason, turn.detail));
113
+ return { exitCode: 1, reply: null };
114
+ }
115
+ const body = turn.final;
116
+ const httpStatus = typeof turn.final.httpStatus === "number" ? turn.final.httpStatus : requested.response.status;
117
+ if (httpStatus >= 400 || !body.ok || !body.reply) {
118
+ live.abandon();
119
+ const reason = body.error ?? body.reply ?? `http_${httpStatus}`;
120
+ writeFailure(context.command, io, "turn_failed", reason);
121
+ return { exitCode: 1, reply: null };
122
+ }
123
+ // A streaming server may leave the settled trace out of the final event
124
+ // because it already sent every step live; the activity we collected is that
125
+ // same trace, so `--json` still gets one.
126
+ const trace = body.trace ?? activityToTrace(turn.activity);
127
+ // BLI-3560: bookmark this turn so `cockpit jarvis --trace last` can open its
128
+ // step tree. Written before the reply is printed for no reason other than
129
+ // keeping the failure — which is only ever a stderr line — above the answer
130
+ // rather than after it.
131
+ await rememberTurnTrace({ traceId: body.traceId ?? null, threadId: body.traceThread ?? null }, io, context.command.homeDir);
132
+ if (context.command.json) {
133
+ writeLine(io.stdout, JSON.stringify({
134
+ ok: true,
135
+ reply: body.reply,
136
+ thread: body.thread ?? context.command.thread,
137
+ model: body.model ?? null,
138
+ trace,
139
+ subject: body.subject ?? null,
140
+ // BLI-3582: the server's own split of the wait. `null` from a
141
+ // dashboard that does not measure it yet.
142
+ latency: latencyFields(body.latency),
143
+ // BLI-3591: this side's own half of the same wait, so a consumer can
144
+ // put the whole journey together without a stopwatch of its own.
145
+ clientLatency: {
146
+ bootMs: context.boot.bootMs,
147
+ sessionMs: context.boot.sessionMs,
148
+ promptMs: context.boot.promptMs,
149
+ preRequestMs,
150
+ respondedMs: requested.respondedMs,
151
+ firstByteMs: turn.timing.firstByteMs,
152
+ firstFrameMs: turn.timing.firstFrameMs,
153
+ firstTokenMs: turn.timing.firstTokenMs,
154
+ elapsedMs: Date.now() - startedAt,
155
+ },
156
+ ...(body.traceId ? { traceId: body.traceId } : {}),
157
+ }));
158
+ }
159
+ else {
160
+ const subject = body.subject?.displayName ? ` (${body.subject.displayName})` : "";
161
+ // BLI-3517: when the answer streamed, `settle` prints only what the live
162
+ // words did not already say — and says so out loud when the grounding gate
163
+ // took some of them back. When nothing streamed it prints the whole reply
164
+ // through `writeReply`, which is byte-for-byte what this command printed
165
+ // before, BLI-3570's dim link line included.
166
+ live.settle(body.reply, subject, body.revised === true);
167
+ // BLI-3567: what the correction did to the answer above, printed under
168
+ // this turn's reply because that is the only place a terminal has.
169
+ if (body.revision)
170
+ writeRevision(context.command, io, body.revision);
171
+ // Only when nothing was drawn live — otherwise every tool would print twice.
172
+ if (liveTraceLines === 0)
173
+ writeTraceBlock(io, trace);
174
+ writeModelReceipt(io, body.model);
175
+ }
176
+ writeLine(io.stderr, `[jarvis cli] answered ${JSON.stringify({
177
+ prompt_length: prompt.length,
178
+ reply_length: body.reply.length,
179
+ trace_steps: trace?.length ?? 0,
180
+ trace_failed: trace?.filter((step) => step.status === "failed").length ?? 0,
181
+ model_requested: context.command.model ?? null,
182
+ elapsed_ms: Date.now() - startedAt,
183
+ thread: context.command.thread === "main" ? "default" : "named",
184
+ subject: context.command.subject ? "selected" : "caller",
185
+ image_attached: attachment !== null,
186
+ image_byte_size: attachment?.bytes.byteLength ?? null,
187
+ streamed: turn.streamed,
188
+ live_trace_lines: liveTraceLines,
189
+ // BLI-3517: whether the words arrived live, and whether the settled
190
+ // answer superseded them. `streamed_chars: 0` against a streaming
191
+ // dashboard means the answer landed all at once.
192
+ streamed_chars: turn.draft.length,
193
+ revised: body.revised === true,
194
+ // BLI-3567: whether this turn was told there was an answer above it, and
195
+ // what the correction did to it. `sent_previous_answer: false` is a
196
+ // one-shot turn; a status with no revision at all is a dashboard that
197
+ // predates the step.
198
+ sent_previous_answer: Boolean(context.previous),
199
+ revision: body.revision?.status ?? null,
200
+ revision_reason: body.revision?.reason ?? null,
201
+ // BLI-3582: `elapsed_ms` above is the whole wait as this side felt it;
202
+ // these three say which part of it was the dashboard's prep, which was
203
+ // the model's first token, and how long the socket stayed silent before
204
+ // the first byte. All null against a dashboard that does not send them.
205
+ ...latencyLogFields(body.latency),
206
+ // BLI-3591: the terminal's own half, on the same line, so the gap
207
+ // between the dashboard's first token and the first character a person
208
+ // sees stops being a mystery with nobody's name on it. `boot_ms` and
209
+ // `session_ms` are process facts (identical on every turn of an
210
+ // interactive session); the rest are this turn's, measured from the
211
+ // moment the turn started.
212
+ ...clientTimingFields(context.boot, {
213
+ preRequestMs,
214
+ respondedMs: requested.respondedMs,
215
+ timing: turn.timing,
216
+ }),
217
+ })}`);
218
+ return { exitCode: 0, reply: body.reply };
219
+ }
220
+ /**
221
+ * `--threads` and `--thread <name> --history`.
222
+ *
223
+ * The whole conversation lives on the server; nothing is cached locally, so a
224
+ * person who moves between machines sees the same history on both. The server
225
+ * scopes it to the device holder's own account — this command cannot ask for
226
+ * anybody else's, and there is no flag that would let it.
227
+ */
228
+ export async function readHistory(context, io) {
229
+ const { command } = context;
230
+ const params = new URLSearchParams();
231
+ if (command.history) {
232
+ params.set("thread", command.thread);
233
+ if (command.limit)
234
+ params.set("limit", String(command.limit));
235
+ }
236
+ else {
237
+ params.set("threads", "1");
238
+ }
239
+ const result = await towerJsonRequest({
240
+ dashboardUrl: context.dashboardUrl,
241
+ path: `/api/jarvis/cli?${params.toString()}`,
242
+ deviceToken: context.deviceToken,
243
+ fetch: io.fetch,
244
+ method: "GET",
245
+ label: command.history ? "jarvis:thread" : "jarvis:threads",
246
+ timeoutMs: 30_000,
247
+ log: (line) => writeLine(io.stderr, line),
248
+ });
249
+ if (!result.ok) {
250
+ writeFailure(command, io, result.reason, result.detail);
251
+ return 1;
252
+ }
253
+ const body = result.body;
254
+ if (body.ok === false) {
255
+ writeFailure(command, io, "history_unavailable", body.reply ?? "Tower had nothing to show.");
256
+ return 1;
257
+ }
258
+ if (command.json) {
259
+ writeLine(io.stdout, JSON.stringify(body));
260
+ }
261
+ else if (command.history) {
262
+ writeThreadHistory(io, body);
263
+ }
264
+ else {
265
+ writeThreadList(io, body.threads ?? []);
266
+ }
267
+ writeLine(io.stderr, `[jarvis cli] history read ${JSON.stringify({
268
+ mode: command.history ? "thread" : "threads",
269
+ thread_count: body.threads?.length ?? null,
270
+ message_count: body.messages?.length ?? null,
271
+ truncated: body.truncated ?? null,
272
+ })}`);
273
+ return 0;
274
+ }
275
+ function writeThreadList(io, threads) {
276
+ if (threads.length === 0) {
277
+ // A real answer, not a blank. Nothing here means nothing was ever asked
278
+ // from this terminal, which is worth saying rather than implying.
279
+ writeLine(io.stdout, "No terminal conversations yet. Ask something with `cockpit jarvis`.");
280
+ return;
281
+ }
282
+ const styled = colorEnabled(io);
283
+ for (const thread of threads) {
284
+ const turns = thread.turnCount === 1 ? "1 turn" : `${thread.turnCount ?? 0} turns`;
285
+ writeLine(io.stdout, `${thread.name} ${dim(`${turns} · ${thread.lastAt ?? "unknown"}`, styled)}`);
286
+ if (thread.preview)
287
+ writeLine(io.stdout, dim(` ${thread.preview}`, styled));
288
+ }
289
+ writeLine(io.stdout, "");
290
+ writeLine(io.stdout, dim("Replay one with `cockpit jarvis --thread <name> --history`.", styled));
291
+ }
292
+ function writeThreadHistory(io, body) {
293
+ const messages = body.messages ?? [];
294
+ if (messages.length === 0) {
295
+ writeLine(io.stdout, `Nothing has been said in “${body.thread ?? "that thread"}” yet.`);
296
+ return;
297
+ }
298
+ if (body.truncated) {
299
+ writeLine(io.stdout, dim(`Showing the most recent ${messages.length}; there is more before this (\`--limit\`).`, colorEnabled(io)));
300
+ }
301
+ for (const message of messages) {
302
+ const speaker = message.role === "you" ? "you" : "jarvis";
303
+ writeLine(io.stdout, `${speaker}> ${message.text ?? ""}`);
304
+ }
305
+ }