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:
@@ -1,10 +1,13 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
agentsMe,
|
||||
agentsMeViaCli,
|
||||
createIssue,
|
||||
createIssueViaCli,
|
||||
discoverPaperclipCliConfig,
|
||||
getIssue,
|
||||
getIssueComments,
|
||||
getIssueViaCli,
|
||||
getRunEvents,
|
||||
resolvePaperclipConfig,
|
||||
wakeAgent,
|
||||
@@ -125,16 +128,32 @@ export class PaperclipRuntimeAdapter implements AgentRuntime {
|
||||
let agentId = this.config.agentId;
|
||||
let companyId = this.config.companyId;
|
||||
|
||||
// Auto-derive agentId/companyId from /agents/me when missing.
|
||||
// Auto-derive agentId/companyId when missing. In CLI mode we go through
|
||||
// `paperclipai agent get <id>` (requires a known agentId); in API mode we
|
||||
// call /agents/me which derives identity from the bearer key.
|
||||
if (!agentId || !companyId) {
|
||||
try {
|
||||
const me = await agentsMe(effectiveApiUrl, effectiveApiKey);
|
||||
agentId = agentId ?? me.agentId;
|
||||
companyId = companyId ?? me.companyId;
|
||||
if (this.config.transport === "cli") {
|
||||
if (!agentId) {
|
||||
throw new Error(
|
||||
"agentId is required in Local CLI mode (paperclipai has no `agents/me` equivalent — pick an agent in settings)",
|
||||
);
|
||||
}
|
||||
const me = await agentsMeViaCli({
|
||||
agentId,
|
||||
cliBinaryPath: this.config.cliBinaryPath,
|
||||
cliConfigPath: this.config.cliConfigPath,
|
||||
});
|
||||
companyId = companyId ?? me.companyId;
|
||||
} else {
|
||||
const me = await agentsMe(effectiveApiUrl, effectiveApiKey);
|
||||
agentId = agentId ?? me.agentId;
|
||||
companyId = companyId ?? me.companyId;
|
||||
}
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(
|
||||
`Paperclip runtime could not derive agentId/companyId from /agents/me. Configure them explicitly or check the API key. Underlying error: ${reason}`,
|
||||
`Paperclip runtime could not derive agentId/companyId. Configure them explicitly or check the API key / CLI auth. Underlying error: ${reason}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -158,6 +177,9 @@ export class PaperclipRuntimeAdapter implements AgentRuntime {
|
||||
projectId: this.config.projectId,
|
||||
goalId: this.config.goalId,
|
||||
issueId: undefined,
|
||||
transport: this.config.transport ?? "api",
|
||||
cliBinaryPath: this.config.cliBinaryPath,
|
||||
cliConfigPath: this.config.cliConfigPath,
|
||||
turnIndex: 0,
|
||||
runTimeoutMs: this.config.runTimeoutMs ?? 600_000,
|
||||
pollIntervalMs: this.config.pollIntervalMs ?? 500,
|
||||
@@ -245,11 +267,20 @@ export class PaperclipRuntimeAdapter implements AgentRuntime {
|
||||
let finalText = stream.text;
|
||||
if (issueId) {
|
||||
try {
|
||||
const issue = await getIssue(session.apiUrl, session.apiKey, issueId);
|
||||
const issue =
|
||||
session.transport === "cli"
|
||||
? await getIssueViaCli({
|
||||
issueId,
|
||||
cliBinaryPath: session.cliBinaryPath,
|
||||
cliConfigPath: session.cliConfigPath,
|
||||
})
|
||||
: await getIssue(session.apiUrl, session.apiKey, issueId);
|
||||
issueStatus = asString(issue.status) ?? undefined;
|
||||
|
||||
// Comment fallback: if no streaming text was captured, use the latest
|
||||
// non-system comment as the visible answer.
|
||||
// non-system comment as the visible answer. Note: paperclipai has no
|
||||
// `issue comments list` command, so this stays on HTTP — in CLI mode
|
||||
// it relies on the apiKey discovered from the local paperclipai config.
|
||||
if (!finalText) {
|
||||
const comments = await getIssueComments(session.apiUrl, session.apiKey, issueId);
|
||||
const latest = pickLatestVisibleComment(comments);
|
||||
@@ -294,7 +325,7 @@ export class PaperclipRuntimeAdapter implements AgentRuntime {
|
||||
session: PaperclipSession,
|
||||
prompt: string,
|
||||
): Promise<string> {
|
||||
const created = await createIssue(session.apiUrl, session.apiKey, session.companyId, {
|
||||
const body = {
|
||||
title: deriveIssueTitle(prompt),
|
||||
description: buildIssueDescription(session, prompt),
|
||||
status: "todo",
|
||||
@@ -302,7 +333,16 @@ export class PaperclipRuntimeAdapter implements AgentRuntime {
|
||||
...(session.parentIssueId ? { parentId: session.parentIssueId } : {}),
|
||||
...(session.projectId ? { projectId: session.projectId } : {}),
|
||||
...(session.goalId ? { goalId: session.goalId } : {}),
|
||||
});
|
||||
};
|
||||
const created =
|
||||
session.transport === "cli"
|
||||
? await createIssueViaCli({
|
||||
companyId: session.companyId,
|
||||
body,
|
||||
cliBinaryPath: session.cliBinaryPath,
|
||||
cliConfigPath: session.cliConfigPath,
|
||||
})
|
||||
: await createIssue(session.apiUrl, session.apiKey, session.companyId, body);
|
||||
return pickIssueId(created);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user