@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 +45 -48
- package/dist/cli.js +58 -3
- package/dist/driver.js +11 -1
- package/dist/execution.js +1 -0
- package/dist/ops.js +199 -0
- package/dist/service.js +25 -2
- package/ops/conduit-ops +10 -0
- package/ops/conduit-ops.cmd +8 -0
- package/ops/connect.cmd +4 -0
- package/ops/connect.sh +4 -0
- package/ops/disconnect.cmd +4 -0
- package/ops/disconnect.sh +4 -0
- package/ops/env.example +13 -0
- package/ops/install.cmd +4 -0
- package/ops/install.sh +4 -0
- package/ops/offline.cmd +4 -0
- package/ops/offline.sh +4 -0
- package/ops/online.cmd +4 -0
- package/ops/online.sh +4 -0
- package/ops/status.cmd +4 -0
- package/ops/status.sh +4 -0
- package/ops/uninstall.cmd +4 -0
- package/ops/uninstall.sh +4 -0
- package/package.json +2 -1
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.
|
|
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
|
|
11
|
-
- **Claude Code** — `claude`
|
|
12
|
-
- **Codex** — `codex`
|
|
13
|
-
- **OpenCode** — `opencode`
|
|
14
|
-
- **Cursor Agent** — `agent` (local fuel
|
|
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
|
-
##
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
36
|
+
Later:
|
|
27
37
|
|
|
28
38
|
```bash
|
|
29
|
-
npx @miraland-labs/conduit-bridge
|
|
30
|
-
|
|
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
|
-
|
|
44
|
+
Optional local shortcuts after `init-ops` (same verbs):
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
## Advanced CLI
|
|
50
56
|
|
|
51
57
|
```bash
|
|
52
|
-
npx @miraland-labs/conduit-bridge
|
|
53
|
-
|
|
54
|
-
npx @miraland-labs/conduit-bridge install-service --
|
|
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
|
-
|
|
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
|
|
70
|
-
| `codex` | pump or local | sandbox `read-only` / `workspace-write
|
|
71
|
-
| `opencode` | pump or local |
|
|
72
|
-
| `cursor` | local only | per-run `.cursor/cli.json`
|
|
73
|
-
| `kiro` | local only | `--trust-tools
|
|
74
|
-
| `antigravity` | local only | `-p --mode plan`
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/ops/conduit-ops
ADDED
|
@@ -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%
|
package/ops/connect.cmd
ADDED
package/ops/connect.sh
ADDED
package/ops/env.example
ADDED
|
@@ -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
|
package/ops/install.cmd
ADDED
package/ops/install.sh
ADDED
package/ops/offline.cmd
ADDED
package/ops/offline.sh
ADDED
package/ops/online.cmd
ADDED
package/ops/online.sh
ADDED
package/ops/status.cmd
ADDED
package/ops/status.sh
ADDED
package/ops/uninstall.sh
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.9.
|
|
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": {
|