From 06f5632d1a7b620a53036ee3f569a2e27f98e0cb Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 23 Mar 2026 13:21:10 -0500 Subject: [PATCH] Polish import adapter defaults Co-Authored-By: Paperclip --- cli/src/__tests__/company.test.ts | 96 +++++++++++++++++++++++++++++- cli/src/commands/client/company.ts | 72 ++++++++++++++++++---- 2 files changed, 154 insertions(+), 14 deletions(-) diff --git a/cli/src/__tests__/company.test.ts b/cli/src/__tests__/company.test.ts index 0958b690..75174dfa 100644 --- a/cli/src/__tests__/company.test.ts +++ b/cli/src/__tests__/company.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import type { CompanyPortabilityPreviewResult } from "@paperclipai/shared"; import { + buildDefaultImportAdapterOverrides, buildDefaultImportSelectionState, buildImportSelectionCatalog, buildSelectedFilesFromImportSelection, @@ -217,6 +218,7 @@ describe("renderCompanyImportPreview", () => { const rendered = renderCompanyImportPreview(preview, { sourceLabel: "GitHub: https://github.com/paperclipai/companies/demo", targetLabel: "Imported Co (company-123)", + infoMessages: ["Using claude-local adapter"], }); expect(rendered).toContain("Include"); @@ -226,6 +228,7 @@ describe("renderCompanyImportPreview", () => { expect(rendered).toContain("1 task total"); expect(rendered).toContain("skills: 1 skill packaged"); expect(rendered).toContain("+1 more"); + expect(rendered).toContain("Using claude-local adapter"); expect(rendered).toContain("Warnings"); expect(rendered).toContain("Errors"); }); @@ -248,12 +251,16 @@ describe("renderCompanyImportResult", () => { envInputs: [], warnings: ["Review API keys"], }, - { targetLabel: "Imported Co (company-123)" }, + { + targetLabel: "Imported Co (company-123)", + infoMessages: ["Using claude-local adapter"], + }, ); expect(rendered).toContain("Company"); expect(rendered).toContain("3 agents total (1 created, 1 updated, 1 skipped)"); expect(rendered).toContain("Agent results"); + expect(rendered).toContain("Using claude-local adapter"); expect(rendered).toContain("Review API keys"); }); }); @@ -421,3 +428,90 @@ describe("import selection catalog", () => { expect(selectedFiles).not.toContain("projects/alpha/issues/kickoff/details.md"); }); }); + +describe("default adapter overrides", () => { + it("maps process-only imported agents to claude_local", () => { + const preview: CompanyPortabilityPreviewResult = { + include: { + company: false, + agents: true, + projects: false, + issues: false, + skills: false, + }, + targetCompanyId: null, + targetCompanyName: null, + collisionStrategy: "rename", + selectedAgentSlugs: ["legacy-agent", "explicit-agent"], + plan: { + companyAction: "none", + agentPlans: [], + projectPlans: [], + issuePlans: [], + }, + manifest: { + schemaVersion: 1, + generatedAt: "2026-03-23T18:20:00.000Z", + source: null, + includes: { + company: false, + agents: true, + projects: false, + issues: false, + skills: false, + }, + company: null, + agents: [ + { + slug: "legacy-agent", + name: "Legacy Agent", + path: "agents/legacy-agent/AGENT.md", + skills: [], + role: "agent", + title: null, + icon: null, + capabilities: null, + reportsToSlug: null, + adapterType: "process", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + budgetMonthlyCents: 0, + metadata: null, + }, + { + slug: "explicit-agent", + name: "Explicit Agent", + path: "agents/explicit-agent/AGENT.md", + skills: [], + role: "agent", + title: null, + icon: null, + capabilities: null, + reportsToSlug: null, + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + budgetMonthlyCents: 0, + metadata: null, + }, + ], + skills: [], + projects: [], + issues: [], + envInputs: [], + }, + files: {}, + envInputs: [], + warnings: [], + errors: [], + }; + + expect(buildDefaultImportAdapterOverrides(preview)).toEqual({ + "legacy-agent": { + adapterType: "claude_local", + }, + }); + }); +}); diff --git a/cli/src/commands/client/company.ts b/cli/src/commands/client/company.ts index 52a06e78..242dc70a 100644 --- a/cli/src/commands/client/company.ts +++ b/cli/src/commands/client/company.ts @@ -50,6 +50,7 @@ interface CompanyImportOptions extends BaseClientOptions { agents?: string; collision?: CompanyCollisionMode; ref?: string; + paperclipUrl?: string; yes?: boolean; dryRun?: boolean; } @@ -346,6 +347,37 @@ export function buildSelectedFilesFromImportSelection( return Array.from(selected).sort((left, right) => left.localeCompare(right)); } +export function buildDefaultImportAdapterOverrides( + preview: Pick, +): Record | undefined { + const selectedAgentSlugs = new Set(preview.selectedAgentSlugs); + const overrides = Object.fromEntries( + preview.manifest.agents + .filter((agent) => selectedAgentSlugs.size === 0 || selectedAgentSlugs.has(agent.slug)) + .filter((agent) => agent.adapterType === "process") + .map((agent) => [ + agent.slug, + { + // TODO: replace this temporary claude_local fallback with adapter selection in the import TUI. + adapterType: "claude_local", + }, + ]), + ); + return Object.keys(overrides).length > 0 ? overrides : undefined; +} + +function buildDefaultImportAdapterMessages( + overrides: Record | undefined, +): string[] { + if (!overrides) return []; + const adapterTypes = Array.from(new Set(Object.values(overrides).map((override) => override.adapterType))) + .map((adapterType) => adapterType.replace(/_/g, "-")); + const agentCount = Object.keys(overrides).length; + return [ + `Using ${adapterTypes.join(", ")} adapter${adapterTypes.length === 1 ? "" : "s"} for ${agentCount} imported ${pluralize(agentCount, "agent")} without an explicit adapter.`, + ]; +} + async function promptForImportSelection(preview: CompanyPortabilityPreviewResult): Promise { const catalog = buildImportSelectionCatalog(preview); const state = buildDefaultImportSelectionState(catalog); @@ -419,7 +451,7 @@ async function promptForImportSelection(preview: CompanyPortabilityPreviewResult } const selection = await p.multiselect({ - message: `${getGroupLabel(group)} to import. Press enter to go back.`, + message: `${getGroupLabel(group)} to import. Space toggles, enter returns to the main menu.`, options: groupItems.map((item) => ({ value: item.key, label: item.label, @@ -544,6 +576,7 @@ export function renderCompanyImportPreview( meta: { sourceLabel: string; targetLabel: string; + infoMessages?: string[]; }, ): string { const lines: string[] = [ @@ -603,6 +636,7 @@ export function renderCompanyImportPreview( })), ); + appendMessageBlock(lines, pc.cyan("Info"), meta.infoMessages ?? []); appendMessageBlock(lines, pc.yellow("Warnings"), preview.warnings); appendMessageBlock(lines, pc.red("Errors"), preview.errors); @@ -611,7 +645,7 @@ export function renderCompanyImportPreview( export function renderCompanyImportResult( result: CompanyPortabilityImportResult, - meta: { targetLabel: string }, + meta: { targetLabel: string; infoMessages?: string[] }, ): string { const lines: string[] = [ `${pc.bold("Target")} ${meta.targetLabel}`, @@ -637,6 +671,7 @@ export function renderCompanyImportResult( ); } + appendMessageBlock(lines, pc.cyan("Info"), meta.infoMessages ?? []); appendMessageBlock(lines, pc.yellow("Warnings"), result.warnings); return lines.join("\n"); @@ -1059,10 +1094,14 @@ export function registerCompanyCommands(program: Command): void { .option("--agents ", "Comma-separated agent slugs to import, or all", "all") .option("--collision ", "Collision strategy: rename | skip | replace", "rename") .option("--ref ", "Git ref to use for GitHub imports (branch, tag, or commit)") + .option("--paperclip-url ", "Alias for --api-base on this command") .option("--yes", "Accept the default import selection without opening the TUI", false) .option("--dry-run", "Run preview only without applying", false) .action(async (fromPathOrUrl: string, opts: CompanyImportOptions) => { try { + if (!opts.apiBase?.trim() && opts.paperclipUrl?.trim()) { + opts.apiBase = opts.paperclipUrl.trim(); + } const ctx = resolveCommandContext(opts); const interactiveView = isInteractiveTerminal() && !ctx.json; const from = fromPathOrUrl.trim(); @@ -1149,7 +1188,7 @@ export function registerCompanyCommands(program: Command): void { selectedFiles = await promptForImportSelection(initialPreview); } - const payload = { + const previewPayload = { source: sourcePayload, include, target: targetPayload, @@ -1157,17 +1196,14 @@ export function registerCompanyCommands(program: Command): void { collisionStrategy: collision, selectedFiles, }; - const importApiPath = resolveCompanyImportApiPath({ - dryRun: Boolean(opts.dryRun), - targetMode: targetPayload.mode, - companyId: targetPayload.mode === "existing_company" ? targetPayload.companyId : null, - }); + const preview = await ctx.api.post(previewApiPath, previewPayload); + if (!preview) { + throw new Error("Import preview returned no data."); + } + const adapterOverrides = buildDefaultImportAdapterOverrides(preview); + const adapterMessages = buildDefaultImportAdapterMessages(adapterOverrides); if (opts.dryRun) { - const preview = await ctx.api.post(importApiPath, payload); - if (!preview) { - throw new Error("Import preview returned no data."); - } if (ctx.json) { printOutput(preview, { json: true }); } else { @@ -1176,6 +1212,7 @@ export function registerCompanyCommands(program: Command): void { renderCompanyImportPreview(preview, { sourceLabel, targetLabel: formatTargetLabel(targetPayload, preview), + infoMessages: adapterMessages, }), { interactive: interactiveView }, ); @@ -1183,7 +1220,15 @@ export function registerCompanyCommands(program: Command): void { return; } - const imported = await ctx.api.post(importApiPath, payload); + const importApiPath = resolveCompanyImportApiPath({ + dryRun: false, + targetMode: targetPayload.mode, + companyId: targetPayload.mode === "existing_company" ? targetPayload.companyId : null, + }); + const imported = await ctx.api.post(importApiPath, { + ...previewPayload, + adapterOverrides, + }); if (!imported) { throw new Error("Import request returned no data."); } @@ -1194,6 +1239,7 @@ export function registerCompanyCommands(program: Command): void { "Import Result", renderCompanyImportResult(imported, { targetLabel, + infoMessages: adapterMessages, }), { interactive: interactiveView }, );