From 01f7bacaa3fd1bcff4691086fc349ece53c9d161 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:50:43 -0700 Subject: [PATCH] feat: run qrate from PATH when qrate did not start Pi The tools required QRATE_CLI, which only qrate's own Agent panel sets, so a Pi installed separately could load the extension but every tool failed. Fall back to the qrate command on PATH and name the caller pi unless QRATE_AGENT says otherwise. A missing command now says to install qrate and put it on PATH. --- README.md | 13 +++++++++++-- src/cli.ts | 19 +++++++++++++------ test/cli.test.ts | 24 +++++++++++++++++++++--- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 32f47bb..cabaa9d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,14 @@ uses `SYSTEM.md`, not the external-agent skill. The extension is not a qrate Lua not write archive cells: `qrate_stage_findings` only publishes drafts that the archivist may accept from qrate's existing Problems panel and Fixes menu. +## Use it in your own Pi + +Inside qrate you install nothing: the Agent panel loads this package for you. To use the same tools +in a Pi you installed yourself, you need qrate with its `qrate` command on `PATH` (check with +`qrate version`; the [command-line guide](https://qrate.dvnl.work/docs/cli) covers each platform). +Then load this package as a Pi extension and open your project in qrate. The tools call `qrate` from +`PATH` and show up in qrate's Agent panel under the name `pi`; set `QRATE_AGENT` to use another. + ## Development ```sh @@ -20,6 +28,7 @@ npm test The runtime contract is qrate's `qrate agent` command. qrate starts Pi with `QRATE_CLI` (the `qrate-cli` shipped beside the app) and `QRATE_AGENT=pi`; each tool runs -`$QRATE_CLI agent ` with its parameters as JSON on stdin. Exit `1` is a refusal with JSON -on stdout, and any other failure reports the CLI's stderr. A change to that command's contract +`$QRATE_CLI agent ` with its parameters as JSON on stdin. Without those variables the tools +run `qrate` from `PATH` as `pi`. Exit `1` is a refusal with JSON on stdout, and any other failure +reports the CLI's stderr. A change to that command's contract needs a new extension release and a new pin in qrate's `scripts/fetch-agent-runtime.*`. diff --git a/src/cli.ts b/src/cli.ts index 4558b49..029776d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -18,16 +18,21 @@ function refusal(stdout: string): string { return detail ? `${code}: ${detail}` : code; } -/** Runs `$QRATE_CLI agent ` with the params on stdin. qrate sets QRATE_CLI and QRATE_AGENT. */ +/** + * Runs `qrate agent ` with the params on stdin. qrate sets QRATE_CLI and QRATE_AGENT for + * its own Pi; a Pi you installed yourself finds `qrate` on PATH and is named `pi`. + */ export async function callQrate( method: string, params: Record | undefined, signal?: AbortSignal, ): Promise { - const cli = process.env.QRATE_CLI; - if (!cli) throw new QrateError("qrate did not provide its CLI path"); - - const child = spawn(cli, ["agent", method.replaceAll("_", "-")], { signal, windowsHide: true }); + const cli = process.env.QRATE_CLI || "qrate"; + const child = spawn(cli, ["agent", method.replaceAll("_", "-")], { + signal, + windowsHide: true, + env: { ...process.env, QRATE_AGENT: process.env.QRATE_AGENT || "pi" }, + }); let stdout = ""; let stderr = ""; child.stdout.setEncoding("utf8").on("data", (chunk: string) => (stdout += chunk)); @@ -36,7 +41,9 @@ export async function callQrate( child.stdin.end(JSON.stringify(params ?? {})); const code = await new Promise((resolve, reject) => { - child.on("error", (error) => reject(new QrateError(`qrate's CLI could not run: ${String(error)}`))); + child.on("error", (error) => + reject(new QrateError(`could not run \`${cli}\`; install qrate and put its command on PATH: ${String(error)}`)), + ); child.on("close", resolve); }); if (code === 1) throw new QrateError(`qrate refused the request: ${refusal(stdout)}`); diff --git a/test/cli.test.ts b/test/cli.test.ts index af82b61..dda7503 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -47,9 +47,27 @@ describe("qrate CLI client", () => { expect(JSON.parse(child.input)).toEqual({ revision: 3, findings: [] }); }); - it("refuses to run without qrate's CLI path", async () => { - await expect(callQrate("overview", undefined)).rejects.toThrow(QrateError); - expect(spawn).not.toHaveBeenCalled(); + it("finds qrate on PATH and names itself when qrate did not start it", async () => { + delete process.env.QRATE_AGENT; + spawn.mockReturnValue(fakeCli(0, "{}")); + await callQrate("overview", undefined); + expect(spawn.mock.calls[0]?.[0]).toBe("qrate"); + expect(spawn.mock.calls[0]?.[2].env.QRATE_AGENT).toBe("pi"); + + process.env.QRATE_AGENT = "pi/review-2"; + spawn.mockReturnValue(fakeCli(0, "{}")); + await callQrate("overview", undefined); + expect(spawn.mock.calls[1]?.[2].env.QRATE_AGENT).toBe("pi/review-2"); + delete process.env.QRATE_AGENT; + }); + + it("says how to fix a command that is not installed", async () => { + const child = fakeCli(0, ""); + spawn.mockReturnValue(child); + const call = callQrate("overview", undefined); + child.emit("error", new Error("spawn qrate ENOENT")); + await expect(call).rejects.toThrow(QrateError); + await expect(call).rejects.toThrow("put its command on PATH"); }); it("reports qrate's structured refusal code", async () => {