Merge upstream/master (53 commits) into local
Build: Production / build (push) Failing after 13m4s

Resolved conflicts:
- ui CompanySettingsSidebar.tsx: keep both Secrets (local) and Cloud upstream (master) nav items
- ui CompanySettingsNav.tsx + test: take master's cloud-upstream/members (drops deprecated `access` tab now consolidated into `members`)
- server plugin-worker-manager.ts: take master's 15min RPC timeout cap
- pnpm-lock.yaml: regenerated via `pnpm install` against merged package.json files

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 08:01:31 -04:00
536 changed files with 60296 additions and 2542 deletions
+40
View File
@@ -0,0 +1,40 @@
import type {
CloudUpstreamActivationEntityType,
CloudUpstreamConnectStartResponse,
CloudUpstreamConnection,
CloudUpstreamPreview,
CloudUpstreamRun,
CloudUpstreamsState,
} from "@paperclipai/shared";
import { api } from "./client";
export const cloudUpstreamsApi = {
list: (companyId: string) =>
api.get<CloudUpstreamsState>(`/cloud-upstreams?companyId=${encodeURIComponent(companyId)}`),
startConnect: (input: { companyId: string; remoteUrl: string; redirectUri: string }) =>
api.post<CloudUpstreamConnectStartResponse>("/cloud-upstreams/connect/start", input),
finishConnect: (input: { pendingConnectionId: string; code: string; state: string }) =>
api.post<CloudUpstreamConnection>("/cloud-upstreams/connect/finish", input),
preview: (connectionId: string, input: { companyId: string }) =>
api.post<CloudUpstreamPreview>(`/cloud-upstreams/${encodeURIComponent(connectionId)}/push-runs/preview`, input),
createRun: (connectionId: string, input: { companyId: string; retryOfRunId?: string | null }) =>
api.post<CloudUpstreamRun>(`/cloud-upstreams/${encodeURIComponent(connectionId)}/push-runs`, input ?? {}),
getRun: (connectionId: string, runId: string, companyId: string) =>
api.get<CloudUpstreamRun>(
`/cloud-upstreams/${encodeURIComponent(connectionId)}/push-runs/${encodeURIComponent(runId)}?companyId=${encodeURIComponent(companyId)}`,
),
cancelRun: (connectionId: string, runId: string, input: { companyId: string }) =>
api.post<CloudUpstreamRun>(
`/cloud-upstreams/${encodeURIComponent(connectionId)}/push-runs/${encodeURIComponent(runId)}/cancel`,
input,
),
activateEntities: (
connectionId: string,
runId: string,
input: { companyId: string; entityType: CloudUpstreamActivationEntityType },
) =>
api.post<CloudUpstreamRun>(
`/cloud-upstreams/${encodeURIComponent(connectionId)}/push-runs/${encodeURIComponent(runId)}/activation`,
input,
),
};
+25
View File
@@ -0,0 +1,25 @@
import type { Company } from "@paperclipai/shared";
import { companiesApi } from "./companies";
import { ApiError } from "./client";
import { queryKeys } from "../lib/queryKeys";
export type CompanyListResult = { companies: Company[]; unauthorized: boolean };
// Single source of truth for the `["companies"]` query. Both CompanyProvider and
// the invite landing page read this cache entry, so they must agree on the shape —
// returning a bare `Company[]` from one and this wrapped object from the other
// silently corrupts the shared cache and crashes whichever reads the other's shape.
export const companiesListQueryOptions = {
queryKey: queryKeys.companies.all,
queryFn: async (): Promise<CompanyListResult> => {
try {
return { companies: await companiesApi.list(), unauthorized: false };
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
return { companies: [], unauthorized: true };
}
throw err;
}
},
retry: false,
} as const;
+1
View File
@@ -26,4 +26,5 @@ describe("executionWorkspacesApi.listSummaries", () => {
"/companies/company-1/execution-workspaces?projectId=project-1&reuseEligible=true&summary=true",
);
});
});
+11
View File
@@ -38,4 +38,15 @@ export const healthApi = {
}
return res.json();
},
requestDevServerRestart: async (): Promise<void> => {
const res = await fetch("/api/health/dev-server/restart", {
method: "POST",
credentials: "include",
headers: { Accept: "application/json" },
});
if (!res.ok) {
const payload = await res.json().catch(() => null) as { error?: string } | null;
throw new Error(payload?.error ?? `Failed to request restart (${res.status})`);
}
},
};
+1
View File
@@ -16,5 +16,6 @@ export { heartbeatsApi } from "./heartbeats";
export { instanceSettingsApi } from "./instanceSettings";
export { sidebarBadgesApi } from "./sidebarBadges";
export { sidebarPreferencesApi } from "./sidebarPreferences";
export { resourceMembershipsApi } from "./resourceMemberships";
export { inboxDismissalsApi } from "./inboxDismissals";
export { companySkillsApi } from "./companySkills";
+12
View File
@@ -51,6 +51,18 @@ describe("issuesApi.list", () => {
);
});
it("passes issue list sort options through to the company issues endpoint", async () => {
await issuesApi.list("company-1", {
limit: 500,
sortField: "updated",
sortDir: "desc",
});
expect(mockApi.get).toHaveBeenCalledWith(
"/companies/company-1/issues?limit=500&sortField=updated&sortDir=desc",
);
});
it("posts recovery action resolution to the source issue endpoint", async () => {
await issuesApi.resolveRecoveryAction("issue-1", {
actionId: "00000000-0000-0000-0000-0000000000aa",
+9 -1
View File
@@ -60,6 +60,8 @@ export const issuesApi = {
q?: string;
limit?: number;
offset?: number;
sortField?: "updated";
sortDir?: "asc" | "desc";
},
) => {
const params = new URLSearchParams();
@@ -86,6 +88,8 @@ export const issuesApi = {
if (filters?.q) params.set("q", filters.q);
if (filters?.limit) params.set("limit", String(filters.limit));
if (filters?.offset !== undefined) params.set("offset", String(filters.offset));
if (filters?.sortField) params.set("sortField", filters.sortField);
if (filters?.sortDir) params.set("sortDir", filters.sortDir);
const qs = params.toString();
return api.get<Issue[]>(`/companies/${companyId}/issues${qs ? `?${qs}` : ""}`);
},
@@ -131,7 +135,7 @@ export const issuesApi = {
data: {
actionId?: string;
outcome: "restored" | "false_positive" | "blocked" | "cancelled";
sourceIssueStatus: "done" | "in_review" | "blocked";
sourceIssueStatus: "todo" | "done" | "in_review" | "blocked";
resolutionNote?: string | null;
},
) => api.post<ResolveRecoveryActionResponse>(`/issues/${id}/recovery-actions/resolve`, data),
@@ -259,6 +263,10 @@ export const issuesApi = {
getDocument: (id: string, key: string) => api.get<IssueDocument>(`/issues/${id}/documents/${encodeURIComponent(key)}`),
upsertDocument: (id: string, key: string, data: UpsertIssueDocument) =>
api.put<IssueDocument>(`/issues/${id}/documents/${encodeURIComponent(key)}`, data),
lockDocument: (id: string, key: string) =>
api.post<IssueDocument>(`/issues/${id}/documents/${encodeURIComponent(key)}/lock`, {}),
unlockDocument: (id: string, key: string) =>
api.post<IssueDocument>(`/issues/${id}/documents/${encodeURIComponent(key)}/unlock`, {}),
listDocumentRevisions: (id: string, key: string) =>
api.get<DocumentRevision[]>(`/issues/${id}/documents/${encodeURIComponent(key)}/revisions`),
restoreDocumentRevision: (id: string, key: string, revisionId: string) =>
+1 -1
View File
@@ -138,7 +138,7 @@ export interface AvailablePluginExample {
displayName: string;
description: string;
localPath: string;
tag: "example";
tag: "example" | "first-party";
}
export interface PluginLocalFolderProblem {
+21
View File
@@ -0,0 +1,21 @@
import type {
ResourceMemberships,
ResourceMembershipUpdateResult,
UpdateResourceMembership,
} from "@paperclipai/shared";
import { api } from "./client";
export const resourceMembershipsApi = {
listMine: (companyId: string) =>
api.get<ResourceMemberships>(`/companies/${companyId}/resource-memberships/me`),
updateProject: (companyId: string, projectId: string, data: UpdateResourceMembership) =>
api.put<ResourceMembershipUpdateResult>(
`/companies/${companyId}/resource-memberships/me/projects/${projectId}`,
data,
),
updateAgent: (companyId: string, agentId: string, data: UpdateResourceMembership) =>
api.put<ResourceMembershipUpdateResult>(
`/companies/${companyId}/resource-memberships/me/agents/${agentId}`,
data,
),
};
+19
View File
@@ -2,6 +2,7 @@ import type {
CompanySecret,
CompanySecretUsageBinding,
CompanySecretProviderConfig,
SecretProviderConfigDiscoveryPreviewResult,
RemoteSecretImportPreviewResult,
RemoteSecretImportResult,
SecretAccessEvent,
@@ -95,6 +96,14 @@ export interface RemoteImportInput {
secrets: RemoteImportSelectionInput[];
}
export interface SecretProviderConfigDiscoveryPreviewInput {
provider: SecretProvider;
config?: Record<string, unknown>;
query?: string | null;
nextToken?: string | null;
pageSize?: number;
}
export const secretsApi = {
list: (companyId: string) => api.get<CompanySecret[]>(`/companies/${companyId}/secrets`),
providers: (companyId: string) =>
@@ -103,11 +112,21 @@ export const secretsApi = {
api.get<SecretProviderHealthResponse>(`/companies/${companyId}/secret-providers/health`),
providerConfigs: (companyId: string) =>
api.get<CompanySecretProviderConfig[]>(`/companies/${companyId}/secret-provider-configs`),
providerConfigDiscoveryPreview: (
companyId: string,
data: SecretProviderConfigDiscoveryPreviewInput,
) =>
api.post<SecretProviderConfigDiscoveryPreviewResult>(
`/companies/${companyId}/secret-provider-configs/discovery/preview`,
data,
),
createProviderConfig: (companyId: string, data: CreateSecretProviderConfigInput) =>
api.post<CompanySecretProviderConfig>(`/companies/${companyId}/secret-provider-configs`, data),
updateProviderConfig: (id: string, data: UpdateSecretProviderConfigInput) =>
api.patch<CompanySecretProviderConfig>(`/secret-provider-configs/${id}`, data),
disableProviderConfig: (id: string) =>
api.patch<CompanySecretProviderConfig>(`/secret-provider-configs/${id}`, { status: "disabled" }),
removeProviderConfig: (id: string) =>
api.delete<CompanySecretProviderConfig>(`/secret-provider-configs/${id}`),
setDefaultProviderConfig: (id: string) =>
api.post<CompanySecretProviderConfig>(`/secret-provider-configs/${id}/default`, {}),