4NK 14c974f54c Add smart-ide-tools-bridge API for submodule tools + central local env
- New service: tools bridge (port 37147) registry + Carbonyl/PageIndex/Chandra POST jobs
- config/services.local.env.example and gitignore for services.local.env
- .env.example for repos-devtools, regex-search, ia-dev-gateway, orchestrator, claw proxy, langextract
- Orchestrator intents: tools.registry, tools.carbonyl.plan, tools.pageindex.run, tools.chandra.ocr
- Docs: API + repo service fiche, architecture index; do not commit dist/
2026-04-03 22:35:57 +02:00

263 lines
8.2 KiB
TypeScript

import * as http from "node:http";
import { readExpectedToken, requireBearer } from "./auth.js";
import { readJsonBody } from "./httpUtil.js";
const HOST = process.env.ORCHESTRATOR_HOST ?? "127.0.0.1";
const PORT = Number(process.env.ORCHESTRATOR_PORT ?? "37145");
type RouteTarget = "ollama" | "anythingllm" | "service" | "ia_dev";
type Resolution = {
resolved: boolean;
target?: RouteTarget;
action?: string;
upstream?: { method: string; url: string; headersHint: string[] };
reason?: string;
};
const ollamaBase = (): string =>
(process.env.OLLAMA_URL ?? "http://127.0.0.1:11434").replace(/\/+$/, "");
const anythingLlmBase = (): string =>
(process.env.ANYTHINGLLM_BASE_URL ?? "").replace(/\/+$/, "");
const reposDevtoolsUrl = (): string =>
(process.env.REPOS_DEVTOOLS_URL ?? "http://127.0.0.1:37140").replace(/\/+$/, "");
const regexSearchUrl = (): string =>
(process.env.REGEX_SEARCH_URL ?? "http://127.0.0.1:37143").replace(/\/+$/, "");
const langextractUrl = (): string =>
(process.env.LANGEXTRACT_URL ?? "http://127.0.0.1:37141").replace(/\/+$/, "");
const localOfficeUrl = (): string =>
(process.env.LOCAL_OFFICE_URL ?? "http://127.0.0.1:8000").replace(/\/+$/, "");
const iaDevGatewayUrl = (): string =>
(process.env.IA_DEV_GATEWAY_URL ?? "http://127.0.0.1:37144").replace(/\/+$/, "");
const toolsBridgeUrl = (): string =>
(process.env.TOOLS_BRIDGE_URL ?? "http://127.0.0.1:37147").replace(/\/+$/, "");
const resolveIntent = (intent: string): Resolution => {
switch (intent) {
case "code.complete":
case "chat.local":
return {
resolved: true,
target: "ollama",
action: "generate",
upstream: { method: "POST", url: `${ollamaBase()}/api/generate`, headersHint: ["Content-Type"] },
};
case "rag.query":
return {
resolved: true,
target: "anythingllm",
action: "workspace_chat",
upstream: {
method: "POST",
url: anythingLlmBase()
? `${anythingLlmBase()}/api/v1/workspace/...`
: "configure ANYTHINGLLM_BASE_URL",
headersHint: ["Authorization", "Content-Type"],
},
};
case "git.clone":
return {
resolved: true,
target: "service",
action: "repos_clone",
upstream: { method: "POST", url: `${reposDevtoolsUrl()}/repos-clone`, headersHint: ["Authorization", "Content-Type"] },
};
case "search.regex":
return {
resolved: true,
target: "service",
action: "regex_search",
upstream: { method: "POST", url: `${regexSearchUrl()}/search`, headersHint: ["Authorization", "Content-Type"] },
};
case "extract.entities":
return {
resolved: true,
target: "service",
action: "langextract",
upstream: { method: "POST", url: `${langextractUrl()}/extract`, headersHint: ["Authorization", "Content-Type"] },
};
case "doc.office.upload":
return {
resolved: true,
target: "service",
action: "local_office_documents",
upstream: { method: "POST", url: `${localOfficeUrl()}/documents`, headersHint: ["X-API-Key", "Content-Type"] },
};
case "agent.run":
return {
resolved: true,
target: "ia_dev",
action: "post_run",
upstream: { method: "POST", url: `${iaDevGatewayUrl()}/v1/runs`, headersHint: ["Authorization", "Content-Type"] },
};
case "tools.registry":
return {
resolved: true,
target: "service",
action: "tools_bridge_registry",
upstream: {
method: "GET",
url: `${toolsBridgeUrl()}/v1/registry`,
headersHint: ["Authorization"],
},
};
case "tools.carbonyl.plan":
return {
resolved: true,
target: "service",
action: "tools_carbonyl_open_plan",
upstream: {
method: "POST",
url: `${toolsBridgeUrl()}/v1/carbonyl/open-plan`,
headersHint: ["Authorization", "Content-Type"],
},
};
case "tools.pageindex.run":
return {
resolved: true,
target: "service",
action: "tools_pageindex_run",
upstream: {
method: "POST",
url: `${toolsBridgeUrl()}/v1/pageindex/run`,
headersHint: ["Authorization", "Content-Type"],
},
};
case "tools.chandra.ocr":
return {
resolved: true,
target: "service",
action: "tools_chandra_ocr",
upstream: {
method: "POST",
url: `${toolsBridgeUrl()}/v1/chandra/ocr`,
headersHint: ["Authorization", "Content-Type"],
},
};
default:
return { resolved: false, reason: `Unknown intent: ${intent}` };
}
};
const json = (res: http.ServerResponse, status: number, body: unknown): void => {
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(body));
};
const isRecord = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" && v !== null && !Array.isArray(v);
const timeline: { at: string; type: string; summary: string; runId?: string; projectId?: string }[] = [];
const main = (): void => {
const token = readExpectedToken();
if (token.length === 0) {
console.error("smart-ide-orchestrator: set ORCHESTRATOR_TOKEN (non-empty secret).");
process.exit(1);
}
const server = http.createServer((req, res) => {
void (async () => {
try {
const url = new URL(req.url ?? "/", `http://${HOST}`);
const p = url.pathname;
if (req.method === "GET" && (p === "/health" || p === "/health/")) {
json(res, 200, { status: "ok" });
return;
}
if (req.method === "GET" && p === "/v1/timeline") {
if (!requireBearer(req, res, token)) {
return;
}
json(res, 200, { items: timeline.slice(-100) });
return;
}
if (req.method === "POST" && p === "/v1/route") {
if (!requireBearer(req, res, token)) {
return;
}
const body = await readJsonBody(req);
if (!isRecord(body) || typeof body.intent !== "string") {
json(res, 422, { error: "Missing intent (string)" });
return;
}
const dryRun = body.dryRun === true;
const r = resolveIntent(body.intent);
if (!r.resolved) {
json(res, 200, { resolved: false, reason: r.reason });
return;
}
if (dryRun) {
json(res, 200, {
resolved: true,
target: r.target,
action: r.action,
upstream: r.upstream,
});
return;
}
json(res, 200, {
resolved: true,
target: r.target,
action: r.action,
upstream: r.upstream,
note: "Use POST /v1/execute to forward (stub)",
});
return;
}
if (req.method === "POST" && p === "/v1/execute") {
if (!requireBearer(req, res, token)) {
return;
}
const body = await readJsonBody(req);
if (!isRecord(body) || typeof body.intent !== "string") {
json(res, 422, { error: "Missing intent (string)" });
return;
}
const r = resolveIntent(body.intent);
if (!r.resolved) {
json(res, 422, { error: r.reason });
return;
}
timeline.push({
at: new Date().toISOString(),
type: "execute",
summary: `${body.intent} -> ${r.target}/${r.action}`,
projectId: typeof body.projectId === "string" ? body.projectId : undefined,
});
json(res, 200, {
ok: true,
forwarded: false,
message:
"Stub: call upstream yourself or extend orchestrator with fetch() and service tokens.",
resolution: r,
});
return;
}
json(res, 404, { error: "Not found" });
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
json(res, 400, { error: msg });
}
})();
});
server.listen(PORT, HOST, () => {
console.error(`smart-ide-orchestrator listening on http://${HOST}:${PORT}`);
});
};
main();