fix(plugins): re-export probe symbols + declare plugin deps in dashboard

- Hermes / OpenClaw plugin index.ts now re-export `probeHermesBinary` /
  `probeOpenClawBinary` and their status types so the dashboard's
  `runtime-provider-probes.ts` façade can import them via the public
  package entry instead of deep paths.
- Dashboard `package.json` adds `@fusion-plugin-examples/hermes-runtime`,
  `…/openclaw-runtime`, `…/paperclip-runtime` as workspace deps so
  pnpm symlinks them into `packages/dashboard/node_modules/`. Without
  these, the new probe imports failed with "Cannot find module" during
  `pnpm typecheck`.

This clears 6 of the 9 outstanding typecheck errors. The remaining 3 are
in the in-flight Hermes plugin rewrite (runtime-adapter still imports
from a deleted `./pi-module.js`; the new `index.ts` calls a factory
with the wrong arg type) and should be resolved by the same change set
that landed the rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Fusion
2026-04-27 20:22:18 -07:00
committed by gsxdsm
parent 5159cc2a68
commit 6e65786bc5
82 changed files with 8481 additions and 2833 deletions

View File

@@ -0,0 +1,265 @@
import { ApiError, badRequest } from "../api-error.js";
import {
discoverPaperclipCli,
listHermesProviderProfiles,
listPaperclipCompanies,
listPaperclipCompanyAgents,
mintPaperclipKeyViaCli,
probeHermesProvider,
probeOpenClawProvider,
probePaperclipProvider,
} from "../runtime-provider-probes.js";
import type { ApiRouteRegistrar } from "./types.js";
/**
* Registers three read-only status probes for the runtime provider plugins:
*
* GET /providers/hermes/status
* GET /providers/openclaw/status
* GET /providers/paperclip/status
*
* These routes mirror the existing GET /providers/claude-cli/status pattern
* in register-auth-routes.ts. They intentionally do NOT require authentication
* because they are introspection endpoints consumed by the provider-card UI.
*/
export const registerRuntimeProviderRoutes: ApiRouteRegistrar = (ctx) => {
const { router, rethrowAsApiError } = ctx;
/**
* GET /providers/hermes/status
*
* Query params:
* binaryPath? — override the hermes binary path (default: "hermes")
*
* Response: { binary: HermesBinaryStatus, ready: boolean }
* ready === binary.available
*/
router.get("/providers/hermes/status", async (req, res) => {
try {
const binaryPath =
typeof req.query.binaryPath === "string" ? req.query.binaryPath : undefined;
const binary = await probeHermesProvider({ binaryPath });
res.json({ binary, ready: binary.available });
} catch (err) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err);
}
});
/**
* GET /providers/hermes/profiles
*
* Query params:
* binaryPath? — override the hermes binary path (default: "hermes")
*
* Response: { profiles: HermesProfileSummary[] }
* On any error: { profiles: [], error: "<reason>" } with HTTP 200.
*/
router.get("/providers/hermes/profiles", async (req, res) => {
const binaryPath =
typeof req.query.binaryPath === "string" ? req.query.binaryPath : undefined;
try {
const profiles = await listHermesProviderProfiles({ binaryPath });
res.json({ profiles });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
res.json({ profiles: [], error: message });
}
});
/**
* GET /providers/openclaw/status
*
* Query params:
* binaryPath? — override the openclaw binary path (default: "openclaw")
*
* Response: { binary: OpenClawBinaryStatus, ready: boolean }
* ready === binary.available
*/
router.get("/providers/openclaw/status", async (req, res) => {
try {
const binaryPath =
typeof req.query.binaryPath === "string" ? req.query.binaryPath : undefined;
const binary = await probeOpenClawProvider({ binaryPath });
res.json({ binary, ready: binary.available });
} catch (err) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err);
}
});
/**
* GET /providers/paperclip/status
*
* Query params:
* apiUrl — REQUIRED. The Paperclip server base URL (must start with http:// or https://).
* apiKey? — Optional API key forwarded to the probe.
*
* Response: { connection: PaperclipConnectionStatus, ready: boolean }
* ready === connection.available
*
* Returns 400 when apiUrl is missing or does not start with http:// or https://.
*/
router.get("/providers/paperclip/status", async (req, res) => {
try {
const rawApiUrl = req.query.apiUrl;
if (typeof rawApiUrl !== "string" || rawApiUrl.trim() === "") {
throw badRequest("Missing required query parameter: apiUrl");
}
const apiUrl = rawApiUrl.trim();
if (!apiUrl.startsWith("http://") && !apiUrl.startsWith("https://")) {
throw badRequest("Invalid apiUrl: must start with http:// or https://");
}
const apiKey =
typeof req.query.apiKey === "string" ? req.query.apiKey : undefined;
const connection = await probePaperclipProvider({ apiUrl, apiKey });
res.json({ connection, ready: connection.available });
} catch (err) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err);
}
});
/**
* GET /providers/paperclip/companies
*
* Query: apiUrl (required), apiKey?
* Returns: { companies: PaperclipCompanySummary[] } — never throws on
* upstream failure; an empty list signals "no companies visible".
*/
router.get("/providers/paperclip/companies", async (req, res) => {
try {
const apiUrl = readApiUrl(req.query.apiUrl);
const apiKey =
typeof req.query.apiKey === "string" ? req.query.apiKey : undefined;
const companies = await listPaperclipCompanies({ apiUrl, apiKey });
res.json({ companies });
} catch (err) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err);
}
});
/**
* GET /providers/paperclip/agents
*
* Query: apiUrl (required), companyId (required), apiKey?
* Returns: { agents: PaperclipAgentSummary[] } — empty list on upstream error.
*/
router.get("/providers/paperclip/agents", async (req, res) => {
try {
const apiUrl = readApiUrl(req.query.apiUrl);
const apiKey =
typeof req.query.apiKey === "string" ? req.query.apiKey : undefined;
const companyId =
typeof req.query.companyId === "string"
? req.query.companyId.trim()
: "";
if (!companyId) {
throw badRequest("Missing required query parameter: companyId");
}
const agents = await listPaperclipCompanyAgents({ apiUrl, apiKey, companyId });
res.json({ agents });
} catch (err) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err);
}
});
/**
* GET /providers/paperclip/cli-discovery
*
* Query: cliConfigPath?
* Returns the result of reading the local paperclipai config (apiUrl,
* deploymentMode, etc.). Never throws — failures are reported as
* `{ ok: false, reason }` so the card can render a clear error state.
*/
router.get("/providers/paperclip/cli-discovery", async (req, res) => {
try {
const cliConfigPath =
typeof req.query.cliConfigPath === "string"
? req.query.cliConfigPath
: undefined;
const result = await discoverPaperclipCli({ cliConfigPath });
res.json(result);
} catch (err) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err);
}
});
/**
* POST /providers/paperclip/cli-mint-key
*
* Body: { agentRef (required), cliBinaryPath?, companyId?, keyName?, configPath?, dataDir? }
*
* Spawns `paperclipai agent local-cli <agentRef> --json --no-install-skills` to
* mint a fresh agent API key. Always responds HTTP 200; failures are reported as
* `{ ok: false, reason }` so the card can render an inline error.
*
* Returns 400 if `agentRef` is missing or empty.
*/
router.post("/providers/paperclip/cli-mint-key", async (req, res) => {
try {
const body = (req.body ?? {}) as Record<string, unknown>;
const agentRef =
typeof body.agentRef === "string" ? body.agentRef.trim() : "";
if (!agentRef) {
throw badRequest("Missing required body field: agentRef");
}
const cliBinaryPath =
typeof body.cliBinaryPath === "string" && body.cliBinaryPath.trim()
? body.cliBinaryPath.trim()
: undefined;
const companyId =
typeof body.companyId === "string" ? body.companyId.trim() : "";
if (!companyId) {
throw badRequest(
"Missing required body field: companyId (paperclipai agent local-cli requires -C/--company-id)",
);
}
const keyName =
typeof body.keyName === "string" && body.keyName.trim()
? body.keyName.trim()
: undefined;
const configPath =
typeof body.configPath === "string" && body.configPath.trim()
? body.configPath.trim()
: undefined;
const dataDir =
typeof body.dataDir === "string" && body.dataDir.trim()
? body.dataDir.trim()
: undefined;
const result = await mintPaperclipKeyViaCli({
agentRef,
cliBinaryPath,
companyId,
keyName,
configPath,
dataDir,
});
res.json(result);
} catch (err) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err);
}
});
};
/**
* Validate the apiUrl query param (must be present + http(s)).
* Throws an ApiError(400) on failure.
*/
function readApiUrl(raw: unknown): string {
if (typeof raw !== "string" || raw.trim() === "") {
throw badRequest("Missing required query parameter: apiUrl");
}
const apiUrl = raw.trim();
if (!apiUrl.startsWith("http://") && !apiUrl.startsWith("https://")) {
throw badRequest("Invalid apiUrl: must start with http:// or https://");
}
return apiUrl;
}