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:
+18
-5
@@ -14,6 +14,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { db } = require("../db");
|
||||
const lanesLib = require("../lib/lanes");
|
||||
const aiTools = require("../lib/ai-tools");
|
||||
const {
|
||||
listPipelines,
|
||||
getPipeline,
|
||||
@@ -333,7 +334,7 @@ router.post("/ensure", sameOriginGuard, (req, res) => {
|
||||
typeof body.cwd === "string" && body.cwd.trim() ? body.cwd.trim() : os.homedir()
|
||||
);
|
||||
|
||||
if (body.tool !== undefined && !lanesLib.VALID_TOOLS.has(body.tool)) {
|
||||
if (body.tool !== undefined && !aiTools.has(body.tool)) {
|
||||
return res.status(400).json({ error: { code: "EBADTOOL", message: "unknown tool" } });
|
||||
}
|
||||
|
||||
@@ -370,7 +371,12 @@ router.post("/ensure", sameOriginGuard, (req, res) => {
|
||||
broadcastLane(lane.id);
|
||||
return res.status(201).json({ lane: payload(lane), created: true });
|
||||
} catch (err) {
|
||||
if (err.code === "EBADCWD" || err.code === "EBADPIPELINE" || err.code === "EBADTOOL") {
|
||||
if (
|
||||
err.code === "EBADCWD" ||
|
||||
err.code === "EBADPIPELINE" ||
|
||||
err.code === "EBADTOOL" ||
|
||||
err.code === "EDISABLED"
|
||||
) {
|
||||
return res.status(400).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
// The cwd UNIQUE constraint is the arbiter: someone else won the race, so
|
||||
@@ -457,7 +463,9 @@ router.post("/", sameOriginGuard, (req, res) => {
|
||||
err.code === "EBADCWD" ||
|
||||
err.code === "EBADPIPELINE" ||
|
||||
err.code === "EBADKIND" ||
|
||||
err.code === "EBADFIELD"
|
||||
err.code === "EBADFIELD" ||
|
||||
err.code === "EBADTOOL" ||
|
||||
err.code === "EDISABLED"
|
||||
) {
|
||||
return res.status(400).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
@@ -1044,8 +1052,12 @@ router.get("/:id/ci/status", async (req, res) => {
|
||||
router.post("/worktree", sameOriginGuard, async (req, res) => {
|
||||
const body = req.body || {};
|
||||
|
||||
if (body.tool !== undefined && !lanesLib.VALID_TOOLS.has(body.tool)) {
|
||||
return res.status(400).json({ error: { code: "EBADTOOL", message: "unknown tool" } });
|
||||
// Checked before any git work: a worktree for a tool that cannot get a lane
|
||||
// would be left behind with no row pointing at it.
|
||||
try {
|
||||
lanesLib.validateTool(body.tool === undefined ? "claude" : body.tool);
|
||||
} catch (err) {
|
||||
return res.status(400).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
|
||||
const sourceRepo = body.sourceRepo;
|
||||
@@ -1709,6 +1721,7 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
|
||||
sandbox: body.sandbox,
|
||||
approval: body.approval,
|
||||
resumeLast: body.resumeLast === true,
|
||||
bypass: body.bypass === true,
|
||||
resumeSessionId: body.resumeSessionId || (body.resume ? current.session_id : undefined),
|
||||
});
|
||||
startedRunId = handle.id;
|
||||
|
||||
Reference in New Issue
Block a user