diff --git a/client/conduit.js b/client/conduit.mjs similarity index 89% rename from client/conduit.js rename to client/conduit.mjs index adcc18e..d743a6a 100644 --- a/client/conduit.js +++ b/client/conduit.mjs @@ -1,7 +1,7 @@ // Conduit client module — import this when using conduit programmatically. // Designed for use by Claude or other tools that want to call conduit actions. -const BASE_URL = process.env.CONDUIT_URL || "http://localhost:3333"; +const BASE_URL = process.env.CONDUIT_URL || "http://localhost:3015"; export async function callAction(action, params = {}) { const res = await fetch(`${BASE_URL}/action`, { diff --git a/client/index.js b/client/index.mjs similarity index 83% rename from client/index.js rename to client/index.mjs index 69e5d6f..57dfe17 100644 --- a/client/index.js +++ b/client/index.mjs @@ -1,11 +1,11 @@ #!/usr/bin/env node // Conduit client — thin CLI wrapper for Claude to call the conduit server. // Usage: -// node client/index.js [key=value ...] -// node client/index.js list-actions -// node client/index.js edit-file filename=/workspace/foo.js +// node client/index.mjs [key=value ...] +// node client/index.mjs list-actions +// node client/index.mjs edit-file filename=/workspace/foo.js -const BASE_URL = process.env.CONDUIT_URL || "http://localhost:3333"; +const BASE_URL = process.env.CONDUIT_URL || "http://localhost:3015"; async function callAction(action, params = {}) { const res = await fetch(`${BASE_URL}/action`, { diff --git a/package.json b/package.json index 7f03944..bbbad23 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "A supervised action bridge between Claude Code and the host system", "type": "module", "scripts": { - "server": "node server/index.js", - "client": "node client/index.js" + "server": "node server/index.mjs", + "client": "node client/index.mjs" }, "dependencies": { "express": "^5.2.1" diff --git a/server/actions.js b/server/actions.mjs similarity index 87% rename from server/actions.js rename to server/actions.mjs index fcf83e3..371979a 100644 --- a/server/actions.js +++ b/server/actions.mjs @@ -1,6 +1,8 @@ // Action registry — defines all available actions, their parameters, and policies. // policy: "auto-accept" | "auto-deny" | "queue" +import { resolvePath, exec } from "./helpers.mjs"; + export const actions = { "list-actions": { description: "List all available actions and their definitions", @@ -20,7 +22,7 @@ export const actions = { description: "Open a file in the editor", params: [{ name: "filename", required: true, type: "path" }], policy: "auto-accept", - handler: async ({ filename }, { resolvePath, exec }) => { + handler: async ({ filename }) => { const resolved = resolvePath(filename); await exec("xdg-open", [resolved]); return { opened: resolved }; @@ -31,7 +33,7 @@ export const actions = { description: "Open a directory in the file manager", params: [{ name: "path", required: true, type: "path" }], policy: "auto-accept", - handler: async ({ path }, { resolvePath, exec }) => { + handler: async ({ path }) => { const resolved = resolvePath(path); await exec("xdg-open", [resolved]); return { opened: resolved }; @@ -42,7 +44,7 @@ export const actions = { description: "Open a URL in the web browser", params: [{ name: "url", required: true, type: "string" }], policy: "queue", - handler: async ({ url }, { exec }) => { + handler: async ({ url }) => { await exec("xdg-open", [url]); return { opened: url }; }, @@ -52,7 +54,7 @@ export const actions = { description: "Open a terminal in a given directory", params: [{ name: "path", required: false, type: "path" }], policy: "queue", - handler: async ({ path }, { resolvePath, exec }) => { + handler: async ({ path }) => { const resolved = path ? resolvePath(path) : process.env.HOME; await exec("xdg-open", [resolved]); return { opened: resolved }; diff --git a/server/helpers.mjs b/server/helpers.mjs new file mode 100644 index 0000000..497013d --- /dev/null +++ b/server/helpers.mjs @@ -0,0 +1,25 @@ +import { spawnSync } from "child_process"; +import path from "path"; + +const WORKSPACE_ROOT = process.env.CONDUIT_ROOT || "/workspace"; + +// Resolve a path param relative to WORKSPACE_ROOT, preventing traversal. +export function resolvePath(userPath) { + const resolved = path.resolve(WORKSPACE_ROOT, userPath.replace(/^\//, "")); + if (!resolved.startsWith(WORKSPACE_ROOT)) { + throw new Error(`Path escapes workspace root: ${userPath}`); + } + return resolved; +} + +// Execute a binary with an argument list — no shell interpolation. +export function exec(bin, args = []) { + return new Promise((resolve, reject) => { + const result = spawnSync(bin, args, { stdio: "inherit" }); + if (result.error) { + reject(result.error); + } else { + resolve(result.status); + } + }); +} diff --git a/server/index.js b/server/index.mjs similarity index 71% rename from server/index.js rename to server/index.mjs index 9a79a43..08e7399 100644 --- a/server/index.js +++ b/server/index.mjs @@ -1,38 +1,12 @@ import express from "express"; -import { spawnSync } from "child_process"; -import path from "path"; -import { actions } from "./actions.js"; -import { enqueue, getEntry, listPending, resolve } from "./queue.js"; +import { actions } from "./actions.mjs"; +import { enqueue, getEntry, listPending, resolve } from "./queue.mjs"; const PORT = process.env.CONDUIT_PORT || 3015; -const WORKSPACE_ROOT = process.env.CONDUIT_ROOT || "/workspace"; const app = express(); app.use(express.json()); -// Resolve a path param relative to WORKSPACE_ROOT, preventing traversal. -function resolvePath(userPath) { - const resolved = path.resolve(WORKSPACE_ROOT, userPath.replace(/^\//, "")); - if (!resolved.startsWith(WORKSPACE_ROOT)) { - throw new Error(`Path escapes workspace root: ${userPath}`); - } - return resolved; -} - -// Execute a binary with an argument list — no shell interpolation. -function exec(bin, args = []) { - return new Promise((resolve, reject) => { - const result = spawnSync(bin, args, { stdio: "inherit" }); - if (result.error) { - reject(result.error); - } else { - resolve(result.status); - } - }); -} - -const helpers = { resolvePath, exec }; - // Validate that all required params are present. function validateParams(actionDef, params) { const errors = []; @@ -68,7 +42,7 @@ app.post("/action", async (req, res) => { if (def.policy === "auto-accept") { try { - const result = await def.handler(params, helpers); + const result = await def.handler(params); return res.json({ status: "accepted", result }); } catch (err) { return res.status(500).json({ status: "error", error: err.message }); @@ -100,7 +74,7 @@ app.post("/queue/:id/approve", async (req, res) => { const def = actions[entry.action]; try { - const result = await def.handler(entry.params, helpers); + const result = await def.handler(entry.params); res.json({ status: "approved", result }); } catch (err) { res.status(500).json({ status: "error", error: err.message }); @@ -123,5 +97,5 @@ app.post("/queue/:id/deny", (req, res) => { app.listen(PORT, () => { console.log(`claude-code-conduit server running on port ${PORT}`); - console.log(`Workspace root: ${WORKSPACE_ROOT}`); + console.log(`Workspace root: ${process.env.CONDUIT_ROOT || "/workspace"}`); }); diff --git a/server/queue.js b/server/queue.mjs similarity index 100% rename from server/queue.js rename to server/queue.mjs