@miraland-labs/conduit-bridge 0.1.0 → 0.4.0
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 +34 -9
- package/dist/cli.js +8 -6
- package/dist/detect.js +6 -1
- package/dist/driver.js +469 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,29 +5,54 @@ Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connect
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
7
|
- Node.js 20+
|
|
8
|
-
-
|
|
8
|
+
- One supported agent CLI on PATH:
|
|
9
|
+
- **Claude Code** — `claude` (Conduit pump or Anthropic login)
|
|
10
|
+
- **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)
|
|
11
|
+
- **OpenCode** — `opencode` (Conduit pump or local provider login)
|
|
12
|
+
- **Cursor Agent** — `agent` (requires `conduit fuel local` + `CURSOR_API_KEY` / Cursor login)
|
|
13
|
+
- **Kiro CLI** — `kiro-cli` (requires `conduit fuel local` + `KIRO_API_KEY` / `kiro-cli login`)
|
|
14
|
+
- **Antigravity** — `agy` (requires `conduit fuel local` + `GEMINI_API_KEY` / `agy login`)
|
|
9
15
|
- macOS or Linux for `install-service` (Windows: keep a terminal runner open)
|
|
10
16
|
|
|
11
17
|
## Install / run
|
|
12
18
|
|
|
13
|
-
No global install required:
|
|
14
|
-
|
|
15
19
|
```bash
|
|
16
20
|
npx @miraland-labs/conduit-bridge join --url <https://your-conduit> --organization <slug>
|
|
17
21
|
```
|
|
18
22
|
|
|
19
|
-
After approval
|
|
23
|
+
After approval:
|
|
20
24
|
|
|
21
25
|
```bash
|
|
22
26
|
npx @miraland-labs/conduit-bridge runner --agent claude-code --workspace /path/to/repo
|
|
23
|
-
npx @miraland-labs/conduit-bridge
|
|
27
|
+
npx @miraland-labs/conduit-bridge runner --agent codex --workspace /path/to/repo
|
|
28
|
+
npx @miraland-labs/conduit-bridge runner --agent opencode --workspace /path/to/repo
|
|
29
|
+
npx @miraland-labs/conduit-bridge fuel local # required before cursor / kiro / antigravity
|
|
30
|
+
npx @miraland-labs/conduit-bridge runner --agent cursor --workspace /path/to/repo
|
|
31
|
+
npx @miraland-labs/conduit-bridge runner --agent kiro --workspace /path/to/repo
|
|
32
|
+
npx @miraland-labs/conduit-bridge runner --agent antigravity --workspace /path/to/repo
|
|
24
33
|
```
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
Persist (macOS/Linux):
|
|
27
36
|
|
|
28
37
|
```bash
|
|
29
|
-
|
|
30
|
-
conduit join --url <https://your-conduit> --organization <slug>
|
|
38
|
+
npx @miraland-labs/conduit-bridge install-service --agent <claude-code|codex|cursor|opencode|kiro|antigravity> --workspace /path/to/repo
|
|
31
39
|
```
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
## Grant enforcement
|
|
42
|
+
|
|
43
|
+
Every driver enforces the assignment's granted actions with its CLI's native mechanism — never just the prompt:
|
|
44
|
+
|
|
45
|
+
| Driver | Fuel | Mechanism |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `claude-code` | pump or local | `--allowedTools` / `--disallowedTools` per grant; bounded verification commands |
|
|
48
|
+
| `codex` | pump or local | sandbox `read-only` / `workspace-write`; network enabled only with `pr_create`; resumes by thread id |
|
|
49
|
+
| `opencode` | pump or local | `plan` agent (no edit/bash) for read-only, `build` for write; `--session` resume; `--format json` |
|
|
50
|
+
| `cursor` | local only | per-run `.cursor/cli.json` allowlist (`approvalMode: allowlist`, shared deny list, no `--force`); read-only runs in `--mode plan` |
|
|
51
|
+
| `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) |
|
|
52
|
+
| `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 |
|
|
53
|
+
|
|
54
|
+
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).
|
|
55
|
+
|
|
56
|
+
**Verification status:**
|
|
57
|
+
- `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).
|
|
58
|
+
- `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.
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,8 @@ const BRIDGE_NPX = "npx @miraland-labs/conduit-bridge";
|
|
|
17
17
|
function bridgeUsage(...args) {
|
|
18
18
|
return `${BRIDGE_NPX} ${args.join(" ")}`;
|
|
19
19
|
}
|
|
20
|
+
/** Single source for the --agent placeholder so CLI help never drifts from DRIVERS. */
|
|
21
|
+
const AGENT_PLACEHOLDER = `<${Object.keys(DRIVERS).join("|")}>`;
|
|
20
22
|
function parseFuelSource(value) {
|
|
21
23
|
if (value === undefined)
|
|
22
24
|
return undefined;
|
|
@@ -85,7 +87,7 @@ async function join() {
|
|
|
85
87
|
console.log(`Assignments at once: ${leaseCapacity}`);
|
|
86
88
|
if (fuelSource)
|
|
87
89
|
console.log(`Fuel source: ${fuelSource === "local" ? "local subscription" : "Conduit pump"}`);
|
|
88
|
-
console.log(
|
|
90
|
+
console.log(`Execution drivers: ${Object.keys(DRIVERS).join(", ")} (cursor/kiro/antigravity require local fuel).`);
|
|
89
91
|
console.log("Use --machine <name> to override the computer label.\n");
|
|
90
92
|
const response = await fetch(`${baseUrl}/runner/v1/connect/requests`, {
|
|
91
93
|
method: "POST",
|
|
@@ -193,9 +195,9 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
193
195
|
console.log(`Fuel source: ${config.fuelSource === "local" ? "local subscription" : "Conduit pump"}`);
|
|
194
196
|
console.log("Heartbeats report capabilities for diagnostics only; matching uses the Connect confirmation. Detected clients never receive grants automatically.");
|
|
195
197
|
console.log(`MCP setup: {"mcpServers":{"conduit":{"command":"npx","args":["-y","@miraland-labs/conduit-bridge","mcp"]}}}`);
|
|
196
|
-
console.log(`Execute work: ${bridgeUsage("runner", "--agent",
|
|
197
|
-
console.log(`Keep on shift (macOS/Linux): ${bridgeUsage("install-service", "--agent",
|
|
198
|
-
console.log(`Flip fuel later: ${bridgeUsage("fuel", "local|conduit")}`);
|
|
198
|
+
console.log(`Execute work: ${bridgeUsage("runner", "--agent", AGENT_PLACEHOLDER, "--workspace", "<repo>")}`);
|
|
199
|
+
console.log(`Keep on shift (macOS/Linux): ${bridgeUsage("install-service", "--agent", AGENT_PLACEHOLDER, "--workspace", "<repo>")}`);
|
|
200
|
+
console.log(`Flip fuel later: ${bridgeUsage("fuel", "local|conduit")} (cursor/kiro/antigravity require local fuel)`);
|
|
199
201
|
console.log("Windows: keep the runner terminal open — install-service is macOS/Linux only.");
|
|
200
202
|
if (Object.keys(fuel).length) {
|
|
201
203
|
console.log(`Fleet fueling: ${Object.keys(fuel).length} project key(s) configured for Conduit /v1`);
|
|
@@ -223,7 +225,7 @@ async function installService() {
|
|
|
223
225
|
agent: { type: "string" }, workspace: { type: "string" }, interval: { type: "string" }, "agent-timeout-minutes": { type: "string" },
|
|
224
226
|
} });
|
|
225
227
|
if (!values.agent || !values.workspace) {
|
|
226
|
-
throw new Error(`Usage: ${bridgeUsage("install-service", "--agent",
|
|
228
|
+
throw new Error(`Usage: ${bridgeUsage("install-service", "--agent", AGENT_PLACEHOLDER, "--workspace", "<repository-path>")} (agent + workspace required; heartbeat-only services are not supported)`);
|
|
227
229
|
}
|
|
228
230
|
if (!DRIVERS[values.agent]) {
|
|
229
231
|
throw new Error(`Unknown agent driver: ${values.agent}. Available: ${Object.keys(DRIVERS).join(", ")}`);
|
|
@@ -279,7 +281,7 @@ async function runner() {
|
|
|
279
281
|
const timeoutMs = values["agent-timeout-minutes"] ? Number(values["agent-timeout-minutes"]) * 60_000 : undefined;
|
|
280
282
|
const fuelLabel = config.fuelSource === "local" ? "local subscription" : "Conduit pump";
|
|
281
283
|
if (!driver || !workspace) {
|
|
282
|
-
console.warn(`WARNING: heartbeat only — no work will execute. Use: ${bridgeUsage("runner", "--agent",
|
|
284
|
+
console.warn(`WARNING: heartbeat only — no work will execute. Use: ${bridgeUsage("runner", "--agent", AGENT_PLACEHOLDER, "--workspace", "<repo>")}`);
|
|
283
285
|
}
|
|
284
286
|
console.log(`Conduit runner connected to ${config.baseUrl}${driver ? ` — executing via ${driver.name} in ${workspace}` : " — heartbeat only (no --agent)"} (fuel: ${fuelLabel})`);
|
|
285
287
|
for (;;) {
|
package/dist/detect.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { access, constants } from "node:fs/promises";
|
|
2
2
|
import { delimiter, join } from "node:path";
|
|
3
|
+
/** PATH probes for Connect diagnostics — not proof of a Bridge driver. */
|
|
3
4
|
const CLIENTS = [
|
|
4
|
-
{ command: "codex", label: "Codex CLI" },
|
|
5
5
|
{ command: "claude", label: "Claude Code" },
|
|
6
|
+
{ command: "codex", label: "Codex CLI" },
|
|
7
|
+
{ command: "opencode", label: "OpenCode" },
|
|
8
|
+
{ command: "agent", label: "Cursor Agent" },
|
|
6
9
|
{ command: "cursor", label: "Cursor" },
|
|
10
|
+
{ command: "kiro-cli", label: "Kiro CLI" },
|
|
11
|
+
{ command: "agy", label: "Antigravity" },
|
|
7
12
|
{ command: "code", label: "Visual Studio Code" },
|
|
8
13
|
];
|
|
9
14
|
export async function detectInstalledClients(pathValue = process.env.PATH ?? "", platform = process.platform) {
|
package/dist/driver.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
export const evidenceKinds = ["change", "test", "preview", "research", "documentation"];
|
|
@@ -20,11 +21,13 @@ export function claudeToolsForGrants(grants, verificationCommands = []) {
|
|
|
20
21
|
tools.push("Bash(git push:*)", "Bash(gh pr create:*)");
|
|
21
22
|
return tools;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
24
|
+
/** Commands no driver may ever run, regardless of grants — one source for every driver's deny list. */
|
|
25
|
+
export const deniedCommands = [
|
|
26
|
+
"git merge", "git rebase", "git reset --hard", "git push --force",
|
|
27
|
+
"npm run deploy", "pnpm deploy", "yarn deploy", "wrangler deploy",
|
|
28
|
+
"kubectl", "terraform apply",
|
|
27
29
|
];
|
|
30
|
+
export const claudeDeniedTools = deniedCommands.map((command) => `Bash(${command}:*)`);
|
|
28
31
|
function isBoundedVerificationCommand(command) {
|
|
29
32
|
return /^(npm run (verify|typecheck|lint|test|build)|pnpm (verify|typecheck|lint|test|build)|yarn (verify|typecheck|lint|test|build)|cargo (test|check)|go test(?: \.\/\.\.\.)?|make (test|check))$/.test(command);
|
|
30
33
|
}
|
|
@@ -112,12 +115,12 @@ export const claudeCodeDriver = {
|
|
|
112
115
|
name: "claude-code",
|
|
113
116
|
async run(input) {
|
|
114
117
|
const fuelSource = input.fuelSource === "local" ? "local" : "conduit";
|
|
115
|
-
if (fuelSource === "local" && !
|
|
118
|
+
if (fuelSource === "local" && !hasClaudeLogin()) {
|
|
116
119
|
return {
|
|
117
120
|
status: "failed",
|
|
118
121
|
resultText: null,
|
|
119
122
|
sessionId: null,
|
|
120
|
-
error: "machine set to local fuel but claude-code has no login",
|
|
123
|
+
error: "machine set to local fuel but claude-code has no login (ANTHROPIC_* or ~/.claude)",
|
|
121
124
|
};
|
|
122
125
|
}
|
|
123
126
|
const tools = claudeToolsForGrants(input.grants, input.verificationCommands);
|
|
@@ -153,22 +156,436 @@ export const claudeCodeDriver = {
|
|
|
153
156
|
return { status: "completed", resultText: message.result ?? "", sessionId };
|
|
154
157
|
},
|
|
155
158
|
};
|
|
156
|
-
|
|
157
|
-
function
|
|
159
|
+
/** Map Conduit grants → Codex sandbox (privileged grants never widen the sandbox). */
|
|
160
|
+
export function codexSandboxForGrants(grants) {
|
|
161
|
+
if (grants.includes("repo_write") || grants.includes("branch_create") || grants.includes("pr_create") || grants.includes("test_run")) {
|
|
162
|
+
return "workspace-write";
|
|
163
|
+
}
|
|
164
|
+
if (grants.includes("repo_read"))
|
|
165
|
+
return "read-only";
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Codex exec arguments. The sandbox is the enforcement boundary: network stays
|
|
170
|
+
* off inside workspace-write unless the assignment actually holds pr_create.
|
|
171
|
+
*/
|
|
172
|
+
export function codexExecArgs(input, sandbox) {
|
|
173
|
+
// exec is non-interactive by design (no approval flag), and `exec resume`
|
|
174
|
+
// takes the sandbox only as a -c override, not --sandbox.
|
|
175
|
+
const args = input.resumeSessionId
|
|
176
|
+
? ["exec", "resume", "--json", "-c", `sandbox_mode="${sandbox}"`]
|
|
177
|
+
: ["exec", "--json", "--sandbox", sandbox];
|
|
178
|
+
if (sandbox === "workspace-write" && input.grants.includes("pr_create")) {
|
|
179
|
+
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
180
|
+
}
|
|
181
|
+
if (input.resumeSessionId)
|
|
182
|
+
args.push(input.resumeSessionId);
|
|
183
|
+
args.push("-");
|
|
184
|
+
return args;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Since the July 2026 desktop merge, macOS machines often carry the codex CLI
|
|
188
|
+
* only inside the ChatGPT app bundle rather than on PATH.
|
|
189
|
+
*/
|
|
190
|
+
export const CODEX_APP_BUNDLE_BINARY = "/Applications/ChatGPT.app/Contents/Resources/codex";
|
|
191
|
+
export function resolveCodexExecutable(env = process.env, platform = process.platform) {
|
|
192
|
+
const onPath = (env.PATH ?? "").split(":").some((dir) => dir && existsSync(join(dir, "codex")));
|
|
193
|
+
if (onPath)
|
|
194
|
+
return "codex";
|
|
195
|
+
if (platform === "darwin" && existsSync(CODEX_APP_BUNDLE_BINARY))
|
|
196
|
+
return CODEX_APP_BUNDLE_BINARY;
|
|
197
|
+
return "codex";
|
|
198
|
+
}
|
|
199
|
+
/** Pull the thread id and final agent message out of `codex exec --json` JSONL events. */
|
|
200
|
+
export function parseCodexJsonl(stdout) {
|
|
201
|
+
let sessionId = null;
|
|
202
|
+
let resultText = null;
|
|
203
|
+
for (const line of stdout.split("\n")) {
|
|
204
|
+
const trimmed = line.trim();
|
|
205
|
+
if (!trimmed.startsWith("{"))
|
|
206
|
+
continue;
|
|
207
|
+
let event;
|
|
208
|
+
try {
|
|
209
|
+
event = JSON.parse(trimmed);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (typeof event.thread_id === "string")
|
|
215
|
+
sessionId = event.thread_id;
|
|
216
|
+
if (event.item?.type === "agent_message" && typeof event.item.text === "string")
|
|
217
|
+
resultText = event.item.text;
|
|
218
|
+
}
|
|
219
|
+
return { sessionId, resultText };
|
|
220
|
+
}
|
|
221
|
+
export const codexDriver = {
|
|
222
|
+
name: "codex",
|
|
223
|
+
async run(input) {
|
|
224
|
+
const fuelSource = input.fuelSource === "local" ? "local" : "conduit";
|
|
225
|
+
if (fuelSource === "local" && !hasOpenAiLogin()) {
|
|
226
|
+
return {
|
|
227
|
+
status: "failed",
|
|
228
|
+
resultText: null,
|
|
229
|
+
sessionId: null,
|
|
230
|
+
error: "machine set to local fuel but codex has no login (OPENAI_API_KEY / CODEX_API_KEY / ~/.codex)",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const sandbox = codexSandboxForGrants(input.grants);
|
|
234
|
+
if (!sandbox) {
|
|
235
|
+
return {
|
|
236
|
+
status: "failed",
|
|
237
|
+
resultText: null,
|
|
238
|
+
sessionId: null,
|
|
239
|
+
error: "No Bridge-mapped Codex sandbox for active grants; refusing to start agent",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Prompt via stdin avoids ARG_MAX limits on large assignment contracts.
|
|
243
|
+
const { code, stdout, stderr } = await execute(input.executable ?? resolveCodexExecutable(), codexExecArgs(input, sandbox), input.workspace, input.timeoutMs ?? 20 * 60_000, fuelSource === "conduit" ? input.fuel : undefined, fuelSource, input.prompt);
|
|
244
|
+
const parsed = parseCodexJsonl(stdout);
|
|
245
|
+
const resultText = parsed.resultText ?? (stdout || null);
|
|
246
|
+
if (code !== 0) {
|
|
247
|
+
return { status: "failed", resultText, sessionId: parsed.sessionId, error: (stderr || stdout || `codex exited with code ${code}`).slice(0, 20_000) };
|
|
248
|
+
}
|
|
249
|
+
return { status: "completed", resultText, sessionId: parsed.sessionId };
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* Grants → Cursor CLI permissions (project .cursor/cli.json). With
|
|
254
|
+
* approvalMode "allowlist" and no --force, any command outside this list is
|
|
255
|
+
* denied in print mode — the same fail-closed contract as Claude's tool lists.
|
|
256
|
+
*/
|
|
257
|
+
export function cursorPermissionsForGrants(grants, verificationCommands = []) {
|
|
258
|
+
const allow = [];
|
|
259
|
+
if (grants.includes("test_run")) {
|
|
260
|
+
allow.push(...verificationCommands.filter(isBoundedVerificationCommand).map((command) => `Shell(${command})`));
|
|
261
|
+
}
|
|
262
|
+
if (grants.includes("branch_create")) {
|
|
263
|
+
allow.push("Shell(git status)", "Shell(git diff)", "Shell(git add)", "Shell(git commit)", "Shell(git branch)", "Shell(git switch)");
|
|
264
|
+
}
|
|
265
|
+
if (grants.includes("pr_create"))
|
|
266
|
+
allow.push("Shell(git push)", "Shell(gh pr create)");
|
|
267
|
+
return { allow, deny: deniedCommands.map((command) => `Shell(${command})`) };
|
|
268
|
+
}
|
|
269
|
+
/** Cursor CLI arguments. Read-only assignments run in plan mode — edits are impossible, not just discouraged. */
|
|
270
|
+
export function cursorRunArgs(input) {
|
|
271
|
+
const args = ["-p", "--output-format", "json", "--workspace", input.workspace];
|
|
272
|
+
const canChange = ["repo_write", "test_run", "branch_create", "pr_create"].some((grant) => input.grants.includes(grant));
|
|
273
|
+
if (!canChange)
|
|
274
|
+
args.push("--mode", "plan");
|
|
275
|
+
if (input.resumeSessionId)
|
|
276
|
+
args.push("--resume", input.resumeSessionId);
|
|
277
|
+
args.push(input.prompt);
|
|
278
|
+
return args;
|
|
279
|
+
}
|
|
280
|
+
/** Tolerant parse of `--output-format json`: accept the documented shape, fall back to raw text. */
|
|
281
|
+
export function parseCursorOutput(stdout) {
|
|
282
|
+
try {
|
|
283
|
+
const parsed = JSON.parse(stdout);
|
|
284
|
+
const resultText = typeof parsed.result === "string" ? parsed.result : typeof parsed.text === "string" ? parsed.text : stdout;
|
|
285
|
+
const sessionCandidate = [parsed.session_id, parsed.chat_id, parsed.chatId].find((value) => typeof value === "string");
|
|
286
|
+
return { resultText, sessionId: sessionCandidate ?? null, isError: parsed.is_error === true || parsed.subtype === "error" };
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return { resultText: stdout || null, sessionId: null, isError: false };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export const cursorDriver = {
|
|
293
|
+
name: "cursor",
|
|
294
|
+
async run(input) {
|
|
295
|
+
const fuelSource = input.fuelSource === "local" ? "local" : "conduit";
|
|
296
|
+
// Cursor Agent authenticates with CURSOR_API_KEY / IDE login — not Conduit /v1 Anthropic/OpenAI shims.
|
|
297
|
+
if (fuelSource === "conduit") {
|
|
298
|
+
return {
|
|
299
|
+
status: "failed",
|
|
300
|
+
resultText: null,
|
|
301
|
+
sessionId: null,
|
|
302
|
+
error: "cursor driver requires local fuel (CURSOR_API_KEY or Cursor login). Run: npx @miraland-labs/conduit-bridge fuel local",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (!input.grants.includes("repo_read") && !input.grants.includes("repo_write")) {
|
|
306
|
+
return {
|
|
307
|
+
status: "failed",
|
|
308
|
+
resultText: null,
|
|
309
|
+
sessionId: null,
|
|
310
|
+
error: "No Bridge-mapped Cursor access for active grants; refusing to start agent",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const executable = input.executable ?? "agent";
|
|
314
|
+
// A ~/.cursor directory exists even after logout — ask the CLI itself.
|
|
315
|
+
// `agent status` exits 0 either way, so the text is the signal.
|
|
316
|
+
if (!process.env.CURSOR_API_KEY) {
|
|
317
|
+
const status = await execute(executable, ["status"], input.workspace, 15_000, undefined, "local");
|
|
318
|
+
if (status.code !== 0 || /not logged in/i.test(status.stdout)) {
|
|
319
|
+
return { status: "failed", resultText: null, sessionId: null, error: "cursor has no login (set CURSOR_API_KEY or run `agent login`)" };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const configured = await withCursorPermissions(input.workspace, cursorPermissionsForGrants(input.grants, input.verificationCommands), () => execute(executable, cursorRunArgs(input), input.workspace, input.timeoutMs ?? 20 * 60_000, undefined, "local"));
|
|
323
|
+
const { code, stdout, stderr } = configured;
|
|
324
|
+
const parsed = parseCursorOutput(stdout);
|
|
325
|
+
if (code !== 0 || parsed.isError) {
|
|
326
|
+
return { status: "failed", resultText: parsed.resultText, sessionId: parsed.sessionId, error: (stderr || parsed.resultText || `cursor agent exited with code ${code}`).slice(0, 20_000) };
|
|
327
|
+
}
|
|
328
|
+
return { status: "completed", resultText: parsed.resultText, sessionId: parsed.sessionId };
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
/** Write the assignment's permission contract to .cursor/cli.json for the run, restoring whatever was there before. */
|
|
332
|
+
async function withCursorPermissions(workspace, permissions, run) {
|
|
333
|
+
const configDir = join(workspace, ".cursor");
|
|
334
|
+
const configPath = join(configDir, "cli.json");
|
|
335
|
+
let previous = null;
|
|
336
|
+
try {
|
|
337
|
+
previous = await readFile(configPath, "utf8");
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
previous = null;
|
|
341
|
+
}
|
|
342
|
+
let config = { version: 1 };
|
|
343
|
+
if (previous) {
|
|
344
|
+
try {
|
|
345
|
+
config = { ...JSON.parse(previous) };
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
config = { version: 1 };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
config.permissions = permissions;
|
|
352
|
+
config.approvalMode = "allowlist";
|
|
353
|
+
await mkdir(configDir, { recursive: true });
|
|
354
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
355
|
+
try {
|
|
356
|
+
return await run();
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
if (previous === null)
|
|
360
|
+
await rm(configPath, { force: true });
|
|
361
|
+
else
|
|
362
|
+
await writeFile(configPath, previous, "utf8");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* OpenCode routes through any OpenAI/Anthropic-compatible endpoint, so it runs
|
|
367
|
+
* on Conduit pump fuel or a local login. Read-only assignments use its `plan`
|
|
368
|
+
* agent (no edit/bash tools); write assignments use `build`.
|
|
369
|
+
*/
|
|
370
|
+
export function openCodeAgentForGrants(grants) {
|
|
371
|
+
if (["repo_write", "test_run", "branch_create", "pr_create"].some((grant) => grants.includes(grant)))
|
|
372
|
+
return "build";
|
|
373
|
+
if (grants.includes("repo_read"))
|
|
374
|
+
return "plan";
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
export function openCodeRunArgs(input, agent) {
|
|
378
|
+
const args = ["run", "--format", "json", "--agent", agent];
|
|
379
|
+
// plan can never write, so --auto only widens the already-bounded build agent.
|
|
380
|
+
if (agent === "build")
|
|
381
|
+
args.push("--auto");
|
|
382
|
+
if (input.resumeSessionId)
|
|
383
|
+
args.push("--session", input.resumeSessionId);
|
|
384
|
+
return args;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* OpenCode `--format json` streams JSONL events. Verified against opencode 1.18:
|
|
388
|
+
* assistant text arrives as `{type:"text", part:{type:"text", text:"…"}}`, the
|
|
389
|
+
* session id is `sessionID` on every event, and failures are `{type:"error",
|
|
390
|
+
* error:{…}}`. Concatenate text parts so the delivery report's fenced block
|
|
391
|
+
* survives multi-step runs.
|
|
392
|
+
*/
|
|
393
|
+
export function parseOpenCodeOutput(stdout) {
|
|
394
|
+
const texts = [];
|
|
395
|
+
let sessionId = null;
|
|
396
|
+
let isError = false;
|
|
397
|
+
let errorMessage = null;
|
|
398
|
+
const whole = stdout.trim();
|
|
399
|
+
for (const line of whole.split("\n")) {
|
|
400
|
+
const trimmed = line.trim();
|
|
401
|
+
if (!trimmed.startsWith("{"))
|
|
402
|
+
continue;
|
|
403
|
+
let event;
|
|
404
|
+
try {
|
|
405
|
+
event = JSON.parse(trimmed);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const session = [event.sessionID, event.session_id].find((value) => typeof value === "string");
|
|
411
|
+
if (typeof session === "string")
|
|
412
|
+
sessionId = session;
|
|
413
|
+
if (event.type === "text" && event.part?.type === "text" && typeof event.part.text === "string")
|
|
414
|
+
texts.push(event.part.text);
|
|
415
|
+
if (event.type === "error") {
|
|
416
|
+
isError = true;
|
|
417
|
+
const err = event.error;
|
|
418
|
+
errorMessage = typeof err === "string" ? err : (typeof err?.data?.message === "string" ? err.data.message : typeof err?.message === "string" ? err.message : null);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const resultText = texts.length ? texts.join("\n") : (isError && errorMessage ? errorMessage : (whole || null));
|
|
422
|
+
return { resultText, sessionId, isError };
|
|
423
|
+
}
|
|
424
|
+
export const openCodeDriver = {
|
|
425
|
+
name: "opencode",
|
|
426
|
+
async run(input) {
|
|
427
|
+
const fuelSource = input.fuelSource === "local" ? "local" : "conduit";
|
|
428
|
+
if (fuelSource === "local" && !hasOpenCodeLogin()) {
|
|
429
|
+
return { status: "failed", resultText: null, sessionId: null, error: "machine set to local fuel but opencode has no login (auth.json / provider key in ~/.local/share/opencode or ~/.config/opencode)" };
|
|
430
|
+
}
|
|
431
|
+
const agent = openCodeAgentForGrants(input.grants);
|
|
432
|
+
if (!agent) {
|
|
433
|
+
return { status: "failed", resultText: null, sessionId: null, error: "No Bridge-mapped OpenCode agent for active grants; refusing to start agent" };
|
|
434
|
+
}
|
|
435
|
+
const args = openCodeRunArgs(input, agent);
|
|
436
|
+
args.push(input.prompt);
|
|
437
|
+
const { code, stdout, stderr } = await execute(input.executable ?? "opencode", args, input.workspace, input.timeoutMs ?? 20 * 60_000, fuelSource === "conduit" ? input.fuel : undefined, fuelSource);
|
|
438
|
+
const parsed = parseOpenCodeOutput(stdout);
|
|
439
|
+
if (code !== 0 || parsed.isError) {
|
|
440
|
+
return { status: "failed", resultText: parsed.resultText, sessionId: parsed.sessionId, error: (stderr || parsed.resultText || `opencode exited with code ${code}`).slice(0, 20_000) };
|
|
441
|
+
}
|
|
442
|
+
return { status: "completed", resultText: parsed.resultText, sessionId: parsed.sessionId };
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Kiro CLI grant map (verified flag surface against kiro-cli 2.13.0).
|
|
447
|
+
* `--trust-tools` takes a comma-separated allowlist of tool NAMES; anything
|
|
448
|
+
* outside it prompts, which in `--no-interactive` mode is a hard stop — the
|
|
449
|
+
* fail-closed contract. Tool names confirmed from the CLI help: fs_read,
|
|
450
|
+
* fs_write, execute_bash. Note the coarseness: kiro-cli's --trust-tools has no
|
|
451
|
+
* per-command scoping, so any bash-requiring grant trusts execute_bash wholesale
|
|
452
|
+
* (Antigravity-tier, not Claude-tier). Fine-grained command allowlisting would
|
|
453
|
+
* need a kiro agent config file — deferred until it can be verified with a login.
|
|
454
|
+
*/
|
|
455
|
+
export function kiroTrustedTools(grants) {
|
|
456
|
+
const tools = [];
|
|
457
|
+
if (grants.includes("repo_read"))
|
|
458
|
+
tools.push("fs_read");
|
|
459
|
+
if (grants.includes("repo_write"))
|
|
460
|
+
tools.push("fs_write");
|
|
461
|
+
if (["test_run", "branch_create", "pr_create"].some((grant) => grants.includes(grant)))
|
|
462
|
+
tools.push("execute_bash");
|
|
463
|
+
return tools;
|
|
464
|
+
}
|
|
465
|
+
export function kiroChatArgs(input, trustedTools) {
|
|
466
|
+
const args = ["chat", "--no-interactive"];
|
|
467
|
+
if (input.resumeSessionId)
|
|
468
|
+
args.push("--resume-id", input.resumeSessionId);
|
|
469
|
+
// Fail closed: an empty allowlist would trust nothing, but we already refused
|
|
470
|
+
// upstream; passing the explicit list keeps ungranted tools blocked.
|
|
471
|
+
args.push("--trust-tools", trustedTools.join(","));
|
|
472
|
+
return args;
|
|
473
|
+
}
|
|
474
|
+
export const kiroDriver = {
|
|
475
|
+
name: "kiro",
|
|
476
|
+
async run(input) {
|
|
477
|
+
const fuelSource = input.fuelSource === "local" ? "local" : "conduit";
|
|
478
|
+
// Kiro authenticates with KIRO_API_KEY or Builder ID login — not Conduit /v1 shims.
|
|
479
|
+
if (fuelSource === "conduit") {
|
|
480
|
+
return { status: "failed", resultText: null, sessionId: null, error: "kiro driver requires local fuel (KIRO_API_KEY or `kiro-cli login`). Run: npx @miraland-labs/conduit-bridge fuel local" };
|
|
481
|
+
}
|
|
482
|
+
const trusted = kiroTrustedTools(input.grants);
|
|
483
|
+
if (!trusted.length) {
|
|
484
|
+
return { status: "failed", resultText: null, sessionId: null, error: "No Bridge-mapped Kiro tools for active grants; refusing to start agent" };
|
|
485
|
+
}
|
|
486
|
+
const executable = input.executable ?? "kiro-cli";
|
|
487
|
+
// ~/.kiro exists even when logged out, so ask the CLI. `whoami` exits non-zero
|
|
488
|
+
// and prints "Not logged in" when unauthenticated — check before the run so we
|
|
489
|
+
// never hang on the interactive browser login in --no-interactive mode.
|
|
490
|
+
if (!hasKiroLogin()) {
|
|
491
|
+
const who = await execute(executable, ["whoami"], input.workspace, 15_000, undefined, "local");
|
|
492
|
+
if (who.code !== 0 || /not logged in/i.test(who.stdout + who.stderr)) {
|
|
493
|
+
return { status: "failed", resultText: null, sessionId: null, error: "kiro has no login (set KIRO_API_KEY or run `kiro-cli login`)" };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const args = kiroChatArgs(input, trusted);
|
|
497
|
+
args.push(input.prompt);
|
|
498
|
+
const { code, stdout, stderr } = await execute(executable, args, input.workspace, input.timeoutMs ?? 20 * 60_000, undefined, "local");
|
|
499
|
+
if (code !== 0) {
|
|
500
|
+
return { status: "failed", resultText: stdout || null, sessionId: null, error: (stderr || stdout || `kiro-cli exited with code ${code}`).slice(0, 20_000) };
|
|
501
|
+
}
|
|
502
|
+
return { status: "completed", resultText: stdout, sessionId: null };
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
/**
|
|
506
|
+
* Antigravity's `agy` CLI (verified against agy 1.1.3): non-interactive runs use
|
|
507
|
+
* `-p`/`--print` with `--mode plan` (read-only) or `--mode accept-edits`. Its
|
|
508
|
+
* enforcement is mode-level, not per-tool — coarser than Claude, the Cursor tier.
|
|
509
|
+
* Write mode adds `--sandbox` for terminal restrictions. Output is plain text.
|
|
510
|
+
*/
|
|
511
|
+
export function antigravityModeForGrants(grants) {
|
|
512
|
+
if (["repo_write", "test_run", "branch_create", "pr_create"].some((grant) => grants.includes(grant)))
|
|
513
|
+
return "accept-edits";
|
|
514
|
+
if (grants.includes("repo_read"))
|
|
515
|
+
return "plan";
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
export function antigravityRunArgs(input, mode) {
|
|
519
|
+
const args = ["-p", "--mode", mode];
|
|
520
|
+
if (mode === "accept-edits")
|
|
521
|
+
args.push("--sandbox");
|
|
522
|
+
args.push(input.prompt);
|
|
523
|
+
return args;
|
|
524
|
+
}
|
|
525
|
+
export const antigravityDriver = {
|
|
526
|
+
name: "antigravity",
|
|
527
|
+
async run(input) {
|
|
528
|
+
const fuelSource = input.fuelSource === "local" ? "local" : "conduit";
|
|
529
|
+
// Antigravity authenticates with a Google login / GEMINI_API_KEY — not Conduit /v1 shims.
|
|
530
|
+
if (fuelSource === "conduit") {
|
|
531
|
+
return { status: "failed", resultText: null, sessionId: null, error: "antigravity driver requires local fuel (GEMINI_API_KEY or `agy` Google login). Run: npx @miraland-labs/conduit-bridge fuel local" };
|
|
532
|
+
}
|
|
533
|
+
if (!hasAntigravityLogin()) {
|
|
534
|
+
return { status: "failed", resultText: null, sessionId: null, error: "antigravity has no login (set GEMINI_API_KEY or sign in with `agy`)" };
|
|
535
|
+
}
|
|
536
|
+
const mode = antigravityModeForGrants(input.grants);
|
|
537
|
+
if (!mode) {
|
|
538
|
+
return { status: "failed", resultText: null, sessionId: null, error: "No Bridge-mapped Antigravity mode for active grants; refusing to start agent" };
|
|
539
|
+
}
|
|
540
|
+
// agy print mode emits plain text and does not surface a resumable id, so rework resume is not wired.
|
|
541
|
+
const { code, stdout, stderr } = await execute(input.executable ?? "agy", antigravityRunArgs({ prompt: input.prompt, grants: input.grants }, mode), input.workspace, input.timeoutMs ?? 20 * 60_000, undefined, "local");
|
|
542
|
+
if (code !== 0) {
|
|
543
|
+
return { status: "failed", resultText: stdout || null, sessionId: null, error: (stderr || stdout || `agy exited with code ${code}`).slice(0, 20_000) };
|
|
544
|
+
}
|
|
545
|
+
return { status: "completed", resultText: stdout, sessionId: null };
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
/** Stable driver ids shown in Connect / CLI. Order is product preference, not exclusivity. */
|
|
549
|
+
export const SUPPORTED_AGENTS = [
|
|
550
|
+
{ id: "claude-code", label: "Claude Code", executableHint: "claude" },
|
|
551
|
+
{ id: "codex", label: "Codex (ChatGPT)", executableHint: "codex" },
|
|
552
|
+
{ id: "cursor", label: "Cursor Agent", executableHint: "agent" },
|
|
553
|
+
{ id: "opencode", label: "OpenCode", executableHint: "opencode" },
|
|
554
|
+
{ id: "kiro", label: "Kiro CLI", executableHint: "kiro-cli" },
|
|
555
|
+
{ id: "antigravity", label: "Antigravity (agy)", executableHint: "agy" },
|
|
556
|
+
];
|
|
557
|
+
export const DRIVERS = {
|
|
558
|
+
"claude-code": claudeCodeDriver,
|
|
559
|
+
codex: codexDriver,
|
|
560
|
+
cursor: cursorDriver,
|
|
561
|
+
opencode: openCodeDriver,
|
|
562
|
+
kiro: kiroDriver,
|
|
563
|
+
antigravity: antigravityDriver,
|
|
564
|
+
};
|
|
565
|
+
function execute(executable, args, cwd, timeoutMs, fuel, fuelSource = "conduit", stdinText) {
|
|
158
566
|
return new Promise((resolve, reject) => {
|
|
159
|
-
const child = spawn(executable, args, {
|
|
567
|
+
const child = spawn(executable, args, {
|
|
568
|
+
cwd,
|
|
569
|
+
stdio: [stdinText !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
570
|
+
env: boundedEnvironment(fuel, fuelSource),
|
|
571
|
+
});
|
|
160
572
|
let stdout = "";
|
|
161
573
|
let stderr = "";
|
|
162
574
|
const timer = setTimeout(() => { child.kill("SIGTERM"); setTimeout(() => child.kill("SIGKILL"), 10_000).unref(); }, timeoutMs);
|
|
163
|
-
child.stdout
|
|
164
|
-
child.stderr
|
|
575
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
576
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
165
577
|
child.on("error", (error) => { clearTimeout(timer); reject(error); });
|
|
166
578
|
child.on("close", (code) => { clearTimeout(timer); resolve({ code, stdout, stderr: stderr.slice(0, 20_000) }); });
|
|
579
|
+
if (stdinText !== undefined && child.stdin) {
|
|
580
|
+
child.stdin.write(stdinText);
|
|
581
|
+
child.stdin.end();
|
|
582
|
+
}
|
|
167
583
|
});
|
|
168
584
|
}
|
|
169
585
|
const LOCAL_VENDOR_ENV = [
|
|
170
586
|
"ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL",
|
|
171
|
-
"OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE",
|
|
587
|
+
"OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "CODEX_API_KEY",
|
|
588
|
+
"CURSOR_API_KEY", "KIRO_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY",
|
|
172
589
|
];
|
|
173
590
|
function boundedEnvironment(fuel, fuelSource = "conduit") {
|
|
174
591
|
// Proxy and TLS variables stay: fueled agents must reach Conduit's /v1 on
|
|
@@ -183,17 +600,53 @@ function boundedEnvironment(fuel, fuelSource = "conduit") {
|
|
|
183
600
|
env.ANTHROPIC_BASE_URL = v1;
|
|
184
601
|
env.OPENAI_API_KEY = fuel.gatewayKey;
|
|
185
602
|
env.OPENAI_BASE_URL = v1;
|
|
603
|
+
env.CODEX_API_KEY = fuel.gatewayKey;
|
|
186
604
|
}
|
|
187
605
|
return env;
|
|
188
606
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
607
|
+
export function hasClaudeLogin(env = process.env) {
|
|
608
|
+
if (env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN)
|
|
609
|
+
return true;
|
|
610
|
+
const home = env.HOME;
|
|
611
|
+
return Boolean(home && existsSync(join(home, ".claude")));
|
|
612
|
+
}
|
|
613
|
+
export function hasOpenAiLogin(env = process.env) {
|
|
614
|
+
if (env.OPENAI_API_KEY || env.CODEX_API_KEY)
|
|
615
|
+
return true;
|
|
616
|
+
const home = env.HOME;
|
|
617
|
+
return Boolean(home && existsSync(join(home, ".codex", "auth.json")));
|
|
618
|
+
}
|
|
619
|
+
export function hasCursorLogin(env = process.env) {
|
|
620
|
+
if (env.CURSOR_API_KEY)
|
|
621
|
+
return true;
|
|
622
|
+
const home = env.HOME;
|
|
623
|
+
return Boolean(home && (existsSync(join(home, ".cursor")) || existsSync(join(home, ".config", "cursor"))));
|
|
624
|
+
}
|
|
625
|
+
export function hasOpenCodeLogin(env = process.env) {
|
|
626
|
+
if (env.OPENAI_API_KEY || env.ANTHROPIC_API_KEY || env.OPENROUTER_API_KEY)
|
|
192
627
|
return true;
|
|
193
628
|
const home = env.HOME;
|
|
194
629
|
if (!home)
|
|
195
630
|
return false;
|
|
196
|
-
return existsSync(join(home, ".
|
|
631
|
+
return existsSync(join(home, ".local", "share", "opencode", "auth.json")) || existsSync(join(home, ".config", "opencode"));
|
|
632
|
+
}
|
|
633
|
+
export function hasKiroLogin(env = process.env) {
|
|
634
|
+
// Only KIRO_API_KEY is a reliable headless signal: ~/.kiro exists even when
|
|
635
|
+
// logged out (the IDE creates it), so a login is confirmed at spawn via
|
|
636
|
+
// `kiro-cli whoami` rather than a directory probe.
|
|
637
|
+
return Boolean(env.KIRO_API_KEY);
|
|
638
|
+
}
|
|
639
|
+
export function hasAntigravityLogin(env = process.env) {
|
|
640
|
+
if (env.GEMINI_API_KEY || env.GOOGLE_API_KEY)
|
|
641
|
+
return true;
|
|
642
|
+
const home = env.HOME;
|
|
643
|
+
// agy 1.1.3 stores its Google session state under ~/.gemini (shared with the IDE).
|
|
644
|
+
return Boolean(home && (existsSync(join(home, ".gemini")) || existsSync(join(home, ".antigravity"))));
|
|
645
|
+
}
|
|
646
|
+
/** True when local fuel can use a host vendor login for at least one supported agent. */
|
|
647
|
+
export function hasLocalVendorLogin(env = process.env) {
|
|
648
|
+
return hasClaudeLogin(env) || hasOpenAiLogin(env) || hasCursorLogin(env)
|
|
649
|
+
|| hasOpenCodeLogin(env) || hasKiroLogin(env) || hasAntigravityLogin(env);
|
|
197
650
|
}
|
|
198
651
|
/** Exported for tests — builds the stripped process env with optional Conduit fuel. */
|
|
199
652
|
export function agentProcessEnv(fuel, fuelSource = "conduit") {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Conduit Bridge CLI — join, connect, and run
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Conduit Bridge CLI — join, connect, and run Claude Code / Codex / Cursor / OpenCode / Kiro / Antigravity agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conduit": "dist/cli.js"
|