mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-13 18:49:34 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7207d64448 | ||
|
|
90e82d5ccc |
No files matched your search
@@ -70,11 +70,24 @@ codex mcp add openrig --env OPENRIG_SESSION_NAME=<address> -- rig mcp serve --fr
|
||||
|
||||
- **VS Code:** verified on 1.145. Older versions, such as 1.110, don't load Agent Plugins.
|
||||
- **Cursor:** verified on 3.27, as a local plugin.
|
||||
- **Copilot CLI:** not yet verified; the steps follow its documentation.
|
||||
- **Copilot CLI:** verified on 1.0.107. Install from this repository's marketplace, and trust the folder when Copilot
|
||||
CLI asks:
|
||||
|
||||
Install the plugin from this folder with the app's own install-from-folder step. In VS Code that's **Chat: Install Plugin
|
||||
from Source**; in Cursor, copy the folder to `~/.cursor/plugins/local/openrig` and reload. The app starts one `openrig`
|
||||
server, `rig mcp serve --front-door`, from the root `mcp.json`, and should load the skill.
|
||||
```bash
|
||||
copilot plugin marketplace add mvschwarz/openrig
|
||||
copilot plugin install openrig@openrig-app
|
||||
```
|
||||
|
||||
Unlike the other apps, this installs the plugin from this repository's main branch, not from your OpenRig package, so
|
||||
its skill can be newer than the OpenRig you run.
|
||||
|
||||
Copilot CLI also loads the skills in `~/.agents/skills`. Where OpenRig's own skills are installed there, it can choose
|
||||
OpenRig's `openrig-skills` skill before this plugin's `working-with-an-openrig-team`. If it does, it may ask for a
|
||||
skill pack that isn't there and get `pack_not_found`: ask it to use `working-with-an-openrig-team`.
|
||||
|
||||
In VS Code and Cursor, install the plugin from this folder with the app's own install-from-folder step. In VS Code
|
||||
that's **Chat: Install Plugin from Source**; in Cursor, copy the folder to `~/.cursor/plugins/local/openrig` and reload.
|
||||
The app starts one `openrig` server, `rig mcp serve --front-door`, from the root `mcp.json`, and should load the skill.
|
||||
|
||||
- **No seat:** these apps can't set a seat for a plugin's server, so it starts with none. An app started from a shell
|
||||
where `OPENRIG_SESSION_NAME` (or the older `RIGGED_SESSION_NAME`) is set passes it on, but only for that launch: on
|
||||
@@ -90,14 +103,20 @@ server, `rig mcp serve --front-door`, from the root `mcp.json`, and should load
|
||||
- Environment export needs an HTTP work grant, which a plugin's local server never has.
|
||||
- **To give the app a seat,** add your own MCP server that runs `rig mcp serve --front-door` with
|
||||
`OPENRIG_SESSION_NAME=<address>` in its environment, and turn the plugin's `openrig` server off, so the app runs one
|
||||
OpenRig server. The plugin's skill stays installed. Verified on VS Code 1.145 and Cursor 3.27, where the team's
|
||||
questions then reach you and your answers are recorded, across a reopen:
|
||||
OpenRig server. The plugin's skill stays installed. Verified on VS Code 1.145, Cursor 3.27 and Copilot CLI 1.0.107,
|
||||
where the team's questions then reach you and your answers are recorded, across a reopen:
|
||||
- **VS Code:** add it to your user `mcp.json` under a name other than `openrig`, such as `openrig-seat`, and disable
|
||||
the plugin's `openrig` in **MCP: List Servers**. Two servers named `openrig` are hard to tell apart, and starting
|
||||
one can stop the other.
|
||||
- **Cursor:** add it to `.cursor/mcp.json` (the project's or your user one), turn it on, and disable
|
||||
**openrig · Plugin** in **Customize → MCPs**.
|
||||
- **Copilot CLI:** add it the CLI's own way and turn the plugin's server off; not yet verified.
|
||||
- **Copilot CLI:** add it, then turn the plugin's server off. Copilot CLI saves both, so the seat holds in new
|
||||
sessions:
|
||||
|
||||
```bash
|
||||
copilot mcp add openrig-seat --env OPENRIG_SESSION_NAME=<address> -- rig mcp serve --front-door
|
||||
copilot mcp disable openrig
|
||||
```
|
||||
|
||||
## What it doesn't do
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ export function daemonStartArgs(target: string): string[] {
|
||||
|| url.username || url.password || url.search || url.hash || url.pathname !== "/") {
|
||||
throw new Error("This target is not a local daemon endpoint. Reconnect after its owner starts it; no local daemon was started.");
|
||||
}
|
||||
// The daemon's default bind already listens on 127.0.0.1, with Tailscale beside it when present; naming that host would
|
||||
// make it the only listener. Another loopback spelling isn't in the default bind, so it's still named.
|
||||
const host = url.hostname.replace(/^\[|\]$/g, "");
|
||||
return ["daemon", "start", "--no-kernel", ...(host === "127.0.0.1" ? [] : ["--host", host]), "--port", url.port || "80"];
|
||||
return ["daemon", "start", "--no-kernel", "--host", url.hostname.replace(/^\[|\]$/g, ""),
|
||||
"--port", url.port || "80"];
|
||||
}
|
||||
@@ -389,8 +389,6 @@ async function run(): Promise<void> {
|
||||
} else {
|
||||
void refreshCrashCart();
|
||||
void live?.refresh();
|
||||
// Daemon only: no seat was started. Start and return lists the rigs, the kernel first, to bring one back.
|
||||
if (startup) view.dispatch({ type: "notice", message: "Daemon started; this start didn't start any seats. Press S to choose seats to bring back, the kernel's first." });
|
||||
}
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -163,8 +163,6 @@ export class StartupController {
|
||||
const failure = s.notice;
|
||||
await this.refresh();
|
||||
if (!confirmed) { s.notice = failure; s.detail = failure; this.changed(); }
|
||||
// refresh() moves the page on; read it again rather than through the narrowed `s.page`.
|
||||
else if (this.state.page === "rigs") { s.notice = daemonOnlyNotice(s.rigs); this.changed(); }
|
||||
return;
|
||||
}
|
||||
if (s.page === "rigs" && key === "k" && !s.rigs.some((r) => r.name === "kernel")) {
|
||||
@@ -246,14 +244,6 @@ export class StartupController {
|
||||
}
|
||||
}
|
||||
|
||||
/** After a daemon-only start: this start brought back no seat, and the existing way to bring one back is the rig list
|
||||
* here, where the kernel comes first. It says nothing about why any seat isn't running. */
|
||||
export function daemonOnlyNotice(rigs: Array<{ name: string }>): string {
|
||||
return rigs.some((r) => r.name === "kernel")
|
||||
? "Daemon started; this start didn't start any seats. The kernel is listed first: choose it to see its seats and resume one."
|
||||
: "Daemon started; this start didn't start any seats. Choose a rig, or press k to set up a kernel.";
|
||||
}
|
||||
|
||||
export function startupLines(s: StartupState): Array<{ text: string; action?: Action }> {
|
||||
const button = (text: string, key: string) => ({ text, action: { type: "startup" as const, key } });
|
||||
const lines: Array<{ text: string; action?: Action }> = [
|
||||
|
||||
@@ -6,16 +6,10 @@ import { resolveCrashCartKey } from "../src/crash-cart/keys.js";
|
||||
|
||||
describe("local startup target", () => {
|
||||
it("starts only the intended local daemon, leaving occupant selection to the user", () => {
|
||||
// The default bind is 127.0.0.1 plus Tailscale when present, so naming 127.0.0.1 would drop the Tailscale listener.
|
||||
expect(daemonStartArgs("http://127.0.0.1:17433")).toEqual([
|
||||
"daemon", "start", "--no-kernel", "--port", "17433",
|
||||
"daemon", "start", "--no-kernel", "--host", "127.0.0.1", "--port", "17433",
|
||||
]);
|
||||
});
|
||||
it.each([["http://localhost:7433", "localhost"], ["http://[::1]:7433", "::1"]])(
|
||||
"names %s's host, which the default bind doesn't listen on", (target, host) => {
|
||||
expect(daemonStartArgs(target)).toEqual(["daemon", "start", "--no-kernel", "--host", host, "--port", "7433"]);
|
||||
},
|
||||
);
|
||||
it.each(["https://example.test", "http://10.0.0.2:7433", "http://user:password@localhost:7433", "http://localhost:7433/other"])(
|
||||
"does not turn %s into local startup authority", (target) => {
|
||||
expect(() => daemonStartArgs(target)).toThrow("no local daemon was started");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { DaemonClient, launchNodeNotice } from "../src/daemon-client.js";
|
||||
import { StartupController, daemonOnlyNotice, startupLines, type StartupSeat } from "../src/startup.js";
|
||||
import { StartupController, startupLines, type StartupSeat } from "../src/startup.js";
|
||||
import { createViewState, emptySnapshot } from "../src/state.js";
|
||||
import { renderScreen } from "../src/render.js";
|
||||
|
||||
@@ -118,11 +118,6 @@ describe("TUI startup choices", () => {
|
||||
expect(f.startDaemon).toHaveBeenCalledTimes(1);
|
||||
expect(f.posts).toEqual([]);
|
||||
expect(f.controller.state.page).toBe("rigs");
|
||||
// It names the existing way back for the kernel, and claims nothing about why it isn't running.
|
||||
expect(f.controller.state.notice).toBe("Daemon started; this start didn't start any seats. The kernel is listed first: choose it to see its seats and resume one.");
|
||||
});
|
||||
it("with no kernel rig, a daemon-only start points at choosing a rig or setting up a kernel", () => {
|
||||
expect(daemonOnlyNotice([{ name: "work" }])).toBe("Daemon started; this start didn't start any seats. Choose a rig, or press k to set up a kernel.");
|
||||
});
|
||||
it("decline and refresh invalidate fresh consent with no launch effect", async () => {
|
||||
const f = fixture(); await chooseOperator(f);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Lists lines added by a change that contain values tied to one machine, network or account, so
|
||||
// the author can replace them with portable ones or keep them on purpose. A finding is a question, not a verdict: the default
|
||||
// exits 0 with or without findings, --strict exits 1 when findings exist, and 2 is an operational error (bad argument, git failure).
|
||||
// the author can replace them with portable ones or keep them on purpose. Findings never fail it: a finding is a question, not a verdict. It exits 0
|
||||
// with or without findings and 2 on an operational error (bad argument, git failure).
|
||||
//
|
||||
// What it looks for:
|
||||
// 1. Credentials: keys, tokens, passwords, key blocks.
|
||||
@@ -17,7 +17,6 @@
|
||||
// node scripts/portability-report.mjs --staged # what is staged for the next commit
|
||||
// add --out report.md to also write the report to a file
|
||||
// add --locations-only to list file and line without quoting the matched text (for public CI output)
|
||||
// add --strict to exit 1 when findings exist (the default stays advisory: exit 0 either way)
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { writeFileSync, realpathSync } from "node:fs";
|
||||
@@ -134,12 +133,11 @@ export function renderReport(findings, label, { locationsOnly = false } = {}) {
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = { staged: false, locationsOnly: false, strict: false };
|
||||
const options = { staged: false, locationsOnly: false };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const key = argv[index];
|
||||
if (key === "--staged") options.staged = true;
|
||||
else if (key === "--locations-only") options.locationsOnly = true;
|
||||
else if (key === "--strict") options.strict = true;
|
||||
else if (["--from", "--to", "--out", "--repo"].includes(key)) options[key.slice(2)] = argv[++index];
|
||||
else throw new Error(`Unknown argument: ${key}`);
|
||||
}
|
||||
@@ -165,13 +163,12 @@ export function main(argv = process.argv.slice(2)) {
|
||||
label = `${from.slice(0, 8)}..${to.length > 12 ? to.slice(0, 8) : to}`;
|
||||
}
|
||||
const diff = git(repo, ["-c", "core.quotePath=false", "diff", "-U0", "--no-color", "--src-prefix=a/", "--dst-prefix=b/", "--diff-filter=ACMR", ...range]);
|
||||
const findings = findPortabilityIssues(addedLines(diff));
|
||||
const report = renderReport(findings, label, {
|
||||
const report = renderReport(findPortabilityIssues(addedLines(diff)), label, {
|
||||
locationsOnly: options.locationsOnly,
|
||||
});
|
||||
process.stdout.write(report);
|
||||
if (options.out) writeFileSync(options.out, report);
|
||||
return options.strict && findings.length > 0 ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function invokedDirectly() {
|
||||
|
||||
@@ -173,32 +173,3 @@ test("strips Git's separator tab and leaves C-quoted filenames quoted", () => {
|
||||
}
|
||||
assert.deepEqual(addedLines("+++ b/ordinary name.md\t\n@@ -0,0 +1 @@\n+see /Users/alice/project\n"), [{ file: "ordinary name.md", line: 1, text: "see /Users/alice/project" }]);
|
||||
});
|
||||
|
||||
test("--strict exits 1 on findings, keeps the advisory default at 0, and exits 0 on a clean stage", async () => {
|
||||
const { mkdtempSync, writeFileSync, rmSync } = await import("node:fs");
|
||||
const { tmpdir } = await import("node:os");
|
||||
const { join } = await import("node:path");
|
||||
const { fileURLToPath } = await import("node:url");
|
||||
const { execFileSync, spawnSync } = await import("node:child_process");
|
||||
const repo = mkdtempSync(join(tmpdir(), "openrig-portability-strict-"));
|
||||
const script = fileURLToPath(new URL("./portability-report.mjs", import.meta.url));
|
||||
try {
|
||||
execFileSync("git", ["init", "--quiet", repo]);
|
||||
execFileSync("git", ["-C", repo, "config", "user.email", "[email protected]"]);
|
||||
execFileSync("git", ["-C", repo, "config", "user.name", "t"]);
|
||||
writeFileSync(join(repo, "a.md"), "see /Users/alice/work\n");
|
||||
execFileSync("git", ["-C", repo, "add", "--", "a.md"]);
|
||||
|
||||
const strictWithFindings = spawnSync(process.execPath, [script, "--repo", repo, "--staged", "--strict"], { encoding: "utf8" });
|
||||
assert.equal(strictWithFindings.status, 1, strictWithFindings.stdout + strictWithFindings.stderr);
|
||||
|
||||
const advisoryWithFindings = spawnSync(process.execPath, [script, "--repo", repo, "--staged"], { encoding: "utf8" });
|
||||
assert.equal(advisoryWithFindings.status, 0, advisoryWithFindings.stdout + advisoryWithFindings.stderr);
|
||||
|
||||
execFileSync("git", ["-C", repo, "commit", "--quiet", "-m", "seed"]);
|
||||
writeFileSync(join(repo, "b.md"), "Nothing machine-specific here.\n");
|
||||
execFileSync("git", ["-C", repo, "add", "--", "b.md"]);
|
||||
const strictClean = spawnSync(process.execPath, [script, "--repo", repo, "--staged", "--strict"], { encoding: "utf8" });
|
||||
assert.equal(strictClean.status, 0, strictClean.stdout + strictClean.stderr);
|
||||
} finally { rmSync(repo, { recursive: true, force: true }); }
|
||||
});
|
||||
Reference in new issue
Block a user