@proagentstore/cli 0.4.158 → 0.4.160
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/browser-runner/coding/engine-adapter.js +5 -1
- package/dist/browser-runner/coding/grok-adapter.js +110 -0
- package/dist/browser-runner/coding/handlers.js +1 -1
- package/dist/browser-runner/coding/headless.js +3 -2
- package/dist/browser-runner/coding/pane-redact.js +5 -0
- package/dist/browser-runner/coding/secure-type.js +78 -0
- package/dist/browser-runner/server.js +3 -3
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { stripAnsi } from "./transcript-lines.js";
|
|
2
|
+
import { createGrokEngineAdapter, grokOutputFormat } from "./grok-adapter.js";
|
|
2
3
|
export function engineInvocationModeFromAdapter(mode) {
|
|
3
4
|
return mode === "stream-json" ? "structured" : "raw";
|
|
4
5
|
}
|
|
5
6
|
export function structuredCapableEngine(clientType) {
|
|
6
|
-
return clientType === "claude" || clientType === "codex";
|
|
7
|
+
return clientType === "claude" || clientType === "codex" || clientType === "grok";
|
|
7
8
|
}
|
|
8
9
|
export function engineInvocationWarning(clientType, mode) {
|
|
9
10
|
if (mode !== "raw" || !structuredCapableEngine(clientType))
|
|
@@ -234,5 +235,8 @@ export function engineAdapterFor(clientType, userArgs = []) {
|
|
|
234
235
|
return claudeEngineAdapter;
|
|
235
236
|
if (clientType === "codex" && userArgs[0] === "exec" && !["resume", "fork", "review", "help"].includes(userArgs[1] ?? ""))
|
|
236
237
|
return codexEngineAdapter;
|
|
238
|
+
// Grok's NDJSON stream (#1187) — unless the preset chose another output format on purpose, which stays raw.
|
|
239
|
+
if (clientType === "grok" && [null, "streaming-json"].includes(grokOutputFormat(userArgs)))
|
|
240
|
+
return createGrokEngineAdapter();
|
|
237
241
|
return genericRawEngineAdapter;
|
|
238
242
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
function record(v) {
|
|
2
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
3
|
+
}
|
|
4
|
+
function hasFlag(args, flag) {
|
|
5
|
+
return args.some((a) => a === flag || a.startsWith(`${flag}=`));
|
|
6
|
+
}
|
|
7
|
+
/** The value a preset gave `--output-format`, or null. */
|
|
8
|
+
export function grokOutputFormat(args) {
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
if (args[i] === "--output-format")
|
|
11
|
+
return args[i + 1] ?? null;
|
|
12
|
+
if (args[i].startsWith("--output-format="))
|
|
13
|
+
return args[i].slice("--output-format=".length);
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
/** The preset's argv with the structured-output flags in front — any order parses, and the trailing `-p` stays last. */
|
|
18
|
+
export function grokStructuredArgs(userArgs) {
|
|
19
|
+
const add = [];
|
|
20
|
+
if (!grokOutputFormat(userArgs))
|
|
21
|
+
add.push("--output-format", "streaming-json");
|
|
22
|
+
if (!hasFlag(userArgs, "--no-auto-update"))
|
|
23
|
+
add.push("--no-auto-update");
|
|
24
|
+
return [...add, ...userArgs];
|
|
25
|
+
}
|
|
26
|
+
/** The text a terminal call produced: its content blocks, else `output_for_prompt` without its `exit: N` header. */
|
|
27
|
+
function toolOutput(ev, raw) {
|
|
28
|
+
const texts = (Array.isArray(ev.content) ? ev.content : []).flatMap((c) => {
|
|
29
|
+
const inner = record(record(c)?.content);
|
|
30
|
+
return typeof inner?.text === "string" ? [inner.text] : [];
|
|
31
|
+
});
|
|
32
|
+
if (texts.some((t) => t.trim()))
|
|
33
|
+
return texts.join("");
|
|
34
|
+
const prompt = typeof raw?.output_for_prompt === "string" ? raw.output_for_prompt : "";
|
|
35
|
+
return prompt.replace(/^exit: -?\d+\n/, "");
|
|
36
|
+
}
|
|
37
|
+
/** A Grok call as the tool the rest of the runner knows: its terminal tool IS a Bash call. */
|
|
38
|
+
function asTool(ev) {
|
|
39
|
+
const rawInput = record(ev.rawInput) ?? {};
|
|
40
|
+
if (ev.kind === "execute" && typeof rawInput.command === "string")
|
|
41
|
+
return { name: "Bash", input: { command: rawInput.command } };
|
|
42
|
+
return { name: typeof ev.toolName === "string" && ev.toolName ? ev.toolName : String(ev.title ?? "tool"), input: rawInput };
|
|
43
|
+
}
|
|
44
|
+
/** One session's adapter: it holds the reply being streamed in deltas until something else happens. */
|
|
45
|
+
export function createGrokEngineAdapter() {
|
|
46
|
+
let text = "";
|
|
47
|
+
const flush = () => {
|
|
48
|
+
const t = text.trim();
|
|
49
|
+
text = "";
|
|
50
|
+
return t ? [{ kind: "assistant_text", text: t }] : [];
|
|
51
|
+
};
|
|
52
|
+
function parseLine(line) {
|
|
53
|
+
let ev;
|
|
54
|
+
try {
|
|
55
|
+
ev = record(JSON.parse(line));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
if (!ev)
|
|
61
|
+
return [];
|
|
62
|
+
switch (ev.type) {
|
|
63
|
+
case "text":
|
|
64
|
+
if (typeof ev.data === "string")
|
|
65
|
+
text += ev.data;
|
|
66
|
+
return [{ kind: "meta" }];
|
|
67
|
+
case "thought":
|
|
68
|
+
case "usage":
|
|
69
|
+
case "available_commands":
|
|
70
|
+
return [{ kind: "meta" }];
|
|
71
|
+
case "tool_call": {
|
|
72
|
+
const id = typeof ev.toolCallId === "string" ? ev.toolCallId : "";
|
|
73
|
+
const { name, input } = asTool(ev);
|
|
74
|
+
return [...flush(), { kind: "tool_use", block: { type: "tool_use", id, name, input }, id, name, input }];
|
|
75
|
+
}
|
|
76
|
+
case "tool_call_update": {
|
|
77
|
+
// Pending and in-progress updates are the same call filling in; only its END settles it.
|
|
78
|
+
if (ev.status !== "completed" && ev.status !== "failed")
|
|
79
|
+
return [{ kind: "meta" }];
|
|
80
|
+
const id = typeof ev.toolCallId === "string" ? ev.toolCallId : "";
|
|
81
|
+
const raw = record(ev.rawOutput);
|
|
82
|
+
const exitCode = typeof raw?.exit_code === "number" ? raw.exit_code : null;
|
|
83
|
+
const content = toolOutput(ev, raw);
|
|
84
|
+
const block = { type: "tool_result", tool_use_id: id, is_error: ev.status === "failed" || (exitCode !== null && exitCode !== 0), content };
|
|
85
|
+
return [{ kind: "tool_result", block, toolUseId: id, content }];
|
|
86
|
+
}
|
|
87
|
+
case "end": {
|
|
88
|
+
const stop = typeof ev.stopReason === "string" ? ev.stopReason : "";
|
|
89
|
+
const isError = stop !== "end_turn";
|
|
90
|
+
return [...flush(), { kind: "turn_end", raw: ev, isError, result: isError ? `stopped: ${stop || "unknown"}` : "" }];
|
|
91
|
+
}
|
|
92
|
+
case "error": {
|
|
93
|
+
const message = typeof ev.message === "string" ? ev.message : typeof record(ev.error)?.message === "string" ? record(ev.error)?.message : "";
|
|
94
|
+
return message.trim() ? [{ kind: "engine_error", text: message.trim() }] : [{ kind: "meta" }];
|
|
95
|
+
}
|
|
96
|
+
default:
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
mode: "stream-json",
|
|
102
|
+
persistent: false,
|
|
103
|
+
prepareArgs: grokStructuredArgs,
|
|
104
|
+
buildLaunchArgs: (userArgs) => [...userArgs],
|
|
105
|
+
buildTurnArgs: (userArgs, turnText) => [...userArgs, turnText],
|
|
106
|
+
parseLine,
|
|
107
|
+
// An older Grok CLI that does not know a flag refuses before it runs the prompt, so one retry raw is safe.
|
|
108
|
+
rejectsStructuredOutput: (line) => /(?:--output-format|--no-auto-update|streaming-json)/.test(line) && /(?:unexpected argument|unknown (?:argument|option)|unrecognized|invalid value|isn't a valid value)/i.test(line),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -15,7 +15,7 @@ const HANDLERS = {
|
|
|
15
15
|
claude: { clientType: "claude", cliCommand: "claude --dangerously-skip-permissions", envVar: "ANTHROPIC_API_KEY" },
|
|
16
16
|
gemini: { clientType: "gemini", cliCommand: "gemini --approval-mode yolo --skip-trust --prompt", envVar: "GEMINI_API_KEY" },
|
|
17
17
|
codex: { clientType: "codex", cliCommand: "codex exec --json --sandbox danger-full-access", envVar: "OPENAI_API_KEY" },
|
|
18
|
-
grok: { clientType: "grok", cliCommand: "grok --permission-mode bypassPermissions -p", envVar: "XAI_API_KEY" },
|
|
18
|
+
grok: { clientType: "grok", cliCommand: "grok --permission-mode bypassPermissions --no-auto-update --output-format streaming-json -p", envVar: "XAI_API_KEY" },
|
|
19
19
|
generic: { clientType: "generic", cliCommand: "bash", envVar: "" },
|
|
20
20
|
};
|
|
21
21
|
export function handlerFor(clientType) {
|
|
@@ -601,11 +601,12 @@ export class HeadlessSession {
|
|
|
601
601
|
if (this.config.clientType === "grok" && this.grokContinuity && !this.grokConversation)
|
|
602
602
|
this.grokConversation = newGrokConversation();
|
|
603
603
|
const grokConversation = this.config.clientType === "grok" && this.grokContinuity ? this.grokConversation : null;
|
|
604
|
+
const presetArgs = this.adapter.prepareArgs?.(this.cmdArgs) ?? this.cmdArgs; // #1187: Grok's structured flags; gone with the adapter if refused
|
|
604
605
|
const turnArgs = resumedCodexThreadId
|
|
605
606
|
? buildCodexResumeArgs(this.cmdArgs, resumedCodexThreadId, text)
|
|
606
607
|
: grokConversation
|
|
607
|
-
? grokTurnArgs(
|
|
608
|
-
: this.adapter.buildTurnArgs(
|
|
608
|
+
? grokTurnArgs(presetArgs, text, grokConversation)
|
|
609
|
+
: this.adapter.buildTurnArgs(presetArgs, text);
|
|
609
610
|
const env = this.spawnEnv;
|
|
610
611
|
const target = spawnTarget(this.cmdPrefix, this.cmdBin, turnArgs);
|
|
611
612
|
const launch = resolveEngineSpawn(target.command, target.args, { env });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { maskSecrets } from "../crash-reports.js";
|
|
2
|
+
import { machineSecureTypeDeps, secureType } from "./secure-type.js";
|
|
2
3
|
import { tmuxExec } from "./tmux.js";
|
|
3
4
|
/** How long a delivered value is masked in captures. */
|
|
4
5
|
export const INJECTED_TTL_MS = 24 * 60 * 60_000;
|
|
@@ -82,3 +83,7 @@ export function scrubDelivered(body) {
|
|
|
82
83
|
return { remembered: true, cleared: false };
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
/** The two secret routes a delivery uses: `/secure/type` (the value over stdin, #1143) and `/secure/scrub` (after it, #1166). */
|
|
87
|
+
export async function secureTerminalRoute(path, body) {
|
|
88
|
+
return path === "/secure/scrub" ? scrubDelivered(body) : secureType(body, await machineSecureTypeDeps());
|
|
89
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type a secret into a terminal WITHOUT putting it in any process's argv (#1143).
|
|
3
|
+
*
|
|
4
|
+
* `secure_input_inject` used to type through `/tmux/send` / `/terminal/send`, which run
|
|
5
|
+
* `tmux send-keys -t <session> -l <value>` and `kitty @ send-text --match id:<n> <value>`: the plaintext sat
|
|
6
|
+
* in a child process's argv for the life of the call, readable by any local user through `ps` or
|
|
7
|
+
* `/proc/<pid>/cmdline`, and by whatever records process launches. Here the value travels over STDIN:
|
|
8
|
+
*
|
|
9
|
+
* • tmux — `load-buffer -b <one-use name> -` (the value on stdin), then `paste-buffer -d -b <name> -t <session>`,
|
|
10
|
+
* which deletes the buffer as it pastes; a paste that fails deletes it anyway.
|
|
11
|
+
* • kitty — `kitty @ send-text --match id:<n> --stdin` (the value on stdin).
|
|
12
|
+
* • iTerm2 — refused: its `write text` takes the value inside an AppleScript argument, and always presses Return.
|
|
13
|
+
*
|
|
14
|
+
* Its own route (`POST /secure/type`), so ordinary `tmux_send_keys` text is unaffected, and so there is one body
|
|
15
|
+
* this runner promises never to log or put in an error: errors name the target, never the value — and with the
|
|
16
|
+
* value off argv, a failed command's own message (which quotes its argv) cannot carry it either.
|
|
17
|
+
*/
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { RunnerInputError } from "../errors.js";
|
|
21
|
+
/** Deliver `value` to `target` (a bare tmux session, `tmux:<session>` or `kitty:<id>`); the pane before and after. */
|
|
22
|
+
export async function secureType(body, deps) {
|
|
23
|
+
const target = typeof body.target === "string" ? body.target.trim() : "";
|
|
24
|
+
const value = typeof body.value === "string" ? body.value : "";
|
|
25
|
+
if (!target)
|
|
26
|
+
throw new RunnerInputError("A `target` is required.");
|
|
27
|
+
if (!value)
|
|
28
|
+
throw new RunnerInputError("Nothing to type.");
|
|
29
|
+
const opts = (input) => ({ input, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
30
|
+
const kitty = /^kitty:(.+)$/i.exec(target);
|
|
31
|
+
if (kitty) {
|
|
32
|
+
const paneBefore = deps.capture(kitty[1], "kitty");
|
|
33
|
+
try {
|
|
34
|
+
deps.exec("kitty", ["@", "send-text", "--match", `id:${kitty[1]}`, "--stdin"], opts(value));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error(`Typing into ${target} failed: kitty remote control is unavailable (allow_remote_control yes).`);
|
|
38
|
+
}
|
|
39
|
+
const pane = deps.capture(kitty[1], "kitty");
|
|
40
|
+
return { target, pane, paneBefore, changed: pane !== paneBefore };
|
|
41
|
+
}
|
|
42
|
+
if (/^iterm2:/i.test(target))
|
|
43
|
+
throw new RunnerInputError("iTerm2 cannot be typed into without the value in a script argument; use a tmux or kitty target.");
|
|
44
|
+
const session = target.replace(/^tmux:/i, "");
|
|
45
|
+
if (!deps.exists(session))
|
|
46
|
+
throw new RunnerInputError(`No tmux session "${session}".`, 404);
|
|
47
|
+
const paneBefore = deps.capture(session, "tmux");
|
|
48
|
+
const buffer = `pags-secure-${randomUUID()}`;
|
|
49
|
+
try {
|
|
50
|
+
deps.exec("tmux", ["load-buffer", "-b", buffer, "-"], opts(value));
|
|
51
|
+
deps.exec("tmux", ["paste-buffer", "-d", "-b", buffer, "-t", session], opts(""));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`Typing into "${session}" failed: tmux did not accept the paste.`);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
// `-d` deleted it on a paste that ran; this is the paste that did not.
|
|
58
|
+
try {
|
|
59
|
+
deps.exec("tmux", ["delete-buffer", "-b", buffer], opts(""));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Already gone — the normal case.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const pane = await deps.settle(session);
|
|
66
|
+
return { target, pane, paneBefore, changed: pane !== paneBefore };
|
|
67
|
+
}
|
|
68
|
+
/** The real hands. */
|
|
69
|
+
export async function machineSecureTypeDeps() {
|
|
70
|
+
const { capturePane, sessionExists, waitForPaneSettle } = await import("./tmux.js");
|
|
71
|
+
const { captureTerminalTarget } = await import("./terminal.js");
|
|
72
|
+
return {
|
|
73
|
+
exec: (file, args, opts) => execFileSync(file, args, opts),
|
|
74
|
+
exists: sessionExists,
|
|
75
|
+
capture: (t, backend) => (backend === "kitty" ? captureTerminalTarget(t, { backend: "kitty" }) : capturePane(t, 200)),
|
|
76
|
+
settle: (session) => waitForPaneSettle(session, { quietMs: 750, timeoutMs: 3_000 }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { outgoing, redactPanesOf,
|
|
1
|
+
import { outgoing, redactPanesOf, secureTerminalRoute } from "./coding/pane-redact.js";
|
|
2
2
|
import { codingSessionRefusal, instanceOf, readJson } from "./request-instance.js";
|
|
3
3
|
import { createServer, } from "node:http";
|
|
4
4
|
import { URL } from "node:url";
|
|
@@ -84,8 +84,8 @@ async function route(runner, req, res) {
|
|
|
84
84
|
return json(res, 403, { error: refused });
|
|
85
85
|
// Panes leave this machine redacted (#1166, coding/pane-redact.ts); a delivered secret is scrubbed.
|
|
86
86
|
redactPanesOf(res, path, req.method === "POST" && /^\/(tmux|terminal)\//.test(path) ? await readJson(req) : undefined);
|
|
87
|
-
if (req.method === "POST" && path === "/secure/scrub")
|
|
88
|
-
return json(res, 200,
|
|
87
|
+
if (req.method === "POST" && (path === "/secure/scrub" || path === "/secure/type"))
|
|
88
|
+
return json(res, 200, await secureTerminalRoute(path, await readJson(req)));
|
|
89
89
|
if ((req.method === "GET" || req.method === "POST") && path === "/health") {
|
|
90
90
|
// #896: a health check that cannot say WHO answered is not enough to decide anything with.
|
|
91
91
|
// A contender for the single-instance lock has a pid and a port from the lock file; what it
|