@miraland-labs/conduit-bridge 0.9.1 → 0.9.2

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/README.md CHANGED
@@ -2,79 +2,76 @@
2
2
 
3
3
  Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connects a computer to one organization, claims work, and drives a local agent.
4
4
 
5
- **Current npm:** `0.9.1` — **disconnect/disengage**, join org prompt when `--organization` is omitted, multi-driver lanes (`drivers online|offline`), shared concurrent slots **1–4**, worktrees, optional `--ensure-checkout`.
5
+ **Current npm:** `0.9.2` — **ops** helpers for macOS / Linux / Windows, LaunchAgent/systemd **PATH** for `~/.local/bin`, disconnect/disengage, multi-driver lanes, shared slots **1–4**, worktrees, optional `--ensure-checkout`.
6
6
 
7
7
  ## Prerequisites
8
8
 
9
9
  - Node.js 20+
10
- - One or more supported agent CLIs on PATH (common: install several on one Mac):
11
- - **Claude Code** — `claude` (Conduit pump or Anthropic login)
12
- - **Codex** — `codex` (Conduit pump or ChatGPT/OpenAI login). Since the July 2026 ChatGPT desktop merge, Bridge also finds the CLI bundled inside ChatGPT.app when `codex` is not on PATH (macOS)
13
- - **OpenCode** — `opencode` (Conduit pump or local provider login)
14
- - **Cursor Agent** — `agent` (local fuel; set with `drivers fuel cursor local` or machine `fuel local`)
10
+ - One or more supported agent CLIs:
11
+ - **Claude Code** — `claude`
12
+ - **Codex** — `codex`
13
+ - **OpenCode** — `opencode`
14
+ - **Cursor Agent** — `agent` (local fuel)
15
15
  - **Kiro CLI** — `kiro-cli` (local fuel)
16
16
  - **Antigravity** — `agy` (local fuel)
17
- - macOS or Linux for `install-service` (Windows: keep a terminal runner open)
18
17
 
19
- ## Install / run
18
+ ## Easy path (all platforms)
19
+
20
+ One config file + the same `ops` commands on macOS, Linux, and Windows:
20
21
 
21
22
  ```bash
22
- npx @miraland-labs/conduit-bridge join --url <https://your-conduit> --organization <slug>
23
- # If --organization is omitted, the CLI asks: Join which org?
23
+ # 1) Config once
24
+ mkdir -p ~/.config/conduit
25
+ # Windows: mkdir %USERPROFILE%\.config\conduit
26
+ npx @miraland-labs/conduit-bridge@latest init-ops
27
+ cp ~/conduit/ops/env.example ~/.config/conduit/ops.env
28
+ # edit ops.env (URL, WORKSPACE, optional REPO, DRIVERS, ROLES)
29
+
30
+ # 2) Connect → approve in browser → install
31
+ npx @miraland-labs/conduit-bridge@latest ops connect
32
+ npx @miraland-labs/conduit-bridge@latest ops install
33
+ npx @miraland-labs/conduit-bridge@latest ops status
24
34
  ```
25
35
 
26
- Disengage (leave the organization; then you can join the same or another org):
36
+ Later:
27
37
 
28
38
  ```bash
29
- npx @miraland-labs/conduit-bridge disconnect
30
- # Or non-interactive: disconnect --yes
39
+ npx @miraland-labs/conduit-bridge@latest ops online cursor
40
+ npx @miraland-labs/conduit-bridge@latest ops offline cursor
41
+ npx @miraland-labs/conduit-bridge@latest ops disconnect
31
42
  ```
32
43
 
33
- After approval, seed detects installed CLIs as **offline lanes**. Bring one or more online (subscription failover / concurrent lanes):
44
+ Optional local shortcuts after `init-ops` (same verbs):
34
45
 
35
- ```bash
36
- npx @miraland-labs/conduit-bridge drivers # list lanes
37
- npx @miraland-labs/conduit-bridge drivers online claude-code
38
- npx @miraland-labs/conduit-bridge drivers online claude-code codex # both can run at once
39
- npx @miraland-labs/conduit-bridge drivers offline claude-code # finish in-flight; no new claims
40
- npx @miraland-labs/conduit-bridge runner --workspace /path/to/repo
41
- ```
46
+ | Platform | Examples |
47
+ | --- | --- |
48
+ | macOS / Linux | `~/conduit/ops/connect.sh`, `install.sh`, |
49
+ | Windows | `%USERPROFILE%\conduit\ops\connect.cmd`, `install.cmd`, |
42
50
 
43
- Legacy single-driver process override (does not change saved online set):
51
+ **Windows note:** `ops install` brings lanes online and prints a `runner --workspace …` command to keep open (no LaunchAgent). macOS/Linux install a background service.
44
52
 
45
- ```bash
46
- npx @miraland-labs/conduit-bridge runner --agent claude-code --workspace /path/to/repo
47
- ```
53
+ `ops.env` keys: `CONDUIT_URL`, `CONDUIT_ORG` (optional), `CONDUIT_WORKSPACE`, `CONDUIT_REPO` (optional), `CONDUIT_DRIVERS` (default `cursor`), `CONDUIT_ROLES` (default `implement research`). Values expand `$HOME`, `%USERPROFILE%`, and `~`.
48
54
 
49
- Persist (macOS/Linux) — one LaunchAgent; honors the drivers online set:
55
+ ## Advanced CLI
50
56
 
51
57
  ```bash
52
- npx @miraland-labs/conduit-bridge install-service --workspace /path/to/repo
53
- # optional: bring a lane online at install time
54
- npx @miraland-labs/conduit-bridge install-service --agent claude-code --workspace /path/to/repo
58
+ npx @miraland-labs/conduit-bridge join --url <https://your-conduit> --organization <slug>
59
+ npx @miraland-labs/conduit-bridge drivers online cursor
60
+ npx @miraland-labs/conduit-bridge install-service --workspace /path/to/repo --ensure-checkout https://github.com/org/repo.git
61
+ npx @miraland-labs/conduit-bridge disconnect --yes
55
62
  ```
56
63
 
57
- Reinstall after upgrading from Bridge versions before 0.8 if the old unit baked a single `--agent`.
58
-
59
- For a Start-created GitHub repo that is not checked out yet, point `--workspace` at the intended path and add `--ensure-checkout <https://github.com/…>` so Bridge clones with the machine’s Git credentials before the first heartbeat (never the control-plane PAT).
60
-
61
- **Capacity:** Connect-approved `lease_capacity` (1–4) is a **shared pool** for all online lanes on that computer. Claude alone may use all 4 slots; Claude + Codex share those 4 — never 4×N.
64
+ **Capacity:** Connect-approved `lease_capacity` (1–4) is a **shared pool** for all online lanes on that computer.
62
65
 
63
66
  ## Grant enforcement
64
67
 
65
- Every driver enforces the assignment's granted actions with its CLI's native mechanism — never just the prompt:
66
-
67
68
  | Driver | Fuel | Mechanism |
68
69
  | --- | --- | --- |
69
- | `claude-code` | pump or local | `--allowedTools` / `--disallowedTools` per grant; bounded verification commands |
70
- | `codex` | pump or local | sandbox `read-only` / `workspace-write`; network enabled only with `pr_create`; resumes by thread id |
71
- | `opencode` | pump or local | `plan`/`build` agents; per-run `opencode.json` bash deny list (shared `deniedCommands`); `--session` resume; `--format json` |
72
- | `cursor` | local only | per-run `.cursor/cli.json` `permissions` allow/deny (no `--force`); read-only runs in `--mode plan` |
73
- | `kiro` | local only | `--trust-tools=fs_read,fs_write,execute_bash` allowlist; `--no-interactive`; `--resume-id`. Tool-level only — no per-command scoping, so a bash grant trusts `execute_bash` wholesale (Antigravity-tier) |
74
- | `antigravity` | local only | `-p --mode plan` (read-only) / `--mode accept-edits --sandbox` (write). Mode-level, coarser than per-tool; plain-text output; resume not surfaced |
75
-
76
- Every driver refuses to start when grants map to nothing, fails closed on missing logins, and shares one denied-command list (merge, force-push, deploy, kubectl, terraform).
77
-
78
- **Verification status:**
79
- - `claude-code`, `codex`, `cursor`, `opencode`, `antigravity` — verified end-to-end against their real CLIs (agy 1.1.3: `-p --mode` args, Google login, plain-text output confirmed through the driver).
80
- - `kiro` — flag surface and login preflight verified against kiro-cli 2.13.0 (`chat --no-interactive --trust-tools --resume-id`; tool names; `whoami`-based login check that avoids the browser-login hang). A full run is **pending a `KIRO_API_KEY`** — kiro-cli requires auth to execute and cannot be exercised offline.
70
+ | `claude-code` | pump or local | `--allowedTools` / `--disallowedTools` per grant |
71
+ | `codex` | pump or local | sandbox `read-only` / `workspace-write` |
72
+ | `opencode` | pump or local | per-run `opencode.json` bash deny list |
73
+ | `cursor` | local only | per-run `.cursor/cli.json` permissions |
74
+ | `kiro` | local only | `--trust-tools` allowlist |
75
+ | `antigravity` | local only | `-p --mode plan` / `accept-edits` |
76
+
77
+ Cursor/Kiro/Antigravity require local fuel (`ops install` sets this for you).
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from "node:util";
3
3
  import { resolve, dirname, join as pathJoin } from "node:path";
4
- import { hostname, userInfo } from "node:os";
4
+ import { hostname, homedir, platform, userInfo } from "node:os";
5
5
  import { spawn } from "node:child_process";
6
6
  import { createInterface } from "node:readline/promises";
7
- import { readFileSync } from "node:fs";
7
+ import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
8
8
  import { stdin as input, stdout as output } from "node:process";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { ConduitClient, ConduitRequestError } from "./client.js";
@@ -17,6 +17,7 @@ import { buildWorkspaceBrief } from "./brief.js";
17
17
  import { ensureCheckout } from "./checkout.js";
18
18
  import { pumpExecutionSlots, renewLeases } from "./execution.js";
19
19
  import { installRunnerService, uninstallRunnerService } from "./service.js";
20
+ import { OPS_VERBS, runOps } from "./ops.js";
20
21
  const [command] = process.argv.slice(2);
21
22
  /** Read our own package version so every runner start logs exactly which build is live. */
22
23
  function bridgeVersion() {
@@ -317,6 +318,56 @@ async function uninstallService() {
317
318
  const result = await uninstallRunnerService();
318
319
  console.log(`Removed Conduit runner service (${result.platform}): ${result.path}`);
319
320
  }
321
+ /** Copy cross-platform ops launchers to ~/conduit/ops (default). Env stays in ~/.config. */
322
+ async function initOps() {
323
+ const targetArg = process.argv[3];
324
+ const target = resolve(targetArg ?? pathJoin(homedir(), "conduit", "ops"));
325
+ const packageRoot = pathJoin(dirname(fileURLToPath(import.meta.url)), "..");
326
+ const source = pathJoin(packageRoot, "ops");
327
+ if (!existsSync(source)) {
328
+ throw new Error(`Ops kit missing from package at ${source}. Reinstall @miraland-labs/conduit-bridge.`);
329
+ }
330
+ mkdirSync(target, { recursive: true });
331
+ for (const name of readdirSync(source)) {
332
+ cpSync(pathJoin(source, name), pathJoin(target, name));
333
+ if (name.endsWith(".sh") || name === "conduit-ops")
334
+ chmodSync(pathJoin(target, name), 0o755);
335
+ }
336
+ const configDir = pathJoin(homedir(), ".config", "conduit");
337
+ const envPath = pathJoin(configDir, "ops.env");
338
+ const host = platform();
339
+ console.log(`Ops launchers installed at ${target}`);
340
+ console.log(`Config file (separate): ${envPath}`);
341
+ console.log("");
342
+ console.log("Works the same on macOS, Linux, and Windows via:");
343
+ console.log(` ${bridgeUsage("ops", "<connect|install|online|offline|status|disconnect|uninstall>")}`);
344
+ console.log("");
345
+ console.log("1. Edit env once:");
346
+ console.log(` Create folder: ${configDir}`);
347
+ console.log(` Copy ${pathJoin(target, "env.example")} → ${envPath}`);
348
+ console.log(` # edit ${envPath}`);
349
+ console.log("2. Connect (approve in browser):");
350
+ if (host === "win32") {
351
+ console.log(` ${pathJoin(target, "connect.cmd")}`);
352
+ console.log(" # or: npx @miraland-labs/conduit-bridge@latest ops connect");
353
+ console.log("3. Install / start runner:");
354
+ console.log(` ${pathJoin(target, "install.cmd")}`);
355
+ }
356
+ else {
357
+ console.log(` ${pathJoin(target, "connect.sh")}`);
358
+ console.log(" # or: npx @miraland-labs/conduit-bridge@latest ops connect");
359
+ console.log("3. Install runner:");
360
+ console.log(` ${pathJoin(target, "install.sh")}`);
361
+ }
362
+ console.log("Later: status · online · offline · disconnect (same names .sh / .cmd)");
363
+ }
364
+ async function opsCommand() {
365
+ const verb = process.argv[3];
366
+ if (!verb || !OPS_VERBS.includes(verb)) {
367
+ throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|online|offline|status|disconnect|uninstall>", "[driver…]")}`);
368
+ }
369
+ await runOps(verb, process.argv.slice(4));
370
+ }
320
371
  async function fuelCommand() {
321
372
  const mode = process.argv[3];
322
373
  if (mode !== "local" && mode !== "conduit") {
@@ -520,8 +571,12 @@ try {
520
571
  await installService();
521
572
  else if (command === "uninstall-service")
522
573
  await uninstallService();
574
+ else if (command === "init-ops")
575
+ await initOps();
576
+ else if (command === "ops")
577
+ await opsCommand();
523
578
  else
524
- throw new Error(`Usage: ${BRIDGE_NPX} <join|disconnect|connect|fuel|drivers|mcp|runner|install-service|uninstall-service>`);
579
+ throw new Error(`Usage: ${BRIDGE_NPX} <join|disconnect|connect|fuel|drivers|mcp|runner|install-service|uninstall-service|init-ops|ops>`);
525
580
  }
526
581
  catch (error) {
527
582
  console.error(error instanceof Error ? redactSecrets(error.message) : "Conduit command failed");
package/dist/driver.js CHANGED
@@ -3,6 +3,15 @@ import { existsSync } from "node:fs";
3
3
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import { z } from "zod";
6
+ function deliveryLanguageRule(language) {
7
+ if (language === "zh") {
8
+ return "- Write every human-facing delivery packet string (outcome, changes, verification, assumptions, risks, limitations, criterion text) in Chinese (Simplified). Keep JSON keys and enum token values in English.";
9
+ }
10
+ if (language === "en") {
11
+ return "- Write every human-facing delivery packet string (outcome, changes, verification, assumptions, risks, limitations, criterion text) in English. Keep JSON keys and enum token values in English.";
12
+ }
13
+ return null;
14
+ }
6
15
  export const evidenceKinds = ["change", "test", "preview", "research", "documentation"];
7
16
  export const agentReportTemplate = '{"outcome": "one-paragraph summary", "changes": ["path — what changed"], "verification": ["command — result"], "acceptance_results": [{"criterion": "exact criterion text", "status": "met|not_met|unknown"}], "evidence": [{"kind": "change|test|preview|research|documentation", "name": "concise evidence name", "uri": "external URL if one exists", "digest": "optional digest", "details": ["observable result"], "acceptance_criteria": ["exact criterion text supported by this evidence"]}], "assumptions": [], "risks": [], "limitations": [], "head_commit": "full sha of your final commit, omit if none"}';
8
17
  /**
@@ -131,7 +140,8 @@ export function buildAssignmentPrompt(context) {
131
140
  ...(context.grants.includes("branch_create") ? branchCreateCommands : []),
132
141
  ...(context.grants.includes("pr_create") ? prCreateCommands : []),
133
142
  ];
134
- lines.push("", "RULES", "- Stay within the change scope and boundaries.",
143
+ const languageRule = deliveryLanguageRule(packageContext?.working_language ?? spec.working_language);
144
+ lines.push("", "RULES", "- Stay within the change scope and boundaries.", ...(languageRule ? [languageRule] : []),
135
145
  // Name the executable commands. The shell allow-list is exact, so an agent that guesses at
136
146
  // `bash scripts/validate-pack.sh`, `python3 ...` or `echo` gets every call rejected and concludes
137
147
  // it has no shell at all — observed twice on live Daily Signal runs, both times while the
package/dist/execution.js CHANGED
@@ -67,6 +67,7 @@ const workPackageSchema = z.object({
67
67
  }).optional(),
68
68
  decisions: z.array(z.object({ question: z.string(), decision: z.string() })).optional(),
69
69
  rework_feedback: z.string().nullable().optional(),
70
+ working_language: z.enum(["en", "zh"]).optional(),
70
71
  }).nullable().optional();
71
72
  export async function renewLeases(client, config) {
72
73
  for (const active of Object.values(config.activeAttempts)) {
package/dist/ops.js ADDED
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Cross-platform operator helpers (macOS / Linux / Windows).
3
+ * Invoked as: npx @miraland-labs/conduit-bridge ops <connect|install|…>
4
+ */
5
+ import { spawnSync } from "node:child_process";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { homedir, platform } from "node:os";
8
+ import { join, resolve } from "node:path";
9
+ export const OPS_VERBS = [
10
+ "connect", "install", "online", "offline", "status", "disconnect", "uninstall",
11
+ ];
12
+ const LOCAL_FUEL_DRIVERS = new Set(["cursor", "kiro", "antigravity"]);
13
+ export function defaultOpsEnvPath(home = homedir()) {
14
+ return join(home, ".config", "conduit", "ops.env");
15
+ }
16
+ /** Expand $HOME, %USERPROFILE%, and leading ~ for cross-platform env values. */
17
+ export function expandOpsValue(value, home = homedir()) {
18
+ let next = value.trim();
19
+ if ((next.startsWith('"') && next.endsWith('"')) || (next.startsWith("'") && next.endsWith("'"))) {
20
+ next = next.slice(1, -1);
21
+ }
22
+ next = next.replace(/%USERPROFILE%/gi, home).replace(/%HOME%/gi, home);
23
+ next = next.replace(/\$HOME\b/g, home).replace(/\$\{HOME\}/g, home);
24
+ if (next.startsWith("~/") || next === "~")
25
+ next = join(home, next.slice(2));
26
+ return next;
27
+ }
28
+ export function parseOpsEnvFile(text, home = homedir()) {
29
+ const out = {};
30
+ for (const raw of text.split(/\r?\n/)) {
31
+ const line = raw.trim();
32
+ if (!line || line.startsWith("#"))
33
+ continue;
34
+ const eq = line.indexOf("=");
35
+ if (eq <= 0)
36
+ continue;
37
+ const key = line.slice(0, eq).trim();
38
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
39
+ continue;
40
+ out[key] = expandOpsValue(line.slice(eq + 1), home);
41
+ }
42
+ return out;
43
+ }
44
+ export function loadOpsEnv(home = homedir(), cwd = process.cwd()) {
45
+ const candidates = [
46
+ process.env.CONDUIT_OPS_ENV,
47
+ join(cwd, ".conduit-ops.env"),
48
+ defaultOpsEnvPath(home),
49
+ ].filter((path) => Boolean(path));
50
+ let loadedFrom = null;
51
+ let fileValues = {};
52
+ for (const path of candidates) {
53
+ if (!existsSync(path))
54
+ continue;
55
+ fileValues = parseOpsEnvFile(readFileSync(path, "utf8"), home);
56
+ loadedFrom = path;
57
+ break;
58
+ }
59
+ const pick = (key, fallback) => {
60
+ const fromEnv = process.env[key];
61
+ if (fromEnv !== undefined && fromEnv !== "")
62
+ return expandOpsValue(fromEnv, home);
63
+ if (fileValues[key])
64
+ return fileValues[key];
65
+ return fallback;
66
+ };
67
+ return {
68
+ CONDUIT_URL: pick("CONDUIT_URL", ""),
69
+ CONDUIT_ORG: pick("CONDUIT_ORG", ""),
70
+ CONDUIT_WORKSPACE: pick("CONDUIT_WORKSPACE", ""),
71
+ CONDUIT_REPO: pick("CONDUIT_REPO", ""),
72
+ CONDUIT_DRIVERS: pick("CONDUIT_DRIVERS", "cursor"),
73
+ CONDUIT_ROLES: pick("CONDUIT_ROLES", "implement research"),
74
+ loadedFrom,
75
+ };
76
+ }
77
+ export function requireOpsEnv(env) {
78
+ if (env.loadedFrom || env.CONDUIT_URL || env.CONDUIT_WORKSPACE)
79
+ return;
80
+ const path = defaultOpsEnvPath();
81
+ throw new Error(`Missing ops env. Create it once:\n` +
82
+ ` Create folder: ${join(homedir(), ".config", "conduit")}\n` +
83
+ ` Copy env.example to: ${path}\n` +
84
+ ` Edit that file, then retry.`);
85
+ }
86
+ export function splitOpsList(value) {
87
+ return value.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
88
+ }
89
+ export function resolveDrivers(env, argv) {
90
+ const fromArgs = argv.map((item) => item.trim()).filter(Boolean);
91
+ const drivers = fromArgs.length ? fromArgs : splitOpsList(env.CONDUIT_DRIVERS);
92
+ if (!drivers.length)
93
+ throw new Error("No drivers. Set CONDUIT_DRIVERS or pass ids.");
94
+ return drivers;
95
+ }
96
+ /** Quote args for a copy-pasteable shell/cmd line (paths with spaces). */
97
+ export function shellQuoteArgs(args) {
98
+ return args.map((arg) => {
99
+ if (!/[^\w@%+=:,./\\-]/i.test(arg))
100
+ return arg;
101
+ return `"${arg.replaceAll('"', '\\"')}"`;
102
+ }).join(" ");
103
+ }
104
+ function defaultRunBridge(args, options = {}) {
105
+ // Re-enter this package's CLI in a child so ops stays a thin orchestrator.
106
+ const cli = process.argv[1];
107
+ const node = process.execPath;
108
+ const result = spawnSync(node, [cli, ...args], { stdio: "inherit", env: process.env });
109
+ if (result.error) {
110
+ if (options.allowFail)
111
+ return;
112
+ throw result.error;
113
+ }
114
+ if (result.status !== 0) {
115
+ if (options.allowFail)
116
+ return;
117
+ process.exit(result.status ?? 1);
118
+ }
119
+ }
120
+ export async function runOps(verb, argv = [], deps = {}) {
121
+ const runBridge = deps.runBridge ?? defaultRunBridge;
122
+ const host = deps.host ?? platform();
123
+ const env = deps.env ?? loadOpsEnv();
124
+ if (verb === "disconnect") {
125
+ runBridge(["uninstall-service"], { allowFail: true });
126
+ runBridge(["disconnect", "--yes"]);
127
+ console.log("Disconnected. Run: npx @miraland-labs/conduit-bridge@latest ops connect");
128
+ return;
129
+ }
130
+ if (verb === "uninstall") {
131
+ runBridge(["uninstall-service"]);
132
+ return;
133
+ }
134
+ if (verb === "status") {
135
+ if (env.loadedFrom) {
136
+ console.log(`Env: ${env.loadedFrom}`);
137
+ console.log(`URL: ${env.CONDUIT_URL || "(unset)"}`);
138
+ console.log(`Workspace: ${env.CONDUIT_WORKSPACE || "(unset)"}`);
139
+ console.log(`Repo: ${env.CONDUIT_REPO || "(none)"}`);
140
+ console.log(`Drivers: ${env.CONDUIT_DRIVERS}`);
141
+ console.log(`Roles: ${env.CONDUIT_ROLES}`);
142
+ console.log("");
143
+ }
144
+ else {
145
+ console.log("No ops.env loaded (showing drivers only).\n");
146
+ }
147
+ runBridge(["drivers"]);
148
+ return;
149
+ }
150
+ // Lane toggles only need Bridge config + optional driver ids — not a full ops.env.
151
+ if (verb === "online" || verb === "offline") {
152
+ const drivers = resolveDrivers(env, argv);
153
+ runBridge(["drivers", verb, ...drivers]);
154
+ return;
155
+ }
156
+ requireOpsEnv(env);
157
+ if (verb === "connect") {
158
+ if (!env.CONDUIT_URL)
159
+ throw new Error(`Set CONDUIT_URL in ${defaultOpsEnvPath()}`);
160
+ const args = ["join", "--url", env.CONDUIT_URL];
161
+ if (env.CONDUIT_ORG)
162
+ args.push("--organization", env.CONDUIT_ORG);
163
+ for (const role of splitOpsList(env.CONDUIT_ROLES))
164
+ args.push("--capability", role);
165
+ console.log(`Connecting to ${env.CONDUIT_URL} (roles: ${env.CONDUIT_ROLES})`);
166
+ console.log("Approve this computer in Connect when the browser opens.");
167
+ runBridge(args);
168
+ return;
169
+ }
170
+ if (verb === "install") {
171
+ if (!env.CONDUIT_WORKSPACE)
172
+ throw new Error(`Set CONDUIT_WORKSPACE in ${defaultOpsEnvPath()}`);
173
+ const workspace = resolve(expandOpsValue(env.CONDUIT_WORKSPACE));
174
+ const drivers = resolveDrivers(env, argv);
175
+ for (const id of drivers) {
176
+ if (LOCAL_FUEL_DRIVERS.has(id))
177
+ runBridge(["drivers", "fuel", id, "local"]);
178
+ }
179
+ runBridge(["drivers", "online", ...drivers]);
180
+ if (host === "win32") {
181
+ const runnerArgs = ["runner", "--workspace", workspace];
182
+ if (env.CONDUIT_REPO)
183
+ runnerArgs.push("--ensure-checkout", env.CONDUIT_REPO);
184
+ console.log("Windows: background install-service is not available.");
185
+ console.log("Keep a terminal open and run:");
186
+ console.log(` npx @miraland-labs/conduit-bridge@latest ${shellQuoteArgs(runnerArgs)}`);
187
+ console.log(`Drivers online: ${drivers.join(", ")}`);
188
+ return;
189
+ }
190
+ const installArgs = ["install-service", "--workspace", workspace];
191
+ if (env.CONDUIT_REPO)
192
+ installArgs.push("--ensure-checkout", env.CONDUIT_REPO);
193
+ console.log(`Installing runner for ${workspace} (drivers: ${drivers.join(", ")})`);
194
+ runBridge(installArgs);
195
+ console.log("Done. Check with: npx @miraland-labs/conduit-bridge@latest ops status");
196
+ return;
197
+ }
198
+ throw new Error(`Unknown ops verb: ${verb}`);
199
+ }
package/dist/service.js CHANGED
@@ -34,7 +34,24 @@ export function runnerProgramArguments(options = {}) {
34
34
  args.push("--agent-timeout-minutes", options.agentTimeoutMinutes);
35
35
  return args;
36
36
  }
37
- export function launchdPlist(programArguments, stdoutPath) {
37
+ /**
38
+ * PATH for LaunchAgent/systemd so agent CLIs in ~/.local/bin and Homebrew resolve
39
+ * without the operator hand-editing the unit.
40
+ */
41
+ export function runnerServicePath(home = homedir(), nodeBin = dirname(process.execPath)) {
42
+ const parts = [
43
+ nodeBin,
44
+ join(home, ".local", "bin"),
45
+ "/opt/homebrew/bin",
46
+ "/usr/local/bin",
47
+ "/usr/bin",
48
+ "/bin",
49
+ "/usr/sbin",
50
+ "/sbin",
51
+ ];
52
+ return [...new Set(parts.filter(Boolean))].join(":");
53
+ }
54
+ export function launchdPlist(programArguments, stdoutPath, pathEnv = runnerServicePath()) {
38
55
  const argsXml = programArguments.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n");
39
56
  return `<?xml version="1.0" encoding="UTF-8"?>
40
57
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -46,6 +63,11 @@ export function launchdPlist(programArguments, stdoutPath) {
46
63
  <array>
47
64
  ${argsXml}
48
65
  </array>
66
+ <key>EnvironmentVariables</key>
67
+ <dict>
68
+ <key>PATH</key>
69
+ <string>${escapeXml(pathEnv)}</string>
70
+ </dict>
49
71
  <key>RunAtLoad</key>
50
72
  <true/>
51
73
  <key>KeepAlive</key>
@@ -58,7 +80,7 @@ ${argsXml}
58
80
  </plist>
59
81
  `;
60
82
  }
61
- export function systemdUserUnit(programArguments, workingDirectory) {
83
+ export function systemdUserUnit(programArguments, workingDirectory, pathEnv = runnerServicePath()) {
62
84
  const execStart = programArguments.map(escapeSystemd).join(" ");
63
85
  return `[Unit]
64
86
  Description=Conduit Bridge runner
@@ -68,6 +90,7 @@ Wants=network-online.target
68
90
  [Service]
69
91
  Type=simple
70
92
  WorkingDirectory=${workingDirectory}
93
+ Environment=PATH=${escapeSystemd(pathEnv)}
71
94
  ExecStart=${execStart}
72
95
  Restart=always
73
96
  RestartSec=5
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: ./conduit-ops <connect|install|online|offline|status|disconnect|uninstall> [args…]
3
+ set -euo pipefail
4
+ if [[ $# -lt 1 ]]; then
5
+ echo "Usage: $0 <connect|install|online|offline|status|disconnect|uninstall> [args…]" >&2
6
+ exit 1
7
+ fi
8
+ verb=$1
9
+ shift
10
+ exec npx --yes @miraland-labs/conduit-bridge@latest ops "$verb" "$@"
@@ -0,0 +1,8 @@
1
+ @echo off
2
+ REM Usage: conduit-ops.cmd <connect|install|online|offline|status|disconnect|uninstall> [args…]
3
+ if "%~1"=="" (
4
+ echo Usage: %~nx0 ^<connect^|install^|online^|offline^|status^|disconnect^|uninstall^> [args…]
5
+ exit /b 1
6
+ )
7
+ npx --yes @miraland-labs/conduit-bridge@latest ops %*
8
+ if errorlevel 1 exit /b %errorlevel%
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ REM Windows launcher. macOS/Linux: use connect.sh
3
+ npx --yes @miraland-labs/conduit-bridge@latest ops connect %*
4
+ if errorlevel 1 exit /b %errorlevel%
package/ops/connect.sh ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Cross-platform launcher (macOS/Linux). Windows: use connect.cmd
3
+ set -euo pipefail
4
+ exec npx --yes @miraland-labs/conduit-bridge@latest ops connect "$@"
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ REM Windows launcher. macOS/Linux: use disconnect.sh
3
+ npx --yes @miraland-labs/conduit-bridge@latest ops disconnect %*
4
+ if errorlevel 1 exit /b %errorlevel%
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Cross-platform launcher (macOS/Linux). Windows: use disconnect.cmd
3
+ set -euo pipefail
4
+ exec npx --yes @miraland-labs/conduit-bridge@latest ops disconnect "$@"
@@ -0,0 +1,13 @@
1
+ # Configuration (not scripts). Copy to ~/.config/conduit/ops.env and edit once.
2
+ # Same file works on macOS, Linux, and Windows ($HOME / %USERPROFILE% / ~ are expanded).
3
+ #
4
+ # Cross-platform commands (any OS):
5
+ # npx @miraland-labs/conduit-bridge@latest ops connect
6
+ # npx @miraland-labs/conduit-bridge@latest ops install
7
+ # Local shortcuts after init-ops: ~/conduit/ops/*.sh (Mac/Linux) or *.cmd (Windows)
8
+ CONDUIT_URL=https://api.conduit.miraland.io
9
+ # CONDUIT_ORG=your-org-slug
10
+ CONDUIT_WORKSPACE=$HOME/agentic/promote-pr402
11
+ # CONDUIT_REPO=https://github.com/miralandlabs/promote-pr402.git
12
+ CONDUIT_DRIVERS=cursor
13
+ CONDUIT_ROLES=implement research
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ REM Windows launcher. macOS/Linux: use install.sh
3
+ npx --yes @miraland-labs/conduit-bridge@latest ops install %*
4
+ if errorlevel 1 exit /b %errorlevel%
package/ops/install.sh ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Cross-platform launcher (macOS/Linux). Windows: use install.cmd
3
+ set -euo pipefail
4
+ exec npx --yes @miraland-labs/conduit-bridge@latest ops install "$@"
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ REM Windows launcher. macOS/Linux: use offline.sh
3
+ npx --yes @miraland-labs/conduit-bridge@latest ops offline %*
4
+ if errorlevel 1 exit /b %errorlevel%
package/ops/offline.sh ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Cross-platform launcher (macOS/Linux). Windows: use offline.cmd
3
+ set -euo pipefail
4
+ exec npx --yes @miraland-labs/conduit-bridge@latest ops offline "$@"
package/ops/online.cmd ADDED
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ REM Windows launcher. macOS/Linux: use online.sh
3
+ npx --yes @miraland-labs/conduit-bridge@latest ops online %*
4
+ if errorlevel 1 exit /b %errorlevel%
package/ops/online.sh ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Cross-platform launcher (macOS/Linux). Windows: use online.cmd
3
+ set -euo pipefail
4
+ exec npx --yes @miraland-labs/conduit-bridge@latest ops online "$@"
package/ops/status.cmd ADDED
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ REM Windows launcher. macOS/Linux: use status.sh
3
+ npx --yes @miraland-labs/conduit-bridge@latest ops status %*
4
+ if errorlevel 1 exit /b %errorlevel%
package/ops/status.sh ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Cross-platform launcher (macOS/Linux). Windows: use status.cmd
3
+ set -euo pipefail
4
+ exec npx --yes @miraland-labs/conduit-bridge@latest ops status "$@"
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ REM Windows launcher. macOS/Linux: use uninstall.sh
3
+ npx --yes @miraland-labs/conduit-bridge@latest ops uninstall %*
4
+ if errorlevel 1 exit /b %errorlevel%
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Cross-platform launcher (macOS/Linux). Windows: use uninstall.cmd
3
+ set -euo pipefail
4
+ exec npx --yes @miraland-labs/conduit-bridge@latest ops uninstall "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Kiro / Antigravity agents for a Conduit organization",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
+ "ops",
11
12
  "README.md"
12
13
  ],
13
14
  "engines": {