From ce293f7ac71af1e0f8cc8773c02fc07dc1247f95 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 3 Nov 2026 22:14:50 -0600 Subject: [PATCH] fix(teams): return canonical links for saved catalog resources Co-Authored-By: Paperclip --- packages/shared/src/types/teams-catalog.ts | 1 + server/src/routes/openapi.ts | 9 ++++++--- server/src/services/teams-catalog.ts | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/types/teams-catalog.ts b/packages/shared/src/types/teams-catalog.ts index a088b07eb0b..632fe75f8e5 100644 --- a/packages/shared/src/types/teams-catalog.ts +++ b/packages/shared/src/types/teams-catalog.ts @@ -182,6 +182,7 @@ export interface CatalogTeamImportPreviewResult { } export interface CatalogTeamInstallResult { + resourceLinks: { kind: "agent" | "project" | "routine"; id: string; name: string; href: string }[]; team: CatalogTeam; portabilityImport: CompanyPortabilityImportResult; skillPreparations: CatalogTeamSkillPreparation[]; diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 9e9714f3f41..f31a278d5d1 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -3103,9 +3103,12 @@ for (const route of [ path: route[1], tags: ["teams"], summary: route[2], - description: route[1].endsWith("/install") - ? "Imports team resources; HTTP success does not prove every required skill is ready. Read warnings for post-import failures and skillPreparations for required sources; a preparation plan is not proof of installation. Report unresolved requirements as partial completion and preserve existing resources. Use the actual returned portabilityImport agents and projects for company-scoped resource links. Before repeating an uncertain install, inspect installed teams and saved resource identities rather than creating another copy." - : undefined, + ...(route[1].endsWith("/install") ? { + description: "Imports team resources; HTTP success does not prove every required skill is ready. Read warnings for post-import failures and skillPreparations for required sources; a preparation plan is not proof of installation. Report unresolved requirements as partial completion and preserve existing resources. Use the actual returned portabilityImport agents and projects for company-scoped resource links. Before repeating an uncertain install, inspect installed teams and saved resource identities rather than creating another copy. resourceLinks provides canonical company-scoped hrefs for actual saved agents, projects and routines, including reused resources; share these links with the installation outcome.", + responses: { 200: r.ok(z.object({ resourceLinks: z.array(z.object({ + kind: z.enum(["agent", "project", "routine"]), id: z.string(), name: z.string(), href: z.string(), + })) }).passthrough()), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound }, + } : {}), }); } diff --git a/server/src/services/teams-catalog.ts b/server/src/services/teams-catalog.ts index 93db66319c5..7a36f5167ea 100644 --- a/server/src/services/teams-catalog.ts +++ b/server/src/services/teams-catalog.ts @@ -1,10 +1,12 @@ import { promises as fs } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { Db } from "@paperclipai/db"; +import { companies, type Db } from "@paperclipai/db"; +import { eq } from "drizzle-orm"; import type { CatalogManifest, CatalogTeam, + CatalogTeamInstallResult as SharedCatalogTeamInstallResult, CatalogTeamFileKind, CatalogTeamKind, CatalogTeamSkillRequirement, @@ -105,6 +107,7 @@ export interface CatalogTeamImportPreviewResult { } export interface CatalogTeamInstallResult { + resourceLinks: SharedCatalogTeamInstallResult["resourceLinks"]; team: CatalogTeam; portabilityImport: CompanyPortabilityImportResult; skillPreparations: CatalogTeamSkillPreparation[]; @@ -904,6 +907,8 @@ export function teamsCatalogService(db: Db) { catalogRef: string, options: CatalogTeamImportOptions = {}, ): Promise { + const [company] = await db.select({ issuePrefix: companies.issuePrefix }).from(companies).where(eq(companies.id, companyId)).limit(1); + if (!company) throw notFound("Company not found"); const prepared = await prepareCatalogTeamSource(companyId, catalogRef, options); if (prepared.errors.length > 0) { throw unprocessable(`Catalog team source preparation failed: ${prepared.errors.join("; ")}`); @@ -957,6 +962,14 @@ export function teamsCatalogService(db: Db) { return { team: prepared.team, portabilityImport: result, + resourceLinks: [ + ...result.agents.flatMap(agent => agent.id ? [{ kind: "agent" as const, id: agent.id, name: agent.name, + href: `/${encodeURIComponent(company.issuePrefix)}/agents/${encodeURIComponent(agent.id)}` }] : []), + ...result.projects.flatMap(project => project.id ? [{ kind: "project" as const, id: project.id, name: project.name, + href: `/${encodeURIComponent(company.issuePrefix)}/projects/${encodeURIComponent(project.id)}` }] : []), + ...(result.routines ?? []).flatMap(routine => routine.id ? [{ kind: "routine" as const, id: routine.id, name: routine.title, + href: `/${encodeURIComponent(company.issuePrefix)}/routines/${encodeURIComponent(routine.id)}` }] : []), + ], skillPreparations: prepared.skillPreparations, warnings: result.warnings, };