feat(paperclip-runtime): route Paperclip calls through paperclipai CLI in Local CLI mode
The Paperclip runtime card's Local CLI tab previously only derived an apiUrl
from the local config and then made HTTP calls itself, so the Test action and
the company/agent pickers ignored the user's onboarded CLI auth context.
Add CLI-backed variants for every Paperclip call that has a `paperclipai`
counterpart, and route through them when transport=cli — both from the
settings card and from the runtime adapter's prompt path.
- New plugin functions spawning `paperclipai … --json`:
* probePaperclipViaCli, listCompaniesViaCli, listCompanyAgentsViaCli
(settings card test + pickers)
* createIssueViaCli, getIssueViaCli, agentsMeViaCli (runtime hot path)
- New dashboard routes: /providers/paperclip/cli-status, /cli-companies,
/cli-agents (read-only façades over the plugin's CLI helpers)
- PaperclipRuntimeCard: branches on transport=cli to use the cli-* fetchers
- PaperclipRuntimeAdapter: stores transport on the session and routes
createIssue/getIssue + identity derivation through CLI variants in CLI mode;
raises a clear error when agentId is unset in CLI mode (paperclipai has no
/agents/me equivalent)
- getIssueComments / wakeAgent / getRunEvents stay on HTTP (no matching
paperclipai subcommands) and continue to use the apiKey discovered from
the local paperclipai config, so CLI mode still works end-to-end
- Tests: 9 new paperclip-client tests covering each CLI variant + 5 new
adapter tests for the Local-CLI transport branch (64/64 plugin tests pass)
- Update routes.test for the existing BUNDLED_PLUGIN_RUNTIMES fallback so
the bundled hermes/openclaw/paperclip entries are expected alongside
installed plugins
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,14 @@ import {
|
||||
discoverPaperclipCli,
|
||||
listHermesProviderProfiles,
|
||||
listPaperclipCompanies,
|
||||
listPaperclipCompaniesViaCliFacade,
|
||||
listPaperclipCompanyAgents,
|
||||
listPaperclipCompanyAgentsViaCliFacade,
|
||||
mintPaperclipKeyViaCli,
|
||||
probeHermesProvider,
|
||||
probeOpenClawProvider,
|
||||
probePaperclipProvider,
|
||||
probePaperclipViaCliFacade,
|
||||
} from "../runtime-provider-probes.js";
|
||||
import type { ApiRouteRegistrar } from "./types.js";
|
||||
|
||||
@@ -168,6 +171,98 @@ export const registerRuntimeProviderRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /providers/paperclip/cli-status
|
||||
*
|
||||
* Query: cliBinaryPath?, cliConfigPath?
|
||||
* Probes Paperclip by spawning the local `paperclipai` CLI (`company list`),
|
||||
* so the connection test in the dashboard's Local-CLI tab exercises exactly
|
||||
* the same auth path as runtime calls. Never throws — failures are reported
|
||||
* inside the connection object so the card can render `available: false`.
|
||||
*/
|
||||
router.get("/providers/paperclip/cli-status", async (req, res) => {
|
||||
try {
|
||||
const cliBinaryPath =
|
||||
typeof req.query.cliBinaryPath === "string"
|
||||
? req.query.cliBinaryPath
|
||||
: undefined;
|
||||
const cliConfigPath =
|
||||
typeof req.query.cliConfigPath === "string"
|
||||
? req.query.cliConfigPath
|
||||
: undefined;
|
||||
const connection = await probePaperclipViaCliFacade({
|
||||
cliBinaryPath,
|
||||
cliConfigPath,
|
||||
});
|
||||
res.json({ connection, ready: connection.available });
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err;
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /providers/paperclip/cli-companies
|
||||
*
|
||||
* Query: cliBinaryPath?, cliConfigPath?
|
||||
* Lists companies via `paperclipai company list --json`. Empty array on failure.
|
||||
*/
|
||||
router.get("/providers/paperclip/cli-companies", async (req, res) => {
|
||||
try {
|
||||
const cliBinaryPath =
|
||||
typeof req.query.cliBinaryPath === "string"
|
||||
? req.query.cliBinaryPath
|
||||
: undefined;
|
||||
const cliConfigPath =
|
||||
typeof req.query.cliConfigPath === "string"
|
||||
? req.query.cliConfigPath
|
||||
: undefined;
|
||||
const companies = await listPaperclipCompaniesViaCliFacade({
|
||||
cliBinaryPath,
|
||||
cliConfigPath,
|
||||
});
|
||||
res.json({ companies });
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err;
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /providers/paperclip/cli-agents
|
||||
*
|
||||
* Query: companyId (required), cliBinaryPath?, cliConfigPath?
|
||||
* Lists agents via `paperclipai agent list -C <id> --json`. Empty array on failure.
|
||||
*/
|
||||
router.get("/providers/paperclip/cli-agents", async (req, res) => {
|
||||
try {
|
||||
const companyId =
|
||||
typeof req.query.companyId === "string"
|
||||
? req.query.companyId.trim()
|
||||
: "";
|
||||
if (!companyId) {
|
||||
throw badRequest("Missing required query parameter: companyId");
|
||||
}
|
||||
const cliBinaryPath =
|
||||
typeof req.query.cliBinaryPath === "string"
|
||||
? req.query.cliBinaryPath
|
||||
: undefined;
|
||||
const cliConfigPath =
|
||||
typeof req.query.cliConfigPath === "string"
|
||||
? req.query.cliConfigPath
|
||||
: undefined;
|
||||
const agents = await listPaperclipCompanyAgentsViaCliFacade({
|
||||
cliBinaryPath,
|
||||
cliConfigPath,
|
||||
companyId,
|
||||
});
|
||||
res.json({ agents });
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err;
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /providers/paperclip/cli-discovery
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user