@bli-cockpit/cli 0.2.31 → 0.2.33
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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { containsSecretLikeContent, redactSecretLikeContent, } from "@bli-cockpit/telemetry-core";
|
|
1
|
+
import { containsSecretLikeContent, redactSecretLikeContent, scannedCleanRedactionMetadata, } from "@bli-cockpit/telemetry-core";
|
|
2
2
|
import { sha256 } from "./raw-evidence-keys.js";
|
|
3
3
|
import { describeError } from "../health-detail.js";
|
|
4
4
|
/** BLI-3238: a crashed redactor has to say so; see the catch below. */
|
|
@@ -165,32 +165,6 @@ function lineEndingOf(segment) {
|
|
|
165
165
|
return "\n";
|
|
166
166
|
return "";
|
|
167
167
|
}
|
|
168
|
-
/**
|
|
169
|
-
* The receipt for a file the scan cleared (BLI-3277).
|
|
170
|
-
*
|
|
171
|
-
* Same schema, same `mode` — the deterministic ruleset is what ran — with the
|
|
172
|
-
* verdict `scanned_clean` and zero of everything else. The content fields are
|
|
173
|
-
* the point: an evidence ref is only readable downstream when its redaction
|
|
174
|
-
* record hashes the bytes that are actually in the bucket, and for a clean file
|
|
175
|
-
* those are the original bytes, so both halves carry the same digest and size.
|
|
176
|
-
*/
|
|
177
|
-
function scannedCleanRedactionMetadata(originalBytes) {
|
|
178
|
-
const digest = sha256(originalBytes);
|
|
179
|
-
return {
|
|
180
|
-
schema_version: "raw-evidence-redaction.v1",
|
|
181
|
-
status: "scanned_clean",
|
|
182
|
-
mode: "deterministic_text_replacement",
|
|
183
|
-
applied_by: ["local_collector"],
|
|
184
|
-
rule_counts: [],
|
|
185
|
-
secret_like_match_count: 0,
|
|
186
|
-
redacted_fields: [],
|
|
187
|
-
redacted_ranges: [],
|
|
188
|
-
original_content_hash_sha256: digest,
|
|
189
|
-
sanitized_content_hash_sha256: digest,
|
|
190
|
-
original_byte_size: originalBytes.byteLength,
|
|
191
|
-
sanitized_byte_size: originalBytes.byteLength,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
168
|
function fallbackRedactionMetadata(options) {
|
|
195
169
|
const fullContentRedacted = options.fullContentRedacted !== false;
|
|
196
170
|
return {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal adapter for the shared JARVIS conversation gateway.
|
|
3
|
+
*
|
|
4
|
+
* This file owns terminal input and output only. Identity comes from the
|
|
5
|
+
* existing paired device session, and every turn is executed by the dashboard
|
|
6
|
+
* through the same JARVIS runtime used by web chat and Slack.
|
|
7
|
+
*/
|
|
8
|
+
import { errorMessage, isInteractiveStdin, readLine, writeLine } from "./cli-io.js";
|
|
9
|
+
import { getCollectorRuntimePaths, readLocalCollectorSessionFile, } from "../local-state.js";
|
|
10
|
+
export async function runJarvis(command, io) {
|
|
11
|
+
const session = await loadPairedSession(command.homeDir);
|
|
12
|
+
const dashboardUrl = command.dashboardUrl ?? session.dashboard_url;
|
|
13
|
+
const oneShotPrompt = await resolveOneShotPrompt(command, io);
|
|
14
|
+
if (oneShotPrompt !== null) {
|
|
15
|
+
return sendOneTurn({ command, dashboardUrl, deviceToken: session.device_token }, oneShotPrompt, io);
|
|
16
|
+
}
|
|
17
|
+
if (command.json) {
|
|
18
|
+
throw new Error("cockpit jarvis --json needs --prompt, positional text, or piped stdin.");
|
|
19
|
+
}
|
|
20
|
+
writeLine(io.stdout, "JARVIS terminal chat. Type /exit to leave.");
|
|
21
|
+
while (true) {
|
|
22
|
+
const prompt = (await readLine(io, "you> ")).trim();
|
|
23
|
+
if (!prompt)
|
|
24
|
+
continue;
|
|
25
|
+
if (prompt === "/exit" || prompt === "/quit")
|
|
26
|
+
return 0;
|
|
27
|
+
const exitCode = await sendOneTurn({ command, dashboardUrl, deviceToken: session.device_token }, prompt, io);
|
|
28
|
+
if (exitCode !== 0)
|
|
29
|
+
return exitCode;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function loadPairedSession(homeDir) {
|
|
33
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
34
|
+
try {
|
|
35
|
+
const session = await readLocalCollectorSessionFile(paths);
|
|
36
|
+
if (session.session_state !== "valid") {
|
|
37
|
+
throw new Error(`session_${session.session_state}`);
|
|
38
|
+
}
|
|
39
|
+
return session;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new Error(`JARVIS needs a valid paired Cockpit session. Run \`cockpit login\`, then try again (${errorMessage(error)}).`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function resolveOneShotPrompt(command, io) {
|
|
46
|
+
if (command.prompt)
|
|
47
|
+
return validatePrompt(command.prompt);
|
|
48
|
+
if (isInteractiveStdin(io))
|
|
49
|
+
return null;
|
|
50
|
+
const piped = await readAll(io.stdin);
|
|
51
|
+
return validatePrompt(piped);
|
|
52
|
+
}
|
|
53
|
+
async function sendOneTurn(context, prompt, io) {
|
|
54
|
+
const startedAt = Date.now();
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = await io.fetch(`${context.dashboardUrl}/api/jarvis/cli`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
authorization: `Bearer ${context.deviceToken}`,
|
|
61
|
+
"content-type": "application/json",
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
question: prompt,
|
|
65
|
+
thread: context.command.thread,
|
|
66
|
+
subject: context.command.subject,
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
writeFailure(context.command, io, "gateway_unreachable", errorMessage(error));
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
const body = await readReply(response);
|
|
75
|
+
if (!response.ok || !body.ok || !body.reply) {
|
|
76
|
+
const reason = body.error ?? body.reply ?? `http_${response.status}`;
|
|
77
|
+
writeFailure(context.command, io, "turn_failed", reason);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
if (context.command.json) {
|
|
81
|
+
writeLine(io.stdout, JSON.stringify({
|
|
82
|
+
ok: true,
|
|
83
|
+
reply: body.reply,
|
|
84
|
+
thread: body.thread ?? context.command.thread,
|
|
85
|
+
model: body.model ?? null,
|
|
86
|
+
subject: body.subject ?? null,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const subject = body.subject?.displayName ? ` (${body.subject.displayName})` : "";
|
|
91
|
+
writeLine(io.stdout, `jarvis${subject}> ${body.reply}`);
|
|
92
|
+
}
|
|
93
|
+
writeLine(io.stderr, `[jarvis cli] answered ${JSON.stringify({
|
|
94
|
+
prompt_length: prompt.length,
|
|
95
|
+
reply_length: body.reply.length,
|
|
96
|
+
elapsed_ms: Date.now() - startedAt,
|
|
97
|
+
thread: context.command.thread === "main" ? "default" : "named",
|
|
98
|
+
subject: context.command.subject ? "selected" : "caller",
|
|
99
|
+
})}`);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
async function readReply(response) {
|
|
103
|
+
const text = await response.text();
|
|
104
|
+
if (!text)
|
|
105
|
+
return { ok: false, error: "empty_response" };
|
|
106
|
+
try {
|
|
107
|
+
const value = JSON.parse(text);
|
|
108
|
+
if (!value || typeof value !== "object") {
|
|
109
|
+
return { ok: false, error: "response_body_not_object" };
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return { ok: false, error: "response_body_not_json" };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function writeFailure(command, io, reason, detail) {
|
|
118
|
+
if (command.json) {
|
|
119
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: reason, detail }));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
writeLine(io.stderr, `JARVIS could not answer: ${detail}`);
|
|
123
|
+
}
|
|
124
|
+
function validatePrompt(raw) {
|
|
125
|
+
const prompt = raw.trim();
|
|
126
|
+
if (!prompt)
|
|
127
|
+
throw new Error("JARVIS needs a non-empty question.");
|
|
128
|
+
if (prompt.length > 4000) {
|
|
129
|
+
throw new Error("JARVIS questions are limited to 4000 characters.");
|
|
130
|
+
}
|
|
131
|
+
return prompt;
|
|
132
|
+
}
|
|
133
|
+
async function readAll(stream) {
|
|
134
|
+
stream.setEncoding("utf8");
|
|
135
|
+
let text = "";
|
|
136
|
+
for await (const chunk of stream) {
|
|
137
|
+
text += chunk;
|
|
138
|
+
if (text.length > 4000) {
|
|
139
|
+
throw new Error("JARVIS questions are limited to 4000 characters.");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return text;
|
|
143
|
+
}
|
|
@@ -47,6 +47,8 @@ export function parseLocalArgs(argv) {
|
|
|
47
47
|
return parseSyncArgs(argv.slice(1));
|
|
48
48
|
case "analyze":
|
|
49
49
|
return parseAnalyzeArgs(argv.slice(1));
|
|
50
|
+
case "jarvis":
|
|
51
|
+
return parseJarvisArgs(argv.slice(1));
|
|
50
52
|
case "backfill":
|
|
51
53
|
return parseBackfillArgs(argv.slice(1));
|
|
52
54
|
case "status":
|
|
@@ -607,6 +609,30 @@ function parseAgentRulesHost(value) {
|
|
|
607
609
|
return host;
|
|
608
610
|
throw new Error("agent-rules --host must be codex, claude, or all.");
|
|
609
611
|
}
|
|
612
|
+
function parseJarvisArgs(args) {
|
|
613
|
+
const values = parseNamedArgs(args, {
|
|
614
|
+
allowedFlags: ["--home", "--dashboard-url", "--prompt", "--as", "--thread", "--json"],
|
|
615
|
+
valueFlags: ["--home", "--dashboard-url", "--prompt", "--as", "--thread"],
|
|
616
|
+
});
|
|
617
|
+
const flaggedPrompt = optionalNonEmpty(values.flags.get("--prompt"));
|
|
618
|
+
const positionalPrompt = optionalNonEmpty(values.positionals.join(" "));
|
|
619
|
+
if (flaggedPrompt && positionalPrompt) {
|
|
620
|
+
throw new Error("jarvis accepts either --prompt or positional text, not both.");
|
|
621
|
+
}
|
|
622
|
+
const thread = optionalNonEmpty(values.flags.get("--thread")) ?? "main";
|
|
623
|
+
if (!/^[A-Za-z0-9_-]{1,40}$/.test(thread)) {
|
|
624
|
+
throw new Error("jarvis --thread must use 1 to 40 letters, numbers, underscores, or hyphens.");
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
kind: "jarvis",
|
|
628
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
629
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
630
|
+
prompt: flaggedPrompt ?? positionalPrompt,
|
|
631
|
+
subject: optionalNonEmpty(values.flags.get("--as")),
|
|
632
|
+
thread,
|
|
633
|
+
json: values.booleans.has("--json"),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
610
636
|
function parseNamedArgs(args, options) {
|
|
611
637
|
const allowed = new Set(options.allowedFlags);
|
|
612
638
|
const valueFlags = new Set(options.valueFlags);
|
|
@@ -21,6 +21,7 @@ export const rootCommandNames = new Set([
|
|
|
21
21
|
"start",
|
|
22
22
|
"sync",
|
|
23
23
|
"analyze",
|
|
24
|
+
"jarvis",
|
|
24
25
|
"backfill",
|
|
25
26
|
"status",
|
|
26
27
|
"sessions",
|
|
@@ -45,6 +46,7 @@ export function localCommandHelp(command) {
|
|
|
45
46
|
" cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
46
47
|
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
47
48
|
" cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
49
|
+
" cockpit jarvis [question] [--prompt <question>] [--as <person>] [--thread <name>] [--dashboard-url <url>] [--json]",
|
|
48
50
|
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--yes] [--workspace <path>] [--json]",
|
|
49
51
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
50
52
|
" cockpit sessions [--source codex|claude] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
@@ -185,6 +187,20 @@ function localSubcommandHelp(command) {
|
|
|
185
187
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
186
188
|
],
|
|
187
189
|
],
|
|
190
|
+
[
|
|
191
|
+
"jarvis",
|
|
192
|
+
[
|
|
193
|
+
"Usage: cockpit jarvis [question] [--prompt <question>] [--as <person>] [--thread <name>] [--dashboard-url <url>] [--json]",
|
|
194
|
+
"",
|
|
195
|
+
"Chats with the same JARVIS used by Cockpit web chat and the BLI Slack DM.",
|
|
196
|
+
"--as selects the existing website person space; it changes who the chat is about, never who is authenticated.",
|
|
197
|
+
"Run with no question for an interactive terminal conversation.",
|
|
198
|
+
"Agents can pass --prompt, positional text, or pipe one question on stdin.",
|
|
199
|
+
"--json writes one machine-readable response to stdout; operational metadata stays on stderr.",
|
|
200
|
+
"The command uses the existing paired device identity. It cannot override the caller, team, role, or person scope.",
|
|
201
|
+
"Run `cockpit login` first if this machine is not paired.",
|
|
202
|
+
],
|
|
203
|
+
],
|
|
188
204
|
[
|
|
189
205
|
"backfill",
|
|
190
206
|
[
|
package/dist/commands/local.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* install-update.ts install, update, self-update, release
|
|
17
17
|
* status.ts `cockpit status`
|
|
18
18
|
* sessions.ts `cockpit sessions`
|
|
19
|
+
* jarvis.ts terminal adapter for the shared JARVIS gateway
|
|
19
20
|
*
|
|
20
21
|
* What stays here is orchestration: onboard, login/logout/start, the sync tick,
|
|
21
22
|
* analyze, serve, autostart and agent-rules.
|
|
@@ -32,6 +33,7 @@ import { attributedSyncRunStatus, cursorStatusLine, displayTicketId, rawEvidence
|
|
|
32
33
|
import { runInstall, runRelease, runSelfUpdate, runUpdate, SelfUpdateError, } from "./install-update.js";
|
|
33
34
|
import { runStatus } from "./status.js";
|
|
34
35
|
import { runSessions } from "./sessions.js";
|
|
36
|
+
import { runJarvis } from "./jarvis.js";
|
|
35
37
|
import { createCollectorServer } from "../server.js";
|
|
36
38
|
import { inspectAgentRules, installAgentRules, uninstallAgentRules, } from "../agent-rules.js";
|
|
37
39
|
import { backfillRetryCommand, runBackfill, runBackfillCommand, } from "./backfill.js";
|
|
@@ -98,6 +100,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
98
100
|
return await runSync(command, io);
|
|
99
101
|
case "analyze":
|
|
100
102
|
return await runAnalyze(command, io);
|
|
103
|
+
case "jarvis":
|
|
104
|
+
return await runJarvis(command, io);
|
|
101
105
|
case "backfill":
|
|
102
106
|
return await runBackfillCommand(command, io);
|
|
103
107
|
case "status":
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.33");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.33",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,6 @@
|
|
|
26
26
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
29
|
+
"@bli-cockpit/telemetry-core": "0.1.24"
|
|
30
30
|
}
|
|
31
31
|
}
|