feat(ai-tools): catalog of AI coding CLIs, detection, enable/default, lanes for any tool (#40 A1)
server/lib/ai-tools.js loads server/data/ai-tools/*.json (38 tools, data from Orca, MIT) plus DASHBOARD_AI_TOOLS_DIR, detects each CLI by walking PATH, and keeps enable/default in <data dir>/ai-tools.json. shell-path.js merges the login shell's PATH at startup and on Refresh. /api/ai-tools lists, refreshes and toggles; /api/run/binary resolves through the catalog. Lanes accept any catalog id (EDISABLED for a disabled one on creation, ENOTOOL at start once a tool left the catalog). pty-run keeps the Claude/Codex builders and builds every other tool's argv from its catalog file, pasting an after-start prompt through a tmux buffer; every pane gets the hydrated PATH. A per-run bypass switch appends the catalog's bypass args (Codex included). POST /api/run now forwards sandbox/approval/resumeLast, which it dropped. agenthub lanes add gains --tool.
This commit is contained in:
+15
-12
@@ -29,6 +29,7 @@ const runs = require("../lib/pty-run");
|
||||
const tmux = require("../lib/tmux");
|
||||
const lanesLib = require("../lib/lanes");
|
||||
const { sameOriginGuard } = require("../lib/same-origin");
|
||||
const aiTools = require("../lib/ai-tools");
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -286,21 +287,16 @@ router.get("/files", (req, res) => {
|
||||
|
||||
router.get("/binary", (req, res) => {
|
||||
// Surface whether the lane's CLI is on PATH so the UI can show a helpful
|
||||
// error before the user clicks Run. We don't actually invoke it. The name is
|
||||
// taken from a fixed allowlist, never from the query string directly — this
|
||||
// string reaches `which`.
|
||||
// error before the user clicks Run. We don't actually invoke it. The tool id
|
||||
// is looked up in the AI Tools catalog; only a catalog file names a command.
|
||||
const tool = req.query.tool === undefined ? "claude" : String(req.query.tool);
|
||||
const bins = { claude: "claude", codex: "codex", antigravity: "agy" };
|
||||
if (!Object.hasOwn(bins, tool)) {
|
||||
if (!aiTools.has(tool)) {
|
||||
return res.status(400).json({ error: { code: "EBADTOOL", message: "unknown tool" } });
|
||||
}
|
||||
const which = require("node:child_process").spawnSync(
|
||||
process.platform === "win32" ? "where" : "which",
|
||||
[bins[tool]],
|
||||
{ encoding: "utf8" }
|
||||
);
|
||||
const stdout = (which.stdout || "").trim();
|
||||
res.json({ found: which.status === 0 && stdout.length > 0, path: stdout || null });
|
||||
// Fresh, not the catalog's cache: a CLI installed while the dashboard is up
|
||||
// should clear the missing-binary banner on the next look.
|
||||
const d = aiTools.detect(tool, { fresh: true });
|
||||
res.json({ found: d.installed, path: d.path });
|
||||
});
|
||||
|
||||
router.get("/tmux", (_req, res) => {
|
||||
@@ -333,6 +329,13 @@ router.post("/", (req, res) => {
|
||||
? body.permissionMode
|
||||
: "auto",
|
||||
effort: typeof body.effort === "string" && body.effort ? body.effort : null,
|
||||
// Codex's own fields, checked against pty-run's allowlists there.
|
||||
sandbox: typeof body.sandbox === "string" && body.sandbox ? body.sandbox : null,
|
||||
approval: typeof body.approval === "string" && body.approval ? body.approval : null,
|
||||
resumeLast: body.resumeLast === true,
|
||||
// Only for this run, only when the user ticked it; the args come from
|
||||
// the lane tool's catalog file, never from here.
|
||||
bypass: body.bypass === true,
|
||||
resumeSessionId:
|
||||
typeof body.resumeSessionId === "string" && body.resumeSessionId
|
||||
? body.resumeSessionId
|
||||
|
||||
Reference in New Issue
Block a user