Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/bdunk881_paperclipai.json
T

1 line
1.5 MiB
Plaintext

{"repo": "bdunk881/paperclipai", "files": {"dashboard/src/api/client.ts": {"b17b9bde50cfb2f72fcb76b2866f2f794c08e1cb": {"t": 1785746584.0885725, "n": 56623, "h": "5692d8ddefeee923", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/**\n * Available models per provider — mirrors PROVIDER_MODELS from the backend\n * (`src/engine/llmProviders/types.ts`). Refreshed 2026-05-23 to the latest\n * production-stable model IDs for each provider's public API. First entry\n * of each list is the default the connect form selects.\n */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\n \"gpt-5.5\",\n \"gpt-5.5-pro\",\n \"gpt-5.4\",\n \"gpt-5.4-mini\",\n \"gpt-5.4-nano\",\n \"o3\",\n \"o4-mini\",\n \"gpt-5\",\n \"gpt-5-mini\",\n \"gpt-5-nano\",\n ],\n anthropic: [\n \"claude-opus-4-8\",\n \"claude-opus-4-7\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5\",\n \"claude-opus-4-6\",\n ],\n gemini: [\n \"gemini-3.5-flash\",\n \"gemini-3.1-pro-preview\",\n \"gemini-3.1-flash-lite\",\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n ],\n mistral: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n \"codestral-latest\",\n ],\n groq: [\n \"meta-llama/llama-4-maverick-17b-128e-instruct\",\n \"meta-llama/llama-4-scout-17b-16e-instruct\",\n \"llama-3.3-70b-versatile\",\n \"llama-3.1-8b-instant\",\n \"openai/gpt-oss-120b\",\n \"openai/gpt-oss-20b\",\n ],\n fireworks: [\n \"accounts/fireworks/models/llama4-maverick-instruct-basic\",\n \"accounts/fireworks/models/llama4-scout-instruct-basic\",\n \"accounts/fireworks/models/llama-v3p3-70b-instruct\",\n \"accounts/fireworks/models/deepseek-r1\",\n \"accounts/fireworks/models/deepseek-v3\",\n ],\n together: [\n \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\",\n \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n \"deepseek-ai/DeepSeek-V3\",\n \"deepseek-ai/DeepSeek-R1\",\n ],\n ollama: [\"llama3.3:70b\", \"llama3.2\", \"deepseek-r1:14b\", \"qwen2.5:14b\"],\n localai: [\n \"llama-3.3-70b-instruct\",\n \"llama-3.1-8b-instruct\",\n \"llama-3.2-3b-instruct\",\n ],\n cohere: [\n \"command-a-plus-05-2026\",\n \"command-a-reasoning-08-2025\",\n \"command-a-03-2025\",\n \"command-r-plus-08-2024\",\n ],\n perplexity: [\n \"sonar-pro\",\n \"sonar-reasoning-pro\",\n \"sonar-deep-research\",\n \"sonar\",\n ],\n xai: [\"grok-4.3\", \"grok-4.3-latest\", \"grok-build-0.1\"],\n deepseek: [\n \"deepseek-v4-pro\",\n \"deepseek-v4-flash\",\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n ],\n bedrock: [\n \"amazon.nova-premier-v1:0\",\n \"amazon.nova-pro-v1:0\",\n \"amazon.nova-lite-v1:0\",\n \"amazon.nova-micro-v1:0\",\n ],\n \"vertex-ai\": [\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n \"gemini-3.5-flash\",\n \"claude-sonnet-4-6\",\n ],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id — update label/model on an existing config. */\nexport async function updateLLMConfig(\n id: string,\n patch: Partial<Pick<LLMConfig, \"label\" | \"model\">>,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"PATCH\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(patch),\n }\n );\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n throw new Error(body?.error ?? `Failed to update LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\n// ---------------------------------------------------------------------------\n// HEL-206: encrypted workspace environment variables (Pro surface).\n// ---------------------------------------------------------------------------\n\nexport type EnvVarScopeKind = \"mission\" | \"team\" | \"agent\";\nexport type EnvVarPermission = \"allow\" | \"ask\" | \"deny\";\n\nexport interface EnvVarGrant {\n scope_kind: EnvVarScopeKind;\n scope_id: string;\n permission: EnvVarPermission;\n}\n\nexport interface EnvVarRecord {\n id: string;\n name: string;\n keyVersion: number;\n createdAt: string;\n lastUsedAt: string | null;\n createdByUserId: string | null;\n grants: EnvVarGrant[];\n}\n\nexport interface CreateEnvVarInput {\n name: string;\n value: string;\n grants?: EnvVarGrant[];\n}\n\nconst ENV_VARS_PATH = \"/env-vars\";\n\nexport async function listEnvVars(accessToken?: string): Promise<EnvVarRecord[]> {\n const res = await trackedFetch(`${BASE}${ENV_VARS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch env vars: ${res.status}`);\n const data = (await res.json()) as { envVars: EnvVarRecord[] };\n return data.envVars;\n}\n\nexport async function createEnvVar(\n input: CreateEnvVarInput,\n accessToken?: string,\n): Promise<EnvVarRecord> {\n const res = await trackedFetch(`${BASE}${ENV_VARS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create env var: ${res.status}`));\n }\n return (await res.json()) as EnvVarRecord;\n}\n\nexport async function rotateEnvVar(\n id: string,\n value: string,\n accessToken?: string,\n): Promise<{ id: string; keyVersion: number }> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ value }),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate env var: ${res.status}`));\n }\n return (await res.json()) as { id: string; keyVersion: number };\n}\n\nexport async function updateEnvVarGrants(\n id: string,\n grants: EnvVarGrant[],\n accessToken?: string,\n): Promise<{ id: string; grants: EnvVarGrant[] }> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}/grants`,\n {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ grants }),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update grants: ${res.status}`));\n }\n return (await res.json()) as { id: string; grants: EnvVarGrant[] };\n}\n\nexport async function deleteEnvVar(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to delete env var: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n /** True for the built-in library templates; false for user-imported/created. */\n seeded?: boolean;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\n// HEL-142: ControlPlaneAgentScheduleType + ControlPlaneAgentSchedule retired\n// 2026-05-21. agents.schedule dropped (migration 057); scheduling lives in\n// routines (routines.schedule_cron + BullMQ).\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowId?: string;\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string, accessToken?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n // GET /api/templates requires requireAuth — trackedFetch does NOT inject the\n // Bearer token (it only adds the workspace header), so pass it explicitly or\n // the real (non-mock) backend returns 401 \"Missing or malformed Authorization\n // header\" and the calling page errors out (HEL-823).\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** DELETE /api/templates/:id */\nexport async function deleteTemplate(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok && res.status !== 204) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to delete template: ${res.status}`);\n }\n}\n\n/** POST /api/templates/import — accepts a portable workflow bundle JSON. */\nexport async function importTemplate(\n bundle: unknown,\n accessToken?: string,\n): Promise<{ imported: boolean; template: WorkflowTemplate }> {\n const res = await trackedFetch(`${BASE}/templates/import`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(bundle),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to import template: ${res.status}`);\n }\n return res.json() as Promise<{ imported: boolean; template: WorkflowTemplate }>;\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/**\n * POST /api/runs/:runId/replay-from-step (HEL-176)\n *\n * Resumes execution from `stepIndex`, cloning the outputs of steps\n * 0..stepIndex-1 so already-successful work is not redone. The original\n * run stays intact; the new run is returned with status `pending`.\n */\nexport async function replayRunFromStep(\n runId: string,\n stepIndex: number,\n accessToken?: string,\n): Promise<WorkflowRun> {\n const res = await trackedFetch(\n `${BASE}/runs/${encodeURIComponent(runId)}/replay-from-step`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ stepIndex }),\n },\n );\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to replay run from step: ${res.status}`);\n }\n const body = (await res.json()) as { run: WorkflowRun };\n return body.run;\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "ad58dce3d9c90cbc30627ce146b730749ddc3d3e": {"t": 1785765956.3652935, "n": 56623, "h": "5692d8ddefeee923", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/**\n * Available models per provider — mirrors PROVIDER_MODELS from the backend\n * (`src/engine/llmProviders/types.ts`). Refreshed 2026-05-23 to the latest\n * production-stable model IDs for each provider's public API. First entry\n * of each list is the default the connect form selects.\n */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\n \"gpt-5.5\",\n \"gpt-5.5-pro\",\n \"gpt-5.4\",\n \"gpt-5.4-mini\",\n \"gpt-5.4-nano\",\n \"o3\",\n \"o4-mini\",\n \"gpt-5\",\n \"gpt-5-mini\",\n \"gpt-5-nano\",\n ],\n anthropic: [\n \"claude-opus-4-8\",\n \"claude-opus-4-7\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5\",\n \"claude-opus-4-6\",\n ],\n gemini: [\n \"gemini-3.5-flash\",\n \"gemini-3.1-pro-preview\",\n \"gemini-3.1-flash-lite\",\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n ],\n mistral: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n \"codestral-latest\",\n ],\n groq: [\n \"meta-llama/llama-4-maverick-17b-128e-instruct\",\n \"meta-llama/llama-4-scout-17b-16e-instruct\",\n \"llama-3.3-70b-versatile\",\n \"llama-3.1-8b-instant\",\n \"openai/gpt-oss-120b\",\n \"openai/gpt-oss-20b\",\n ],\n fireworks: [\n \"accounts/fireworks/models/llama4-maverick-instruct-basic\",\n \"accounts/fireworks/models/llama4-scout-instruct-basic\",\n \"accounts/fireworks/models/llama-v3p3-70b-instruct\",\n \"accounts/fireworks/models/deepseek-r1\",\n \"accounts/fireworks/models/deepseek-v3\",\n ],\n together: [\n \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\",\n \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n \"deepseek-ai/DeepSeek-V3\",\n \"deepseek-ai/DeepSeek-R1\",\n ],\n ollama: [\"llama3.3:70b\", \"llama3.2\", \"deepseek-r1:14b\", \"qwen2.5:14b\"],\n localai: [\n \"llama-3.3-70b-instruct\",\n \"llama-3.1-8b-instruct\",\n \"llama-3.2-3b-instruct\",\n ],\n cohere: [\n \"command-a-plus-05-2026\",\n \"command-a-reasoning-08-2025\",\n \"command-a-03-2025\",\n \"command-r-plus-08-2024\",\n ],\n perplexity: [\n \"sonar-pro\",\n \"sonar-reasoning-pro\",\n \"sonar-deep-research\",\n \"sonar\",\n ],\n xai: [\"grok-4.3\", \"grok-4.3-latest\", \"grok-build-0.1\"],\n deepseek: [\n \"deepseek-v4-pro\",\n \"deepseek-v4-flash\",\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n ],\n bedrock: [\n \"amazon.nova-premier-v1:0\",\n \"amazon.nova-pro-v1:0\",\n \"amazon.nova-lite-v1:0\",\n \"amazon.nova-micro-v1:0\",\n ],\n \"vertex-ai\": [\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n \"gemini-3.5-flash\",\n \"claude-sonnet-4-6\",\n ],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id — update label/model on an existing config. */\nexport async function updateLLMConfig(\n id: string,\n patch: Partial<Pick<LLMConfig, \"label\" | \"model\">>,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"PATCH\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(patch),\n }\n );\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n throw new Error(body?.error ?? `Failed to update LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\n// ---------------------------------------------------------------------------\n// HEL-206: encrypted workspace environment variables (Pro surface).\n// ---------------------------------------------------------------------------\n\nexport type EnvVarScopeKind = \"mission\" | \"team\" | \"agent\";\nexport type EnvVarPermission = \"allow\" | \"ask\" | \"deny\";\n\nexport interface EnvVarGrant {\n scope_kind: EnvVarScopeKind;\n scope_id: string;\n permission: EnvVarPermission;\n}\n\nexport interface EnvVarRecord {\n id: string;\n name: string;\n keyVersion: number;\n createdAt: string;\n lastUsedAt: string | null;\n createdByUserId: string | null;\n grants: EnvVarGrant[];\n}\n\nexport interface CreateEnvVarInput {\n name: string;\n value: string;\n grants?: EnvVarGrant[];\n}\n\nconst ENV_VARS_PATH = \"/env-vars\";\n\nexport async function listEnvVars(accessToken?: string): Promise<EnvVarRecord[]> {\n const res = await trackedFetch(`${BASE}${ENV_VARS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch env vars: ${res.status}`);\n const data = (await res.json()) as { envVars: EnvVarRecord[] };\n return data.envVars;\n}\n\nexport async function createEnvVar(\n input: CreateEnvVarInput,\n accessToken?: string,\n): Promise<EnvVarRecord> {\n const res = await trackedFetch(`${BASE}${ENV_VARS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create env var: ${res.status}`));\n }\n return (await res.json()) as EnvVarRecord;\n}\n\nexport async function rotateEnvVar(\n id: string,\n value: string,\n accessToken?: string,\n): Promise<{ id: string; keyVersion: number }> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ value }),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate env var: ${res.status}`));\n }\n return (await res.json()) as { id: string; keyVersion: number };\n}\n\nexport async function updateEnvVarGrants(\n id: string,\n grants: EnvVarGrant[],\n accessToken?: string,\n): Promise<{ id: string; grants: EnvVarGrant[] }> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}/grants`,\n {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ grants }),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update grants: ${res.status}`));\n }\n return (await res.json()) as { id: string; grants: EnvVarGrant[] };\n}\n\nexport async function deleteEnvVar(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to delete env var: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n /** True for the built-in library templates; false for user-imported/created. */\n seeded?: boolean;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\n// HEL-142: ControlPlaneAgentScheduleType + ControlPlaneAgentSchedule retired\n// 2026-05-21. agents.schedule dropped (migration 057); scheduling lives in\n// routines (routines.schedule_cron + BullMQ).\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowId?: string;\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string, accessToken?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n // GET /api/templates requires requireAuth — trackedFetch does NOT inject the\n // Bearer token (it only adds the workspace header), so pass it explicitly or\n // the real (non-mock) backend returns 401 \"Missing or malformed Authorization\n // header\" and the calling page errors out (HEL-823).\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** DELETE /api/templates/:id */\nexport async function deleteTemplate(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok && res.status !== 204) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to delete template: ${res.status}`);\n }\n}\n\n/** POST /api/templates/import — accepts a portable workflow bundle JSON. */\nexport async function importTemplate(\n bundle: unknown,\n accessToken?: string,\n): Promise<{ imported: boolean; template: WorkflowTemplate }> {\n const res = await trackedFetch(`${BASE}/templates/import`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(bundle),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to import template: ${res.status}`);\n }\n return res.json() as Promise<{ imported: boolean; template: WorkflowTemplate }>;\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/**\n * POST /api/runs/:runId/replay-from-step (HEL-176)\n *\n * Resumes execution from `stepIndex`, cloning the outputs of steps\n * 0..stepIndex-1 so already-successful work is not redone. The original\n * run stays intact; the new run is returned with status `pending`.\n */\nexport async function replayRunFromStep(\n runId: string,\n stepIndex: number,\n accessToken?: string,\n): Promise<WorkflowRun> {\n const res = await trackedFetch(\n `${BASE}/runs/${encodeURIComponent(runId)}/replay-from-step`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ stepIndex }),\n },\n );\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to replay run from step: ${res.status}`);\n }\n const body = (await res.json()) as { run: WorkflowRun };\n return body.run;\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "5f1036ba32289dd484c38072fa5b652ef94fa844": {"t": 1785765958.2977808, "n": 56244, "h": "b84ed4ee6b4979b1", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/**\n * Available models per provider — mirrors PROVIDER_MODELS from the backend\n * (`src/engine/llmProviders/types.ts`). Refreshed 2026-05-23 to the latest\n * production-stable model IDs for each provider's public API. First entry\n * of each list is the default the connect form selects.\n */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\n \"gpt-5.5\",\n \"gpt-5.5-pro\",\n \"gpt-5.4\",\n \"gpt-5.4-mini\",\n \"gpt-5.4-nano\",\n \"o3\",\n \"o4-mini\",\n \"gpt-5\",\n \"gpt-5-mini\",\n \"gpt-5-nano\",\n ],\n anthropic: [\n \"claude-opus-4-8\",\n \"claude-opus-4-7\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5\",\n \"claude-opus-4-6\",\n ],\n gemini: [\n \"gemini-3.5-flash\",\n \"gemini-3.1-pro-preview\",\n \"gemini-3.1-flash-lite\",\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n ],\n mistral: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n \"codestral-latest\",\n ],\n groq: [\n \"meta-llama/llama-4-maverick-17b-128e-instruct\",\n \"meta-llama/llama-4-scout-17b-16e-instruct\",\n \"llama-3.3-70b-versatile\",\n \"llama-3.1-8b-instant\",\n \"openai/gpt-oss-120b\",\n \"openai/gpt-oss-20b\",\n ],\n fireworks: [\n \"accounts/fireworks/models/llama4-maverick-instruct-basic\",\n \"accounts/fireworks/models/llama4-scout-instruct-basic\",\n \"accounts/fireworks/models/llama-v3p3-70b-instruct\",\n \"accounts/fireworks/models/deepseek-r1\",\n \"accounts/fireworks/models/deepseek-v3\",\n ],\n together: [\n \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\",\n \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n \"deepseek-ai/DeepSeek-V3\",\n \"deepseek-ai/DeepSeek-R1\",\n ],\n ollama: [\"llama3.3:70b\", \"llama3.2\", \"deepseek-r1:14b\", \"qwen2.5:14b\"],\n localai: [\n \"llama-3.3-70b-instruct\",\n \"llama-3.1-8b-instruct\",\n \"llama-3.2-3b-instruct\",\n ],\n cohere: [\n \"command-a-plus-05-2026\",\n \"command-a-reasoning-08-2025\",\n \"command-a-03-2025\",\n \"command-r-plus-08-2024\",\n ],\n perplexity: [\n \"sonar-pro\",\n \"sonar-reasoning-pro\",\n \"sonar-deep-research\",\n \"sonar\",\n ],\n xai: [\"grok-4.3\", \"grok-4.3-latest\", \"grok-build-0.1\"],\n deepseek: [\n \"deepseek-v4-pro\",\n \"deepseek-v4-flash\",\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n ],\n bedrock: [\n \"amazon.nova-premier-v1:0\",\n \"amazon.nova-pro-v1:0\",\n \"amazon.nova-lite-v1:0\",\n \"amazon.nova-micro-v1:0\",\n ],\n \"vertex-ai\": [\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n \"gemini-3.5-flash\",\n \"claude-sonnet-4-6\",\n ],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id — update label/model on an existing config. */\nexport async function updateLLMConfig(\n id: string,\n patch: Partial<Pick<LLMConfig, \"label\" | \"model\">>,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"PATCH\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(patch),\n }\n );\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n throw new Error(body?.error ?? `Failed to update LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\n// ---------------------------------------------------------------------------\n// HEL-206: encrypted workspace environment variables (Pro surface).\n// ---------------------------------------------------------------------------\n\nexport type EnvVarScopeKind = \"mission\" | \"team\" | \"agent\";\nexport type EnvVarPermission = \"allow\" | \"ask\" | \"deny\";\n\nexport interface EnvVarGrant {\n scope_kind: EnvVarScopeKind;\n scope_id: string;\n permission: EnvVarPermission;\n}\n\nexport interface EnvVarRecord {\n id: string;\n name: string;\n keyVersion: number;\n createdAt: string;\n lastUsedAt: string | null;\n createdByUserId: string | null;\n grants: EnvVarGrant[];\n}\n\nexport interface CreateEnvVarInput {\n name: string;\n value: string;\n grants?: EnvVarGrant[];\n}\n\nconst ENV_VARS_PATH = \"/env-vars\";\n\nexport async function listEnvVars(accessToken?: string): Promise<EnvVarRecord[]> {\n const res = await trackedFetch(`${BASE}${ENV_VARS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch env vars: ${res.status}`);\n const data = (await res.json()) as { envVars: EnvVarRecord[] };\n return data.envVars;\n}\n\nexport async function createEnvVar(\n input: CreateEnvVarInput,\n accessToken?: string,\n): Promise<EnvVarRecord> {\n const res = await trackedFetch(`${BASE}${ENV_VARS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create env var: ${res.status}`));\n }\n return (await res.json()) as EnvVarRecord;\n}\n\nexport async function rotateEnvVar(\n id: string,\n value: string,\n accessToken?: string,\n): Promise<{ id: string; keyVersion: number }> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ value }),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate env var: ${res.status}`));\n }\n return (await res.json()) as { id: string; keyVersion: number };\n}\n\nexport async function updateEnvVarGrants(\n id: string,\n grants: EnvVarGrant[],\n accessToken?: string,\n): Promise<{ id: string; grants: EnvVarGrant[] }> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}/grants`,\n {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ grants }),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update grants: ${res.status}`));\n }\n return (await res.json()) as { id: string; grants: EnvVarGrant[] };\n}\n\nexport async function deleteEnvVar(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to delete env var: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n /** True for the built-in library templates; false for user-imported/created. */\n seeded?: boolean;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\n// HEL-142: ControlPlaneAgentScheduleType + ControlPlaneAgentSchedule retired\n// 2026-05-21. agents.schedule dropped (migration 057); scheduling lives in\n// routines (routines.schedule_cron + BullMQ).\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowId?: string;\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** DELETE /api/templates/:id */\nexport async function deleteTemplate(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok && res.status !== 204) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to delete template: ${res.status}`);\n }\n}\n\n/** POST /api/templates/import — accepts a portable workflow bundle JSON. */\nexport async function importTemplate(\n bundle: unknown,\n accessToken?: string,\n): Promise<{ imported: boolean; template: WorkflowTemplate }> {\n const res = await trackedFetch(`${BASE}/templates/import`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(bundle),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to import template: ${res.status}`);\n }\n return res.json() as Promise<{ imported: boolean; template: WorkflowTemplate }>;\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/**\n * POST /api/runs/:runId/replay-from-step (HEL-176)\n *\n * Resumes execution from `stepIndex`, cloning the outputs of steps\n * 0..stepIndex-1 so already-successful work is not redone. The original\n * run stays intact; the new run is returned with status `pending`.\n */\nexport async function replayRunFromStep(\n runId: string,\n stepIndex: number,\n accessToken?: string,\n): Promise<WorkflowRun> {\n const res = await trackedFetch(\n `${BASE}/runs/${encodeURIComponent(runId)}/replay-from-step`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ stepIndex }),\n },\n );\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to replay run from step: ${res.status}`);\n }\n const body = (await res.json()) as { run: WorkflowRun };\n return body.run;\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "da9ec76721d05d4b30fe094375562fe4a58b2d07": {"t": 1785765960.9909832, "n": 56221, "h": "a97331191db26dbf", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/**\n * Available models per provider — mirrors PROVIDER_MODELS from the backend\n * (`src/engine/llmProviders/types.ts`). Refreshed 2026-05-23 to the latest\n * production-stable model IDs for each provider's public API. First entry\n * of each list is the default the connect form selects.\n */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\n \"gpt-5.5\",\n \"gpt-5.5-pro\",\n \"gpt-5.4\",\n \"gpt-5.4-mini\",\n \"gpt-5.4-nano\",\n \"o3\",\n \"o4-mini\",\n \"gpt-5\",\n \"gpt-5-mini\",\n \"gpt-5-nano\",\n ],\n anthropic: [\n \"claude-opus-4-7\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5\",\n \"claude-opus-4-6\",\n ],\n gemini: [\n \"gemini-3.5-flash\",\n \"gemini-3.1-pro-preview\",\n \"gemini-3.1-flash-lite\",\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n ],\n mistral: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n \"codestral-latest\",\n ],\n groq: [\n \"meta-llama/llama-4-maverick-17b-128e-instruct\",\n \"meta-llama/llama-4-scout-17b-16e-instruct\",\n \"llama-3.3-70b-versatile\",\n \"llama-3.1-8b-instant\",\n \"openai/gpt-oss-120b\",\n \"openai/gpt-oss-20b\",\n ],\n fireworks: [\n \"accounts/fireworks/models/llama4-maverick-instruct-basic\",\n \"accounts/fireworks/models/llama4-scout-instruct-basic\",\n \"accounts/fireworks/models/llama-v3p3-70b-instruct\",\n \"accounts/fireworks/models/deepseek-r1\",\n \"accounts/fireworks/models/deepseek-v3\",\n ],\n together: [\n \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\",\n \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n \"deepseek-ai/DeepSeek-V3\",\n \"deepseek-ai/DeepSeek-R1\",\n ],\n ollama: [\"llama3.3:70b\", \"llama3.2\", \"deepseek-r1:14b\", \"qwen2.5:14b\"],\n localai: [\n \"llama-3.3-70b-instruct\",\n \"llama-3.1-8b-instruct\",\n \"llama-3.2-3b-instruct\",\n ],\n cohere: [\n \"command-a-plus-05-2026\",\n \"command-a-reasoning-08-2025\",\n \"command-a-03-2025\",\n \"command-r-plus-08-2024\",\n ],\n perplexity: [\n \"sonar-pro\",\n \"sonar-reasoning-pro\",\n \"sonar-deep-research\",\n \"sonar\",\n ],\n xai: [\"grok-4.3\", \"grok-4.3-latest\", \"grok-build-0.1\"],\n deepseek: [\n \"deepseek-v4-pro\",\n \"deepseek-v4-flash\",\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n ],\n bedrock: [\n \"amazon.nova-premier-v1:0\",\n \"amazon.nova-pro-v1:0\",\n \"amazon.nova-lite-v1:0\",\n \"amazon.nova-micro-v1:0\",\n ],\n \"vertex-ai\": [\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n \"gemini-3.5-flash\",\n \"claude-sonnet-4-6\",\n ],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id — update label/model on an existing config. */\nexport async function updateLLMConfig(\n id: string,\n patch: Partial<Pick<LLMConfig, \"label\" | \"model\">>,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"PATCH\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(patch),\n }\n );\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n throw new Error(body?.error ?? `Failed to update LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\n// ---------------------------------------------------------------------------\n// HEL-206: encrypted workspace environment variables (Pro surface).\n// ---------------------------------------------------------------------------\n\nexport type EnvVarScopeKind = \"mission\" | \"team\" | \"agent\";\nexport type EnvVarPermission = \"allow\" | \"ask\" | \"deny\";\n\nexport interface EnvVarGrant {\n scope_kind: EnvVarScopeKind;\n scope_id: string;\n permission: EnvVarPermission;\n}\n\nexport interface EnvVarRecord {\n id: string;\n name: string;\n keyVersion: number;\n createdAt: string;\n lastUsedAt: string | null;\n createdByUserId: string | null;\n grants: EnvVarGrant[];\n}\n\nexport interface CreateEnvVarInput {\n name: string;\n value: string;\n grants?: EnvVarGrant[];\n}\n\nconst ENV_VARS_PATH = \"/env-vars\";\n\nexport async function listEnvVars(accessToken?: string): Promise<EnvVarRecord[]> {\n const res = await trackedFetch(`${BASE}${ENV_VARS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch env vars: ${res.status}`);\n const data = (await res.json()) as { envVars: EnvVarRecord[] };\n return data.envVars;\n}\n\nexport async function createEnvVar(\n input: CreateEnvVarInput,\n accessToken?: string,\n): Promise<EnvVarRecord> {\n const res = await trackedFetch(`${BASE}${ENV_VARS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create env var: ${res.status}`));\n }\n return (await res.json()) as EnvVarRecord;\n}\n\nexport async function rotateEnvVar(\n id: string,\n value: string,\n accessToken?: string,\n): Promise<{ id: string; keyVersion: number }> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ value }),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate env var: ${res.status}`));\n }\n return (await res.json()) as { id: string; keyVersion: number };\n}\n\nexport async function updateEnvVarGrants(\n id: string,\n grants: EnvVarGrant[],\n accessToken?: string,\n): Promise<{ id: string; grants: EnvVarGrant[] }> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}/grants`,\n {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ grants }),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update grants: ${res.status}`));\n }\n return (await res.json()) as { id: string; grants: EnvVarGrant[] };\n}\n\nexport async function deleteEnvVar(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to delete env var: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n /** True for the built-in library templates; false for user-imported/created. */\n seeded?: boolean;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\n// HEL-142: ControlPlaneAgentScheduleType + ControlPlaneAgentSchedule retired\n// 2026-05-21. agents.schedule dropped (migration 057); scheduling lives in\n// routines (routines.schedule_cron + BullMQ).\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowId?: string;\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** DELETE /api/templates/:id */\nexport async function deleteTemplate(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok && res.status !== 204) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to delete template: ${res.status}`);\n }\n}\n\n/** POST /api/templates/import — accepts a portable workflow bundle JSON. */\nexport async function importTemplate(\n bundle: unknown,\n accessToken?: string,\n): Promise<{ imported: boolean; template: WorkflowTemplate }> {\n const res = await trackedFetch(`${BASE}/templates/import`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(bundle),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to import template: ${res.status}`);\n }\n return res.json() as Promise<{ imported: boolean; template: WorkflowTemplate }>;\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/**\n * POST /api/runs/:runId/replay-from-step (HEL-176)\n *\n * Resumes execution from `stepIndex`, cloning the outputs of steps\n * 0..stepIndex-1 so already-successful work is not redone. The original\n * run stays intact; the new run is returned with status `pending`.\n */\nexport async function replayRunFromStep(\n runId: string,\n stepIndex: number,\n accessToken?: string,\n): Promise<WorkflowRun> {\n const res = await trackedFetch(\n `${BASE}/runs/${encodeURIComponent(runId)}/replay-from-step`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ stepIndex }),\n },\n );\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to replay run from step: ${res.status}`);\n }\n const body = (await res.json()) as { run: WorkflowRun };\n return body.run;\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "1e7bf63a1e829817db5bd6147c0c6e76298d41c8": {"t": 1785765962.5779161, "n": 55039, "h": "e402e3e6cf495b8e", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/**\n * Available models per provider — mirrors PROVIDER_MODELS from the backend\n * (`src/engine/llmProviders/types.ts`). Refreshed 2026-05-23 to the latest\n * production-stable model IDs for each provider's public API. First entry\n * of each list is the default the connect form selects.\n */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\n \"gpt-5.5\",\n \"gpt-5.5-pro\",\n \"gpt-5.4\",\n \"gpt-5.4-mini\",\n \"gpt-5.4-nano\",\n \"o3\",\n \"o4-mini\",\n \"gpt-5\",\n \"gpt-5-mini\",\n \"gpt-5-nano\",\n ],\n anthropic: [\n \"claude-opus-4-7\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5\",\n \"claude-opus-4-6\",\n ],\n gemini: [\n \"gemini-3.5-flash\",\n \"gemini-3.1-pro-preview\",\n \"gemini-3.1-flash-lite\",\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n ],\n mistral: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n \"codestral-latest\",\n ],\n groq: [\n \"meta-llama/llama-4-maverick-17b-128e-instruct\",\n \"meta-llama/llama-4-scout-17b-16e-instruct\",\n \"llama-3.3-70b-versatile\",\n \"llama-3.1-8b-instant\",\n \"openai/gpt-oss-120b\",\n \"openai/gpt-oss-20b\",\n ],\n fireworks: [\n \"accounts/fireworks/models/llama4-maverick-instruct-basic\",\n \"accounts/fireworks/models/llama4-scout-instruct-basic\",\n \"accounts/fireworks/models/llama-v3p3-70b-instruct\",\n \"accounts/fireworks/models/deepseek-r1\",\n \"accounts/fireworks/models/deepseek-v3\",\n ],\n together: [\n \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\",\n \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n \"deepseek-ai/DeepSeek-V3\",\n \"deepseek-ai/DeepSeek-R1\",\n ],\n ollama: [\"llama3.3:70b\", \"llama3.2\", \"deepseek-r1:14b\", \"qwen2.5:14b\"],\n localai: [\n \"llama-3.3-70b-instruct\",\n \"llama-3.1-8b-instruct\",\n \"llama-3.2-3b-instruct\",\n ],\n cohere: [\n \"command-a-plus-05-2026\",\n \"command-a-reasoning-08-2025\",\n \"command-a-03-2025\",\n \"command-r-plus-08-2024\",\n ],\n perplexity: [\n \"sonar-pro\",\n \"sonar-reasoning-pro\",\n \"sonar-deep-research\",\n \"sonar\",\n ],\n xai: [\"grok-4.3\", \"grok-4.3-latest\", \"grok-build-0.1\"],\n deepseek: [\n \"deepseek-v4-pro\",\n \"deepseek-v4-flash\",\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n ],\n bedrock: [\n \"amazon.nova-premier-v1:0\",\n \"amazon.nova-pro-v1:0\",\n \"amazon.nova-lite-v1:0\",\n \"amazon.nova-micro-v1:0\",\n ],\n \"vertex-ai\": [\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n \"gemini-3.5-flash\",\n \"claude-sonnet-4-6\",\n ],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id — update label/model on an existing config. */\nexport async function updateLLMConfig(\n id: string,\n patch: Partial<Pick<LLMConfig, \"label\" | \"model\">>,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"PATCH\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(patch),\n }\n );\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n throw new Error(body?.error ?? `Failed to update LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\n// ---------------------------------------------------------------------------\n// HEL-206: encrypted workspace environment variables (Pro surface).\n// ---------------------------------------------------------------------------\n\nexport type EnvVarScopeKind = \"mission\" | \"team\" | \"agent\";\nexport type EnvVarPermission = \"allow\" | \"ask\" | \"deny\";\n\nexport interface EnvVarGrant {\n scope_kind: EnvVarScopeKind;\n scope_id: string;\n permission: EnvVarPermission;\n}\n\nexport interface EnvVarRecord {\n id: string;\n name: string;\n keyVersion: number;\n createdAt: string;\n lastUsedAt: string | null;\n createdByUserId: string | null;\n grants: EnvVarGrant[];\n}\n\nexport interface CreateEnvVarInput {\n name: string;\n value: string;\n grants?: EnvVarGrant[];\n}\n\nconst ENV_VARS_PATH = \"/env-vars\";\n\nexport async function listEnvVars(accessToken?: string): Promise<EnvVarRecord[]> {\n const res = await trackedFetch(`${BASE}${ENV_VARS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch env vars: ${res.status}`);\n const data = (await res.json()) as { envVars: EnvVarRecord[] };\n return data.envVars;\n}\n\nexport async function createEnvVar(\n input: CreateEnvVarInput,\n accessToken?: string,\n): Promise<EnvVarRecord> {\n const res = await trackedFetch(`${BASE}${ENV_VARS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create env var: ${res.status}`));\n }\n return (await res.json()) as EnvVarRecord;\n}\n\nexport async function rotateEnvVar(\n id: string,\n value: string,\n accessToken?: string,\n): Promise<{ id: string; keyVersion: number }> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ value }),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate env var: ${res.status}`));\n }\n return (await res.json()) as { id: string; keyVersion: number };\n}\n\nexport async function updateEnvVarGrants(\n id: string,\n grants: EnvVarGrant[],\n accessToken?: string,\n): Promise<{ id: string; grants: EnvVarGrant[] }> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}/grants`,\n {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ grants }),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update grants: ${res.status}`));\n }\n return (await res.json()) as { id: string; grants: EnvVarGrant[] };\n}\n\nexport async function deleteEnvVar(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${ENV_VARS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to delete env var: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\n// HEL-142: ControlPlaneAgentScheduleType + ControlPlaneAgentSchedule retired\n// 2026-05-21. agents.schedule dropped (migration 057); scheduling lives in\n// routines (routines.schedule_cron + BullMQ).\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowId?: string;\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/**\n * POST /api/runs/:runId/replay-from-step (HEL-176)\n *\n * Resumes execution from `stepIndex`, cloning the outputs of steps\n * 0..stepIndex-1 so already-successful work is not redone. The original\n * run stays intact; the new run is returned with status `pending`.\n */\nexport async function replayRunFromStep(\n runId: string,\n stepIndex: number,\n accessToken?: string,\n): Promise<WorkflowRun> {\n const res = await trackedFetch(\n `${BASE}/runs/${encodeURIComponent(runId)}/replay-from-step`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ stepIndex }),\n },\n );\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to replay run from step: ${res.status}`);\n }\n const body = (await res.json()) as { run: WorkflowRun };\n return body.run;\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "c8a9588eff12969ae500072dec436a3a5f23aa56": {"t": 1785765964.2224464, "n": 51962, "h": "f5f99bee5bc55dfd", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/**\n * Available models per provider — mirrors PROVIDER_MODELS from the backend\n * (`src/engine/llmProviders/types.ts`). Refreshed 2026-05-23 to the latest\n * production-stable model IDs for each provider's public API. First entry\n * of each list is the default the connect form selects.\n */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\n \"gpt-5.5\",\n \"gpt-5.5-pro\",\n \"gpt-5.4\",\n \"gpt-5.4-mini\",\n \"gpt-5.4-nano\",\n \"o3\",\n \"o4-mini\",\n \"gpt-5\",\n \"gpt-5-mini\",\n \"gpt-5-nano\",\n ],\n anthropic: [\n \"claude-opus-4-7\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5\",\n \"claude-opus-4-6\",\n ],\n gemini: [\n \"gemini-3.5-flash\",\n \"gemini-3.1-pro-preview\",\n \"gemini-3.1-flash-lite\",\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n ],\n mistral: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n \"codestral-latest\",\n ],\n groq: [\n \"meta-llama/llama-4-maverick-17b-128e-instruct\",\n \"meta-llama/llama-4-scout-17b-16e-instruct\",\n \"llama-3.3-70b-versatile\",\n \"llama-3.1-8b-instant\",\n \"openai/gpt-oss-120b\",\n \"openai/gpt-oss-20b\",\n ],\n fireworks: [\n \"accounts/fireworks/models/llama4-maverick-instruct-basic\",\n \"accounts/fireworks/models/llama4-scout-instruct-basic\",\n \"accounts/fireworks/models/llama-v3p3-70b-instruct\",\n \"accounts/fireworks/models/deepseek-r1\",\n \"accounts/fireworks/models/deepseek-v3\",\n ],\n together: [\n \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\",\n \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n \"deepseek-ai/DeepSeek-V3\",\n \"deepseek-ai/DeepSeek-R1\",\n ],\n ollama: [\"llama3.3:70b\", \"llama3.2\", \"deepseek-r1:14b\", \"qwen2.5:14b\"],\n localai: [\n \"llama-3.3-70b-instruct\",\n \"llama-3.1-8b-instruct\",\n \"llama-3.2-3b-instruct\",\n ],\n cohere: [\n \"command-a-plus-05-2026\",\n \"command-a-reasoning-08-2025\",\n \"command-a-03-2025\",\n \"command-r-plus-08-2024\",\n ],\n perplexity: [\n \"sonar-pro\",\n \"sonar-reasoning-pro\",\n \"sonar-deep-research\",\n \"sonar\",\n ],\n xai: [\"grok-4.3\", \"grok-4.3-latest\", \"grok-build-0.1\"],\n deepseek: [\n \"deepseek-v4-pro\",\n \"deepseek-v4-flash\",\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n ],\n bedrock: [\n \"amazon.nova-premier-v1:0\",\n \"amazon.nova-pro-v1:0\",\n \"amazon.nova-lite-v1:0\",\n \"amazon.nova-micro-v1:0\",\n ],\n \"vertex-ai\": [\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n \"gemini-3.5-flash\",\n \"claude-sonnet-4-6\",\n ],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id — update label/model on an existing config. */\nexport async function updateLLMConfig(\n id: string,\n patch: Partial<Pick<LLMConfig, \"label\" | \"model\">>,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"PATCH\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(patch),\n }\n );\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n throw new Error(body?.error ?? `Failed to update LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\n// HEL-142: ControlPlaneAgentScheduleType + ControlPlaneAgentSchedule retired\n// 2026-05-21. agents.schedule dropped (migration 057); scheduling lives in\n// routines (routines.schedule_cron + BullMQ).\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowId?: string;\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/**\n * POST /api/runs/:runId/replay-from-step (HEL-176)\n *\n * Resumes execution from `stepIndex`, cloning the outputs of steps\n * 0..stepIndex-1 so already-successful work is not redone. The original\n * run stays intact; the new run is returned with status `pending`.\n */\nexport async function replayRunFromStep(\n runId: string,\n stepIndex: number,\n accessToken?: string,\n): Promise<WorkflowRun> {\n const res = await trackedFetch(\n `${BASE}/runs/${encodeURIComponent(runId)}/replay-from-step`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ stepIndex }),\n },\n );\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to replay run from step: ${res.status}`);\n }\n const body = (await res.json()) as { run: WorkflowRun };\n return body.run;\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "cebad8fc92a4f2c6aedaa16a62a1151ede4d04df": {"t": 1785765967.1061246, "n": 51301, "h": "87626b341f1be1af", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/**\n * Available models per provider — mirrors PROVIDER_MODELS from the backend\n * (`src/engine/llmProviders/types.ts`). Refreshed 2026-05-23 to the latest\n * production-stable model IDs for each provider's public API. First entry\n * of each list is the default the connect form selects.\n */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\n \"gpt-5.5\",\n \"gpt-5.5-pro\",\n \"gpt-5.4\",\n \"gpt-5.4-mini\",\n \"gpt-5.4-nano\",\n \"o3\",\n \"o4-mini\",\n \"gpt-5\",\n \"gpt-5-mini\",\n \"gpt-5-nano\",\n ],\n anthropic: [\n \"claude-opus-4-7\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5\",\n \"claude-opus-4-6\",\n ],\n gemini: [\n \"gemini-3.5-flash\",\n \"gemini-3.1-pro-preview\",\n \"gemini-3.1-flash-lite\",\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n ],\n mistral: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n \"codestral-latest\",\n ],\n groq: [\n \"meta-llama/llama-4-maverick-17b-128e-instruct\",\n \"meta-llama/llama-4-scout-17b-16e-instruct\",\n \"llama-3.3-70b-versatile\",\n \"llama-3.1-8b-instant\",\n \"openai/gpt-oss-120b\",\n \"openai/gpt-oss-20b\",\n ],\n fireworks: [\n \"accounts/fireworks/models/llama4-maverick-instruct-basic\",\n \"accounts/fireworks/models/llama4-scout-instruct-basic\",\n \"accounts/fireworks/models/llama-v3p3-70b-instruct\",\n \"accounts/fireworks/models/deepseek-r1\",\n \"accounts/fireworks/models/deepseek-v3\",\n ],\n together: [\n \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\",\n \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n \"deepseek-ai/DeepSeek-V3\",\n \"deepseek-ai/DeepSeek-R1\",\n ],\n ollama: [\"llama3.3:70b\", \"llama3.2\", \"deepseek-r1:14b\", \"qwen2.5:14b\"],\n localai: [\n \"llama-3.3-70b-instruct\",\n \"llama-3.1-8b-instruct\",\n \"llama-3.2-3b-instruct\",\n ],\n cohere: [\n \"command-a-plus-05-2026\",\n \"command-a-reasoning-08-2025\",\n \"command-a-03-2025\",\n \"command-r-plus-08-2024\",\n ],\n perplexity: [\n \"sonar-pro\",\n \"sonar-reasoning-pro\",\n \"sonar-deep-research\",\n \"sonar\",\n ],\n xai: [\"grok-4.3\", \"grok-4.3-latest\", \"grok-build-0.1\"],\n deepseek: [\n \"deepseek-v4-pro\",\n \"deepseek-v4-flash\",\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n ],\n bedrock: [\n \"amazon.nova-premier-v1:0\",\n \"amazon.nova-pro-v1:0\",\n \"amazon.nova-lite-v1:0\",\n \"amazon.nova-micro-v1:0\",\n ],\n \"vertex-ai\": [\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n \"gemini-3.5-flash\",\n \"claude-sonnet-4-6\",\n ],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\n// HEL-142: ControlPlaneAgentScheduleType + ControlPlaneAgentSchedule retired\n// 2026-05-21. agents.schedule dropped (migration 057); scheduling lives in\n// routines (routines.schedule_cron + BullMQ).\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowId?: string;\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/**\n * POST /api/runs/:runId/replay-from-step (HEL-176)\n *\n * Resumes execution from `stepIndex`, cloning the outputs of steps\n * 0..stepIndex-1 so already-successful work is not redone. The original\n * run stays intact; the new run is returned with status `pending`.\n */\nexport async function replayRunFromStep(\n runId: string,\n stepIndex: number,\n accessToken?: string,\n): Promise<WorkflowRun> {\n const res = await trackedFetch(\n `${BASE}/runs/${encodeURIComponent(runId)}/replay-from-step`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ stepIndex }),\n },\n );\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to replay run from step: ${res.status}`);\n }\n const body = (await res.json()) as { run: WorkflowRun };\n return body.run;\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "ac8ed91612f9b8ba84c217c2833eaa5d7154948c": {"t": 1785765968.7837684, "n": 50305, "h": "ceb7b04fc4947e4e", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\n// HEL-142: ControlPlaneAgentScheduleType + ControlPlaneAgentSchedule retired\n// 2026-05-21. agents.schedule dropped (migration 057); scheduling lives in\n// routines (routines.schedule_cron + BullMQ).\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowId?: string;\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/**\n * POST /api/runs/:runId/replay-from-step (HEL-176)\n *\n * Resumes execution from `stepIndex`, cloning the outputs of steps\n * 0..stepIndex-1 so already-successful work is not redone. The original\n * run stays intact; the new run is returned with status `pending`.\n */\nexport async function replayRunFromStep(\n runId: string,\n stepIndex: number,\n accessToken?: string,\n): Promise<WorkflowRun> {\n const res = await trackedFetch(\n `${BASE}/runs/${encodeURIComponent(runId)}/replay-from-step`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ stepIndex }),\n },\n );\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to replay run from step: ${res.status}`);\n }\n const body = (await res.json()) as { run: WorkflowRun };\n return body.run;\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "5b89cb193cdf42da40afdbeef3cbbca5aaf95892": {"t": 1785765970.905563, "n": 50361, "h": "74806ddb881cdd95", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowId?: string;\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/**\n * POST /api/runs/:runId/replay-from-step (HEL-176)\n *\n * Resumes execution from `stepIndex`, cloning the outputs of steps\n * 0..stepIndex-1 so already-successful work is not redone. The original\n * run stays intact; the new run is returned with status `pending`.\n */\nexport async function replayRunFromStep(\n runId: string,\n stepIndex: number,\n accessToken?: string,\n): Promise<WorkflowRun> {\n const res = await trackedFetch(\n `${BASE}/runs/${encodeURIComponent(runId)}/replay-from-step`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ stepIndex }),\n },\n );\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to replay run from step: ${res.status}`);\n }\n const body = (await res.json()) as { run: WorkflowRun };\n return body.run;\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "1db35521f5f5ae9a818fdd5399e495d354ebf52d": {"t": 1785765972.5421453, "n": 50663, "h": "fcbffbb75dce9ef5", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n /** @deprecated HEL-119 — use {@link workflowId}. */\n workflowTemplateId?: string;\n /** @deprecated HEL-119 — use {@link workflowName}. */\n workflowTemplateName?: string;\n /** Canonical noun alias for {@link workflowTemplateId}. */\n workflowId?: string;\n /** Canonical noun alias for {@link workflowTemplateName}. */\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/**\n * POST /api/runs/:runId/replay-from-step (HEL-176)\n *\n * Resumes execution from `stepIndex`, cloning the outputs of steps\n * 0..stepIndex-1 so already-successful work is not redone. The original\n * run stays intact; the new run is returned with status `pending`.\n */\nexport async function replayRunFromStep(\n runId: string,\n stepIndex: number,\n accessToken?: string,\n): Promise<WorkflowRun> {\n const res = await trackedFetch(\n `${BASE}/runs/${encodeURIComponent(runId)}/replay-from-step`,\n {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ stepIndex }),\n },\n );\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to replay run from step: ${res.status}`);\n }\n const body = (await res.json()) as { run: WorkflowRun };\n return body.run;\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "9acdf110647f5b45809030e7f19f48f449dcc8bf": {"t": 1785765974.154186, "n": 49795, "h": "b7f7fd3d6ad509fa", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n /** @deprecated HEL-119 — use {@link workflowId}. */\n workflowTemplateId?: string;\n /** @deprecated HEL-119 — use {@link workflowName}. */\n workflowTemplateName?: string;\n /** Canonical noun alias for {@link workflowTemplateId}. */\n workflowId?: string;\n /** Canonical noun alias for {@link workflowTemplateName}. */\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "754ae28af89adb45c0910f9ba9ee98e45bd2d2d1": {"t": 1785765975.5845506, "n": 53346, "h": "595636f3c0b83fd7", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport interface ApiKeyRecord {\n id: string;\n workspaceId: string;\n name: string;\n maskedKey: string;\n createdByUserId: string;\n rotatedFromKeyId: string | null;\n lastUsedAt: string | null;\n revokedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface CreateApiKeyInput {\n name: string;\n}\n\nexport interface ApiKeySecretResponse {\n key: ApiKeyRecord;\n secret: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\n// ---------------------------------------------------------------------------\n// Platform API key lifecycle\n// ---------------------------------------------------------------------------\n\nconst API_KEYS_PATH = \"/api-keys\";\n\n/** GET /api/api-keys */\nexport async function listApiKeys(accessToken?: string): Promise<ApiKeyRecord[]> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch API keys: ${res.status}`);\n const data = await res.json();\n return data.keys as ApiKeyRecord[];\n}\n\n/** POST /api/api-keys */\nexport async function createApiKey(\n input: CreateApiKeyInput,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(`${BASE}${API_KEYS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** POST /api/api-keys/:id/rotate */\nexport async function rotateApiKey(\n id: string,\n accessToken?: string,\n): Promise<ApiKeySecretResponse> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}/rotate`,\n {\n method: \"POST\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to rotate API key: ${res.status}`));\n }\n return res.json() as Promise<ApiKeySecretResponse>;\n}\n\n/** DELETE /api/api-keys/:id */\nexport async function revokeApiKey(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${API_KEYS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n },\n );\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to revoke API key: ${res.status}`));\n }\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n /** @deprecated HEL-119 — use {@link workflowId}. */\n workflowTemplateId?: string;\n /** @deprecated HEL-119 — use {@link workflowName}. */\n workflowTemplateName?: string;\n /** Canonical noun alias for {@link workflowTemplateId}. */\n workflowId?: string;\n /** Canonical noun alias for {@link workflowTemplateName}. */\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst EMPTY_PROPOSAL_CONTEXT: ProposalContextResponse = {\n records: [],\n templates: [],\n history: [],\n usage: { used: 0, limit: 0 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await trackedFetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return EMPTY_PROPOSAL_CONTEXT;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? []) as ProposalCrmRecord[],\n templates: (data.templates ?? []) as ProposalTemplateOption[],\n history: (data.history ?? []) as ProposalHistoryItem[],\n usage: (data.usage ?? EMPTY_PROPOSAL_CONTEXT.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await trackedFetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n throw new Error(\"Proposal drafts are not available on this environment yet.\");\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await trackedFetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n throw new Error(\"Proposal job status is not available on this environment yet.\");\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "309320010226517748a04432ca472c6d5af63785": {"t": 1785765977.2249398, "n": 50956, "h": "6f31641fb6a2cccc", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n /** @deprecated HEL-119 — use {@link workflowId}. */\n workflowTemplateId?: string;\n /** @deprecated HEL-119 — use {@link workflowName}. */\n workflowTemplateName?: string;\n /** Canonical noun alias for {@link workflowTemplateId}. */\n workflowId?: string;\n /** Canonical noun alias for {@link workflowTemplateName}. */\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst EMPTY_PROPOSAL_CONTEXT: ProposalContextResponse = {\n records: [],\n templates: [],\n history: [],\n usage: { used: 0, limit: 0 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await trackedFetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return EMPTY_PROPOSAL_CONTEXT;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? []) as ProposalCrmRecord[],\n templates: (data.templates ?? []) as ProposalTemplateOption[],\n history: (data.history ?? []) as ProposalHistoryItem[],\n usage: (data.usage ?? EMPTY_PROPOSAL_CONTEXT.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await trackedFetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n throw new Error(\"Proposal drafts are not available on this environment yet.\");\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await trackedFetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n throw new Error(\"Proposal job status is not available on this environment yet.\");\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "0124e8ccc47183ed5ce2721df17e082fc2b2a37f": {"t": 1785765979.131761, "n": 53608, "h": "355a28298d5e5810", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n /** @deprecated HEL-119 — use {@link workflowId}. */\n workflowTemplateId?: string;\n /** @deprecated HEL-119 — use {@link workflowName}. */\n workflowTemplateName?: string;\n /** Canonical noun alias for {@link workflowTemplateId}. */\n workflowId?: string;\n /** Canonical noun alias for {@link workflowTemplateName}. */\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await trackedFetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await trackedFetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await trackedFetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n /**\n * DASH-14: real FK to the originating agent when known. Lets the\n * dashboard render the presence pill without the legacy UX-10\n * name-match. Nullable until engine + backfill catch up.\n */\n agentId?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "c848c10905100b23f3149654a5fa21c44d13eb2e": {"t": 1785765981.152926, "n": 53383, "h": "b01ae72e5302e35d", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n// HEL-117: canonical noun is `llm_credentials` (migration 025). The dashboard\n// now calls the canonical path; the legacy `/api/llm-configs` mount still\n// resolves for any older clients during the rename window.\nconst LLM_CREDENTIALS_PATH = \"/llm-credentials\";\n\n/** GET /api/llm-credentials */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM credentials: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-credentials */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}${LLM_CREDENTIALS_PATH}`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM credential: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-credentials/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}/default`,\n {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-credentials/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(\n `${BASE}${LLM_CREDENTIALS_PATH}/${encodeURIComponent(id)}`,\n {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n }\n );\n if (!res.ok) throw new Error(`Failed to delete LLM credential: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n /** @deprecated HEL-119 — use {@link workflowId}. */\n workflowTemplateId?: string;\n /** @deprecated HEL-119 — use {@link workflowName}. */\n workflowTemplateName?: string;\n /** Canonical noun alias for {@link workflowTemplateId}. */\n workflowId?: string;\n /** Canonical noun alias for {@link workflowTemplateName}. */\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await trackedFetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await trackedFetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await trackedFetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "cb10eabb5cc369262c9d76f58183026b3014625d": {"t": 1785765983.250288, "n": 53011, "h": "8e6e8122cfeba7c0", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n /** @deprecated HEL-119 — use {@link workflowId}. */\n workflowTemplateId?: string;\n /** @deprecated HEL-119 — use {@link workflowName}. */\n workflowTemplateName?: string;\n /** Canonical noun alias for {@link workflowTemplateId}. */\n workflowId?: string;\n /** Canonical noun alias for {@link workflowTemplateName}. */\n workflowName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await trackedFetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await trackedFetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await trackedFetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "77df48f5cc77794788d2fe118b8e3d945d4e1f97": {"t": 1785765984.97562, "n": 52725, "h": "91e67f363aef2c69", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { EntitlementError } from \"./entitlementError\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n const entitlementErr = EntitlementError.fromBody(body);\n if (entitlementErr) throw entitlementErr;\n throw new Error(body?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await trackedFetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await trackedFetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await trackedFetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "c308c133f5f4096a62f288abdeba848f40158292": {"t": 1785765987.0835853, "n": 52562, "h": "11e761a7580866b3", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await trackedFetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await trackedFetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await trackedFetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "da717039be638da696e2b0d4df5cde7450e74e10": {"t": 1785765988.8682601, "n": 52637, "h": "e6f063707929a0c8", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport { trackedFetch } from \"./trackedFetch\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"azure-openai\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n \"azure-openai\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4.1\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await trackedFetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await trackedFetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await trackedFetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await trackedFetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput, accessToken?: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await trackedFetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await trackedFetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await trackedFetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await trackedFetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await trackedFetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await trackedFetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await trackedFetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await trackedFetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await trackedFetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await trackedFetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await trackedFetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await trackedFetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await trackedFetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await trackedFetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await trackedFetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await trackedFetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await trackedFetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await trackedFetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await trackedFetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await trackedFetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "7e7871150e2f650785cfc3a8c7436fb55f7dcf91": {"t": 1785765990.595828, "n": 52240, "h": "c266a1a7aa0c5f4e", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"azure-openai\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n \"azure-openai\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4.1\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await fetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await fetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId?: string;\n template?: WorkflowTemplate;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await fetch(`${BASE}/connectors/health`);\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await fetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await fetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await fetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "dd4f601fd35582e8e5447d0bc827821f7655c358": {"t": 1785765992.7515252, "n": 52208, "h": "40f270c93d0e3ea5", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"azure-openai\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n \"azure-openai\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4.1\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source?: \"mock\" | \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source?: \"mock\" | \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await fetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await fetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await fetch(`${BASE}/connectors/health`);\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n const payload = (await res.json()) as {\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n };\n\n if (payload.summary?.source === \"mock\") {\n throw new Error(\"Connector health is still serving mock telemetry.\");\n }\n\n return payload;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await fetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await fetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await fetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "ae19d68aa6481cf037889ff1208557923d366542": {"t": 1785765995.4083726, "n": 52040, "h": "dab0f17ffa57d465", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"mistral\"\n | \"azure-openai\"\n | \"groq\"\n | \"fireworks\"\n | \"together\"\n | \"ollama\"\n | \"localai\"\n | \"cohere\"\n | \"perplexity\"\n | \"xai\"\n | \"deepseek\"\n | \"bedrock\"\n | \"vertex-ai\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n \"azure-openai\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4.1\"],\n groq: [\"llama-3.3-70b-versatile\", \"llama-3.1-8b-instant\", \"mixtral-8x7b-32768\"],\n fireworks: [\n \"accounts/fireworks/models/llama-v3p1-8b-instruct\",\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n \"accounts/fireworks/models/mixtral-8x7b-instruct\",\n ],\n together: [\n \"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo\",\n \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n ],\n ollama: [\"llama3.1:8b\", \"llama3.1:70b\", \"mixtral:8x7b\"],\n localai: [\"llama-3.1-8b-instruct\", \"llama-3.1-70b-instruct\", \"mixtral-8x7b-instruct\"],\n cohere: [\"command-r\", \"command-r-plus\", \"command-a-03-2025\"],\n perplexity: [\"sonar\", \"sonar-pro\", \"llama-3.1-sonar-large-128k-online\"],\n xai: [\"grok-2-1212\", \"grok-2-vision-1212\", \"grok-beta\"],\n deepseek: [\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-coder\"],\n bedrock: [\n \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n \"meta.llama3-1-70b-instruct-v1:0\",\n \"amazon.nova-pro-v1:0\",\n ],\n \"vertex-ai\": [\"gemini-2.0-flash-001\", \"gemini-1.5-pro-002\", \"claude-3-5-sonnet-v2@20241022\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n apiKeyMasked: string; // e.g. \"****x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source: \"mock\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source: \"mock\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await fetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await fetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await fetch(`${BASE}/connectors/health`);\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n return res.json() as Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n }>;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await fetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await fetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await fetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "deab5c65a3a36bcbd0568d5334958d38a5af1203": {"t": 1785765997.2517943, "n": 50720, "h": "beb6f05dee537324", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName = \"openai\" | \"anthropic\" | \"gemini\" | \"mistral\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n maskedApiKey: string; // e.g. \"sk-...x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failed\"\n | \"provider_error\"\n | \"disabled\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source: \"mock\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source: \"mock\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failed\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"provider_error\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failed: connectors.filter((c) => c.state === \"auth_failed\").length,\n provider_error: connectors.filter((c) => c.state === \"provider_error\").length,\n disabled: connectors.filter((c) => c.state === \"disabled\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await fetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await fetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await fetch(`${BASE}/connectors/health`);\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n return res.json() as Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n }>;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await fetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await fetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await fetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "8572dc292f3eb13eca6868bfca6853694ba8b511": {"t": 1785765998.713062, "n": 50596, "h": "d8f62a21b65bc7a2", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName = \"openai\" | \"anthropic\" | \"gemini\" | \"mistral\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n maskedApiKey: string; // e.g. \"sk-...x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failure\"\n | \"down\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source: \"mock\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source: \"mock\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failure\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"down\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failure: connectors.filter((c) => c.state === \"auth_failure\").length,\n down: connectors.filter((c) => c.state === \"down\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await fetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await fetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await fetch(`${BASE}/connectors/health`);\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n return res.json() as Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n }>;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await fetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await fetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await fetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "d439ccc05d022da16c7924e444fc1763ba7b24f1": {"t": 1785766000.4289448, "n": 44192, "h": "7e986243bc83a56a", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName = \"openai\" | \"anthropic\" | \"gemini\" | \"mistral\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n maskedApiKey: string; // e.g. \"sk-...x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failure\"\n | \"down\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source: \"mock\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source: \"mock\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failure\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"down\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failure: connectors.filter((c) => c.state === \"auth_failure\").length,\n down: connectors.filter((c) => c.state === \"down\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await fetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await fetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await fetch(`${BASE}/connectors/health`);\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n return res.json() as Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n }>;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// HITL API — checkpoints, comments, Ask the CEO, and schedule controls\n// ---------------------------------------------------------------------------\n\nexport type HitlNotificationChannel = \"inbox\" | \"email\" | \"agent_wake\";\nexport type HitlRecipientType = \"agent\" | \"user\";\nexport type HitlCheckpointTriggerType =\n | \"end_of_week_review\"\n | \"milestone_gate\"\n | \"kpi_deviation\"\n | \"manual\";\nexport type HitlCheckpointStatus = \"pending\" | \"acknowledged\" | \"resolved\" | \"dismissed\";\nexport type HitlCommentStatus = \"open\" | \"resolved\";\n\nexport interface HitlKpiThreshold {\n metricKey: string;\n comparator: \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"percent_drop\";\n threshold: number;\n window: \"hour\" | \"day\" | \"week\";\n}\n\nexport interface HitlCheckpointSchedule {\n id: string;\n companyId: string;\n userId: string;\n enabled: boolean;\n timezone: string;\n notificationChannels: HitlNotificationChannel[];\n weeklyReview: {\n enabled: boolean;\n dayOfWeek: number;\n hour: number;\n minute: number;\n };\n milestoneGate: {\n enabled: boolean;\n blockingStatuses: string[];\n };\n kpiDeviation: {\n enabled: boolean;\n thresholds: HitlKpiThreshold[];\n };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HitlCheckpoint {\n id: string;\n companyId: string;\n userId: string;\n triggerType: HitlCheckpointTriggerType;\n source: \"system\" | \"manual\";\n title: string;\n description?: string;\n status: HitlCheckpointStatus;\n dueAt?: string;\n artifactRefs: string[];\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlArtifactComment {\n id: string;\n companyId: string;\n userId: string;\n artifact: {\n kind: \"ticket\" | \"approval\" | \"run\" | \"document\" | \"workflow_step\" | \"other\";\n id: string;\n title?: string;\n path?: string;\n version?: string;\n };\n anchor?: {\n quote?: string;\n lineStart?: number;\n lineEnd?: number;\n startOffset?: number;\n endOffset?: number;\n fieldKey?: string;\n };\n body: string;\n status: HitlCommentStatus;\n routing: {\n recipientType: HitlRecipientType;\n recipientId: string;\n responsibleAgentId?: string;\n reason?: string;\n };\n createdAt: string;\n updatedAt: string;\n notificationIds: string[];\n}\n\nexport interface HitlAskCeoRequest {\n id: string;\n companyId: string;\n userId: string;\n question: string;\n context?: {\n artifactRef?: string;\n taskId?: string;\n checkpointId?: string;\n };\n status: \"answered\";\n response: {\n summary: string;\n recommendedActions: string[];\n citedEntities: Array<{\n type: \"team\" | \"task\" | \"checkpoint\" | \"artifact_comment\";\n id: string;\n label: string;\n }>;\n companyStateVersion: string;\n };\n createdAt: string;\n}\n\nexport interface HitlNotification {\n id: string;\n companyId: string;\n userId: string;\n kind: \"checkpoint\" | \"artifact_comment\" | \"ask_ceo_response\";\n channel: HitlNotificationChannel;\n recipientType: HitlRecipientType;\n recipientId: string;\n status: \"pending\" | \"sent\" | \"failed\";\n payload: Record<string, unknown>;\n createdAt: string;\n}\n\nexport interface HitlCompanyState {\n companyId: string;\n version: string;\n summary: {\n companyId: string;\n version: string;\n team: {\n id: string;\n name: string;\n status: string;\n budgetMonthlyUsd: number;\n agentCount: number;\n activeExecutionCount: number;\n openTaskCount: number;\n } | null;\n hitl: {\n openCheckpointCount: number;\n unresolvedCommentCount: number;\n askCeoRequestCount: number;\n };\n };\n checkpointSchedule: HitlCheckpointSchedule;\n checkpoints: HitlCheckpoint[];\n artifactComments: HitlArtifactComment[];\n askCeoRequests: HitlAskCeoRequest[];\n}\n\nexport interface UpdateHitlCheckpointScheduleInput {\n enabled?: boolean;\n timezone?: string;\n notificationChannels?: HitlNotificationChannel[];\n weeklyReview?: Partial<HitlCheckpointSchedule[\"weeklyReview\"]>;\n milestoneGate?: Partial<HitlCheckpointSchedule[\"milestoneGate\"]>;\n kpiDeviation?: {\n enabled?: boolean;\n thresholds?: HitlKpiThreshold[];\n };\n}\n\nexport interface CreateHitlCheckpointInput {\n triggerType?: HitlCheckpointTriggerType;\n title: string;\n description?: string;\n dueAt?: string;\n artifactRefs?: string[];\n metadata?: Record<string, unknown>;\n recipientType: HitlRecipientType;\n recipientId: string;\n}\n\nexport interface CreateHitlArtifactCommentInput {\n artifact: HitlArtifactComment[\"artifact\"];\n anchor?: HitlArtifactComment[\"anchor\"];\n body: string;\n routing: HitlArtifactComment[\"routing\"];\n}\n\nexport interface CreateHitlAskCeoRequestInput {\n question: string;\n context?: HitlAskCeoRequest[\"context\"];\n}\n\nfunction createPaperclipRunId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `hitl-${Date.now()}`;\n}\n\n/** GET /api/hitl/companies/:companyId/state */\nexport async function getHitlCompanyState(\n companyId: string,\n accessToken?: string\n): Promise<HitlCompanyState> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/state`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL state: ${res.status}`));\n }\n return res.json() as Promise<HitlCompanyState>;\n}\n\n/** GET /api/hitl/companies/:companyId/notifications */\nexport async function listHitlNotifications(\n companyId: string,\n accessToken?: string\n): Promise<HitlNotification[]> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/notifications`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to fetch HITL notifications: ${res.status}`));\n }\n const data = await res.json();\n return data.notifications as HitlNotification[];\n}\n\n/** PUT /api/hitl/companies/:companyId/checkpoint-schedule */\nexport async function updateHitlCheckpointSchedule(\n companyId: string,\n input: UpdateHitlCheckpointScheduleInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpointSchedule> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoint-schedule`, {\n method: \"PUT\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to update HITL schedule: ${res.status}`));\n }\n const data = await res.json();\n return data.schedule as HitlCheckpointSchedule;\n}\n\n/** POST /api/hitl/companies/:companyId/checkpoints */\nexport async function createHitlCheckpoint(\n companyId: string,\n input: CreateHitlCheckpointInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlCheckpoint> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/checkpoints`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL checkpoint: ${res.status}`));\n }\n const data = await res.json();\n return data.checkpoint as HitlCheckpoint;\n}\n\n/** POST /api/hitl/companies/:companyId/artifact-comments */\nexport async function createHitlArtifactComment(\n companyId: string,\n input: CreateHitlArtifactCommentInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlArtifactComment> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/artifact-comments`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to create HITL comment: ${res.status}`));\n }\n const data = await res.json();\n return data.comment as HitlArtifactComment;\n}\n\n/** POST /api/hitl/companies/:companyId/ask-ceo/requests */\nexport async function createHitlAskCeoRequest(\n companyId: string,\n input: CreateHitlAskCeoRequestInput,\n accessToken?: string,\n runId = createPaperclipRunId()\n): Promise<HitlAskCeoRequest> {\n const res = await fetch(`${BASE}/hitl/companies/${encodeURIComponent(companyId)}/ask-ceo/requests`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n throw new Error(await readApiError(res, `Failed to ask the CEO: ${res.status}`));\n }\n const data = await res.json();\n return data.request as HitlAskCeoRequest;\n}\n"}, "cb32bc8cd576330e910731a636690b68a9575124": {"t": 1785766002.0179622, "n": 37795, "h": "070dab88e363067f", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName = \"openai\" | \"anthropic\" | \"gemini\" | \"mistral\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n maskedApiKey: string; // e.g. \"sk-...x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failure\"\n | \"down\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source: \"api\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string | null;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source: \"api\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await fetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await fetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(accessToken?: string): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n const res = await fetch(`${BASE}/connectors/health`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n return res.json() as Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n }>;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await fetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await fetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await fetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n"}, "07de0e639fba6b3a2eb45051920b59dac4d6dc77": {"t": 1785766003.686907, "n": 41859, "h": "14c74a6bb5b04393", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName = \"openai\" | \"anthropic\" | \"gemini\" | \"mistral\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n maskedApiKey: string; // e.g. \"sk-...x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failure\"\n | \"down\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source: \"mock\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source: \"mock\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\nexport type ObservabilityEventCategory = \"issue\" | \"run\" | \"heartbeat\" | \"budget\" | \"alert\";\n\nexport interface ObservabilityEvent {\n id: string;\n sequence: string;\n userId: string;\n category: ObservabilityEventCategory;\n type: string;\n actor: {\n type: \"agent\" | \"user\" | \"system\" | \"run\";\n id: string;\n label?: string;\n };\n subject: {\n type: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n id: string;\n label?: string;\n parentType?: \"team\" | \"agent\" | \"task\" | \"execution\" | \"ticket\" | \"workspace\";\n parentId?: string;\n };\n summary: string;\n payload: Record<string, unknown>;\n occurredAt: string;\n}\n\nexport interface ObservabilityFeedPage {\n events: ObservabilityEvent[];\n nextCursor: string | null;\n hasMore: boolean;\n generatedAt: string;\n}\n\nexport interface ObservabilityThroughputSnapshot {\n windowHours: number;\n generatedAt: string;\n summary: {\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n completionRate: number;\n };\n buckets: Array<{\n bucketStart: string;\n createdCount: number;\n completedCount: number;\n blockedCount: number;\n }>;\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failure\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"down\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failure: connectors.filter((c) => c.state === \"auth_failure\").length,\n down: connectors.filter((c) => c.state === \"down\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\nexport function getObservabilityStreamPath(params?: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n}): string {\n const search = new URLSearchParams();\n if (params?.after) {\n search.set(\"after\", params.after);\n }\n if (params?.categories?.length) {\n search.set(\"categories\", params.categories.join(\",\"));\n }\n if (typeof params?.limit === \"number\") {\n search.set(\"limit\", String(params.limit));\n }\n\n const query = search.toString();\n return `${BASE}/observability/events/stream${query ? `?${query}` : \"\"}`;\n}\n\nexport async function listObservabilityEvents(\n input: {\n after?: string;\n categories?: ObservabilityEventCategory[];\n limit?: number;\n } = {},\n accessToken?: string\n): Promise<ObservabilityFeedPage> {\n const search = new URLSearchParams();\n if (input.after) {\n search.set(\"after\", input.after);\n }\n if (input.categories?.length) {\n search.set(\"categories\", input.categories.join(\",\"));\n }\n if (typeof input.limit === \"number\") {\n search.set(\"limit\", String(input.limit));\n }\n\n const response = await fetch(\n `${BASE}/observability/events${search.toString() ? `?${search.toString()}` : \"\"}`,\n {\n headers: buildAuthHeaders(accessToken),\n }\n );\n return parseJsonOrError<ObservabilityFeedPage>(\n response,\n `Failed to fetch observability events: ${response.status}`\n );\n}\n\nexport async function getObservabilityThroughput(\n windowHours = 24,\n accessToken?: string\n): Promise<ObservabilityThroughputSnapshot> {\n const response = await fetch(`${BASE}/observability/throughput?windowHours=${windowHours}`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<ObservabilityThroughputSnapshot>(\n response,\n `Failed to fetch throughput snapshot: ${response.status}`\n );\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await fetch(`${BASE}/connectors/health`);\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n return res.json() as Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n }>;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await fetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await fetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await fetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n"}, "e6957f2d5fee8ec3fbb2c7f9e437aeb162eab989": {"t": 1785766005.2324524, "n": 38888, "h": "2bfc497d18728aa1", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName = \"openai\" | \"anthropic\" | \"gemini\" | \"mistral\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n maskedApiKey: string; // e.g. \"sk-...x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failure\"\n | \"down\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source: \"mock\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source: \"mock\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function parseJsonOrError<T>(response: Response, fallback: string): Promise<T> {\n if (!response.ok) {\n throw new Error(await readApiError(response, fallback));\n }\n return response.json() as Promise<T>;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failure\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"down\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failure: connectors.filter((c) => c.state === \"auth_failure\").length,\n down: connectors.filter((c) => c.state === \"down\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport type TeamAssemblySourceType = \"free_text\" | \"notion\" | \"google-doc\" | \"markdown\";\nexport type TeamAssemblyModelTier = \"lite\" | \"standard\" | \"power\";\nexport type TeamAssemblyRoleType = \"executive\" | \"operator\";\n\nexport interface TeamAssemblyNormalizedGoalDocument {\n sourceType: TeamAssemblySourceType;\n goal: string;\n targetCustomer: string | null;\n successMetrics: string[];\n constraints: string[];\n budget: string | null;\n timeHorizon: string | null;\n importedContextSummary?: string | null;\n planReadinessThreshold: number;\n}\n\nexport interface TeamAssemblyPrd {\n title: string;\n summary: string;\n targetCustomer: string;\n problemStatement: string;\n proposedSolution: string;\n successMetrics: string[];\n constraints: string[];\n budget: string;\n timeHorizon: string;\n assumptions?: string[];\n risks?: string[];\n openQuestions?: string[];\n}\n\nexport interface TeamAssemblyRoleLibraryEntry {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n mandate: string;\n defaultReportsToRoleKey?: string | null;\n defaultSkills?: string[];\n defaultTools?: string[];\n defaultModelTier: TeamAssemblyModelTier;\n hiringSignals?: string[];\n}\n\nexport interface TeamAssemblyStaffingRecommendation {\n roleKey: string;\n title: string;\n roleType: TeamAssemblyRoleType;\n department: string;\n headcount: number;\n reportsToRoleKey: string | null;\n mandate: string;\n justification: string;\n kpis: string[];\n skills: string[];\n tools: string[];\n modelTier: TeamAssemblyModelTier;\n budgetMonthlyUsd: number | null;\n provisioningInstructions: string;\n}\n\nexport interface TeamAssemblyResult {\n schemaVersion: string;\n company: {\n name: string | null;\n goal: string;\n targetCustomer: string | null;\n budget: string | null;\n timeHorizon: string | null;\n };\n summary: string;\n rationale: string;\n orgChart: {\n executives: TeamAssemblyStaffingRecommendation[];\n operators: TeamAssemblyStaffingRecommendation[];\n reportingLines: Array<{\n managerRoleKey: string;\n reportRoleKey: string;\n }>;\n };\n provisioningPlan: {\n teamName: string;\n deploymentMode: \"continuous_agents\";\n agents: TeamAssemblyStaffingRecommendation[];\n };\n roadmap306090: {\n day30: TeamAssemblyPhasePlan;\n day60: TeamAssemblyPhasePlan;\n day90: TeamAssemblyPhasePlan;\n };\n}\n\nexport interface TeamAssemblyPhasePlan {\n objectives: string[];\n deliverables: string[];\n ownerRoleKeys: string[];\n}\n\nexport interface TeamAssemblyRequestInput {\n companyName?: string;\n normalizedGoalDocument: TeamAssemblyNormalizedGoalDocument;\n prd?: TeamAssemblyPrd;\n roleLibrary?: TeamAssemblyRoleLibraryEntry[];\n}\n\nexport interface CompanyRoleTemplate {\n id: string;\n name: string;\n description: string;\n defaultModel: string;\n defaultInstructions: string;\n defaultSkills: string[];\n}\n\nexport interface CompanyProvisioningContract {\n schemaVersion: string;\n endpoint: string;\n requiredHeaders: string[];\n companyFields: {\n required: string[];\n optional: string[];\n };\n agentFields: {\n identifierFields: string[];\n requiredOneOf: string[];\n optional: string[];\n };\n}\n\nexport interface CompanyRoleTemplateCatalogResponse {\n roleTemplates: CompanyRoleTemplate[];\n total: number;\n provisioningContract: CompanyProvisioningContract;\n}\n\nexport interface CompanyProvisioningAgentInput {\n roleTemplateId?: string;\n roleKey?: string;\n name?: string;\n budgetMonthlyUsd?: number;\n model?: string;\n instructions?: string;\n skills?: string[];\n}\n\nexport interface CompanyProvisioningInput {\n name: string;\n workspaceName?: string;\n externalCompanyId?: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n orchestrationEnabled?: boolean;\n secretBindings: Record<string, string>;\n agents: CompanyProvisioningAgentInput[];\n}\n\nexport interface ProvisionedCompanySummary {\n id: string;\n userId: string;\n name: string;\n externalCompanyId?: string;\n workspaceId: string;\n teamId: string;\n idempotencyKey: string;\n budgetMonthlyUsd: number;\n allocatedBudgetMonthlyUsd: number;\n remainingBudgetMonthlyUsd: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedWorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ProvisionedSecretSummary {\n key: string;\n maskedValue: string;\n}\n\nexport interface ProvisioningSkillSummary {\n id: string;\n name: string;\n description: string;\n scope: \"workflow\" | \"agent\" | \"security\" | \"integration\";\n}\n\nexport interface CompanyProvisioningResult {\n company: ProvisionedCompanySummary;\n workspace: ProvisionedWorkspaceSummary;\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n secretBindings: ProvisionedSecretSummary[];\n availableSkills: ProvisioningSkillSummary[];\n idempotentReplay: boolean;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await fetch(`${BASE}/connectors/health`);\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n return res.json() as Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n }>;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\nexport async function generateTeamAssemblyPlan(\n input: TeamAssemblyRequestInput,\n accessToken?: string\n): Promise<TeamAssemblyResult> {\n const res = await fetch(`${BASE}/goals/team-assembly`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to generate staffing plan: ${res.status}`);\n }\n return res.json() as Promise<TeamAssemblyResult>;\n}\n\nexport async function listCompanyRoleTemplates(\n accessToken?: string\n): Promise<CompanyRoleTemplateCatalogResponse> {\n const res = await fetch(`${BASE}/companies/role-templates`, {\n headers: buildAuthHeaders(accessToken),\n });\n return parseJsonOrError<CompanyRoleTemplateCatalogResponse>(\n res,\n `Failed to fetch company role templates: ${res.status}`\n );\n}\n\nexport async function provisionCompanyWorkspace(\n input: CompanyProvisioningInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `company-provision-${Date.now()}`\n): Promise<CompanyProvisioningResult> {\n const res = await fetch(`${BASE}/companies`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to provision company workspace: ${res.status}`);\n }\n return res.json() as Promise<CompanyProvisioningResult>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n"}, "a6cdcaf8413ffda58b079aaf7824515ebd4aafb4": {"t": 1785766006.773922, "n": 32267, "h": "4360cd132d5f6d6c", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName = \"openai\" | \"anthropic\" | \"gemini\" | \"mistral\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n maskedApiKey: string; // e.g. \"sk-...x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nexport type ConnectorHealthState =\n | \"healthy\"\n | \"degraded\"\n | \"rate_limited\"\n | \"auth_failure\"\n | \"down\";\n\nexport interface ConnectorHealthTransition {\n at: string;\n from: ConnectorHealthState;\n to: ConnectorHealthState;\n reason: string;\n}\n\nexport interface ConnectorHealthRecord {\n connectorKey: string;\n connectorName: string;\n state: ConnectorHealthState;\n lastSuccessAt: string | null;\n lastErrorAt: string | null;\n lastErrorMessage: string | null;\n successRate24h: number;\n authFailures15m: number;\n rateLimitEvents15m: number;\n transitions: ConnectorHealthTransition[];\n source: \"mock\";\n}\n\nexport interface ConnectorHealthSummary {\n total: number;\n states: Record<ConnectorHealthState, number>;\n lastUpdatedAt: string;\n alertPolicy: {\n degradedWithinMinutes: number;\n authFailureThreshold15m: number;\n rateLimitThreshold15m: number;\n outageThresholdMinutes: number;\n };\n source: \"mock\";\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\nconst MOCK_CONNECTOR_HEALTH: ConnectorHealthRecord[] = [\n {\n connectorKey: \"slack\",\n connectorName: \"Slack\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.8,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [\n {\n at: \"2026-04-28T02:10:00.000Z\",\n from: \"degraded\",\n to: \"healthy\",\n reason: \"API latency recovered below threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"hubspot\",\n connectorName: \"HubSpot\",\n state: \"degraded\",\n lastSuccessAt: \"2026-04-28T04:31:00.000Z\",\n lastErrorAt: \"2026-04-28T04:33:00.000Z\",\n lastErrorMessage: \"Elevated 5xx responses from provider API\",\n successRate24h: 94.3,\n authFailures15m: 0,\n rateLimitEvents15m: 1,\n transitions: [\n {\n at: \"2026-04-28T04:20:00.000Z\",\n from: \"healthy\",\n to: \"degraded\",\n reason: \"Connector-wide provider failures crossed threshold\",\n },\n ],\n source: \"mock\",\n },\n {\n connectorKey: \"stripe\",\n connectorName: \"Stripe\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorAt: \"2026-04-28T01:11:00.000Z\",\n lastErrorMessage: \"Transient timeout retried successfully\",\n successRate24h: 99.5,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"gmail\",\n connectorName: \"Gmail\",\n state: \"rate_limited\",\n lastSuccessAt: \"2026-04-28T04:32:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"429 rate limit window active for sync jobs\",\n successRate24h: 92.9,\n authFailures15m: 0,\n rateLimitEvents15m: 8,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"sentry\",\n connectorName: \"Sentry\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:30:00.000Z\",\n lastErrorAt: null,\n lastErrorMessage: null,\n successRate24h: 99.9,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"linear\",\n connectorName: \"Linear\",\n state: \"auth_failure\",\n lastSuccessAt: \"2026-04-28T03:58:00.000Z\",\n lastErrorAt: \"2026-04-28T04:34:00.000Z\",\n lastErrorMessage: \"OAuth refresh token rejected by provider\",\n successRate24h: 88.1,\n authFailures15m: 6,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"teams\",\n connectorName: \"Teams\",\n state: \"healthy\",\n lastSuccessAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorAt: \"2026-04-27T22:42:00.000Z\",\n lastErrorMessage: \"Webhook delivery delay recovered\",\n successRate24h: 98.7,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n {\n connectorKey: \"jira\",\n connectorName: \"Jira\",\n state: \"down\",\n lastSuccessAt: \"2026-04-28T02:48:00.000Z\",\n lastErrorAt: \"2026-04-28T04:35:00.000Z\",\n lastErrorMessage: \"Connector worker has not completed a successful sync in 90 minutes\",\n successRate24h: 76.4,\n authFailures15m: 0,\n rateLimitEvents15m: 0,\n transitions: [],\n source: \"mock\",\n },\n];\n\nfunction summarizeConnectorHealth(connectors: ConnectorHealthRecord[]): ConnectorHealthSummary {\n return {\n total: connectors.length,\n states: {\n healthy: connectors.filter((c) => c.state === \"healthy\").length,\n degraded: connectors.filter((c) => c.state === \"degraded\").length,\n rate_limited: connectors.filter((c) => c.state === \"rate_limited\").length,\n auth_failure: connectors.filter((c) => c.state === \"auth_failure\").length,\n down: connectors.filter((c) => c.state === \"down\").length,\n },\n lastUpdatedAt: \"2026-04-28T04:35:00.000Z\",\n alertPolicy: {\n degradedWithinMinutes: 5,\n authFailureThreshold15m: 5,\n rateLimitThreshold15m: 5,\n outageThresholdMinutes: 15,\n },\n source: \"mock\",\n };\n}\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\nexport async function getConnectorHealth(): Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n}> {\n if (USE_MOCK_API) {\n return {\n connectors: [...MOCK_CONNECTOR_HEALTH],\n summary: summarizeConnectorHealth(MOCK_CONNECTOR_HEALTH),\n };\n }\n\n const res = await fetch(`${BASE}/connectors/health`);\n if (!res.ok) throw new Error(`Failed to fetch connector health: ${res.status}`);\n return res.json() as Promise<{\n connectors: ConnectorHealthRecord[];\n summary: ConnectorHealthSummary;\n }>;\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n"}, "ab5a79efb6fdcc1cf119c3c26c64f477c15238c1": {"t": 1785766008.19864, "n": 26798, "h": "15811ce28deb7dd0", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName = \"openai\" | \"anthropic\" | \"gemini\" | \"mistral\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n maskedApiKey: string; // e.g. \"sk-...x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n return withMockApi(\n async () => {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n },\n () => []\n );\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n"}, "342e7454f851bdcc4b8460b6457a279321932ec1": {"t": 1785766009.6202846, "n": 26705, "h": "75b33444afcd6219", "c": "import type { WorkflowTemplate, WorkflowRun, WorkflowStep } from \"../types/workflow\";\nimport { getApiBasePath } from \"./baseUrl\";\nimport { readStoredAuthUser } from \"../auth/authStorage\";\nimport {\n createMockTemplate,\n getMockRun,\n getMockTemplate,\n listMockLLMConfigs,\n listMockRuns,\n listMockTemplates,\n startMockRun,\n} from \"./mockWorkflowData\";\n\n// ---------------------------------------------------------------------------\n// LLM Config types — mirrors src/engine/llmProviders/types.ts\n// ---------------------------------------------------------------------------\n\nexport type ProviderName = \"openai\" | \"anthropic\" | \"gemini\" | \"mistral\";\n\n/** Available models per provider — mirrors PROVIDER_MODELS from the backend */\nexport const PROVIDER_MODELS: Record<ProviderName, string[]> = {\n openai: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\"],\n anthropic: [\"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5-20251001\"],\n gemini: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n mistral: [\"mistral-large-latest\", \"mistral-small-latest\", \"open-mistral-7b\"],\n};\n\n/** A saved LLM provider config (API key stored server-side, never returned) */\nexport interface LLMConfig {\n id: string;\n label: string;\n provider: ProviderName;\n model: string;\n isDefault: boolean;\n maskedApiKey: string; // e.g. \"sk-...x7k3\"\n createdAt: string;\n}\n\nexport interface CreateLLMConfigInput {\n label: string;\n provider: ProviderName;\n model: string;\n apiKey: string;\n}\n\nconst USE_MOCK_API = import.meta.env.VITE_USE_MOCK === \"true\";\n\nfunction buildAuthHeaders(accessToken?: string): HeadersInit | undefined {\n if (accessToken) {\n return { Authorization: `Bearer ${accessToken}` };\n }\n\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n return { \"X-User-Id\": storedUser.id };\n }\n\n return undefined;\n}\n\nfunction buildJsonHeaders(\n accessToken?: string,\n extras?: Record<string, string | undefined>\n): HeadersInit {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n if (accessToken) {\n headers.Authorization = `Bearer ${accessToken}`;\n } else {\n const storedUser = readStoredAuthUser();\n if (storedUser?.id) {\n headers[\"X-User-Id\"] = storedUser.id;\n }\n }\n\n if (extras) {\n for (const [key, value] of Object.entries(extras)) {\n if (value) {\n headers[key] = value;\n }\n }\n }\n\n return headers;\n}\n\nasync function readApiError(response: Response, fallback: string): Promise<string> {\n const payload = (await response.json().catch(() => null)) as { error?: string } | null;\n return payload?.error ?? fallback;\n}\n\nasync function withMockApi<T>(remote: () => Promise<T>, local: () => T | Promise<T>): Promise<T> {\n if (USE_MOCK_API) {\n return await local();\n }\n return await remote();\n}\n\n// ---------------------------------------------------------------------------\n// LLM Config API functions\n// ---------------------------------------------------------------------------\n\n/** GET /api/llm-configs */\nexport async function listLLMConfigs(accessToken: string): Promise<LLMConfig[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/llm-configs`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to fetch LLM configs: ${res.status}`);\n const data = await res.json();\n return data.configs as LLMConfig[];\n },\n () => listMockLLMConfigs()\n );\n}\n\n/** POST /api/llm-configs */\nexport async function createLLMConfig(\n input: CreateLLMConfigInput,\n accessToken?: string\n): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create LLM config: ${res.status}`);\n }\n return res.json() as Promise<LLMConfig>;\n}\n\n/** PATCH /api/llm-configs/:id/default */\nexport async function setDefaultLLMConfig(id: string, accessToken?: string): Promise<LLMConfig> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}/default`, {\n method: \"PATCH\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to set default: ${res.status}`);\n return res.json() as Promise<LLMConfig>;\n}\n\n/** DELETE /api/llm-configs/:id */\nexport async function deleteLLMConfig(id: string, accessToken?: string): Promise<void> {\n const res = await fetch(`${BASE}/llm-configs/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Failed to delete LLM config: ${res.status}`);\n}\n\nconst BASE = getApiBasePath();\n\n/** Template summary returned by GET /api/templates (list) */\nexport interface TemplateSummary {\n id: string;\n name: string;\n description: string;\n category: WorkflowTemplate[\"category\"];\n version: string;\n stepCount: number;\n configFieldCount: number;\n}\n\nexport type ControlPlaneAgentLifecycleStatus = \"active\" | \"paused\" | \"terminated\";\nexport type ControlPlaneHeartbeatStatus = \"queued\" | \"running\" | \"completed\" | \"blocked\";\nexport type ControlPlaneTaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"blocked\";\nexport type ControlPlaneTeamDeploymentMode = \"workflow_runtime\" | \"continuous_agents\";\nexport type ControlPlaneAgentScheduleType = \"manual\" | \"interval\" | \"cron\";\n\nexport interface ControlPlaneAgentSchedule {\n type: ControlPlaneAgentScheduleType;\n cronExpression?: string;\n intervalMinutes?: number;\n}\n\nexport interface ControlPlaneAgent {\n id: string;\n teamId: string;\n userId: string;\n name: string;\n roleKey: string;\n workflowStepId?: string;\n workflowStepKind?: string;\n model?: string;\n instructions: string;\n budgetMonthlyUsd: number;\n reportingToAgentId?: string;\n schedule: ControlPlaneAgentSchedule;\n status: ControlPlaneAgentLifecycleStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTeam {\n id: string;\n userId: string;\n name: string;\n description?: string;\n workflowTemplateId?: string;\n workflowTemplateName?: string;\n deploymentMode: ControlPlaneTeamDeploymentMode;\n budgetMonthlyUsd: number;\n orchestrationEnabled: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ControlPlaneTaskAuditEvent {\n id: string;\n type: \"created\" | \"checked_out\" | \"status_changed\";\n actor: string;\n detail: string;\n timestamp: string;\n}\n\nexport interface ControlPlaneTask {\n id: string;\n teamId: string;\n userId: string;\n title: string;\n description?: string;\n sourceRunId?: string;\n sourceWorkflowStepId?: string;\n assignedAgentId?: string;\n checkedOutBy?: string;\n checkedOutAt?: string;\n status: ControlPlaneTaskStatus;\n metadata?: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n auditTrail: ControlPlaneTaskAuditEvent[];\n}\n\nexport interface ControlPlaneHeartbeatRecord {\n id: string;\n teamId: string;\n agentId: string;\n userId: string;\n status: ControlPlaneHeartbeatStatus;\n summary?: string;\n costUsd?: number;\n createdTaskIds: string[];\n startedAt: string;\n completedAt?: string;\n}\n\nexport interface ControlPlaneDeployment {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n workflow: Pick<WorkflowTemplate, \"id\" | \"name\" | \"category\" | \"version\">;\n}\n\nexport interface ControlPlaneTeamDetail {\n team: ControlPlaneTeam;\n agents: ControlPlaneAgent[];\n tasks: ControlPlaneTask[];\n heartbeats: ControlPlaneHeartbeatRecord[];\n}\n\nexport interface DeployWorkflowAsTeamInput {\n templateId: string;\n teamName?: string;\n budgetMonthlyUsd?: number;\n defaultIntervalMinutes?: number;\n}\n\ntype CreateTemplateInput = Omit<WorkflowTemplate, \"id\"> & { id?: string };\n\n/** GET /api/templates */\nexport async function listTemplates(category?: string): Promise<TemplateSummary[]> {\n return withMockApi(\n async () => {\n const url = category ? `${BASE}/templates?category=${encodeURIComponent(category)}` : `${BASE}/templates`;\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to fetch templates: ${res.status}`);\n const data = await res.json();\n return data.templates as TemplateSummary[];\n },\n () => listMockTemplates(category as WorkflowTemplate[\"category\"] | undefined)\n );\n}\n\n/** POST /api/templates */\nexport async function createTemplate(input: CreateTemplateInput): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to save template: ${res.status}`);\n }\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => createMockTemplate(input)\n );\n}\n\n/** GET /api/templates/:id */\nexport async function getTemplate(id: string): Promise<WorkflowTemplate> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/templates/${encodeURIComponent(id)}`);\n if (!res.ok) throw new Error(`Template not found: ${id}`);\n return res.json() as Promise<WorkflowTemplate>;\n },\n () => getMockTemplate(id)\n );\n}\n\n/** GET /api/runs */\nexport async function listRuns(templateId?: string, accessToken?: string): Promise<WorkflowRun[]> {\n return withMockApi(\n async () => {\n const url = templateId\n ? `${BASE}/runs?templateId=${encodeURIComponent(templateId)}`\n : `${BASE}/runs`;\n const res = await fetch(url, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch runs: ${res.status}`));\n const data = await res.json();\n return data.runs as WorkflowRun[];\n },\n () => listMockRuns(templateId)\n );\n}\n\n/** GET /api/runs/:id */\nexport async function getRun(id: string, accessToken?: string): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/${encodeURIComponent(id)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(`Run not found: ${id}`);\n return res.json() as Promise<WorkflowRun>;\n },\n () => getMockRun(id)\n );\n}\n\n/** GET /api/control-plane/teams */\nexport async function listControlPlaneTeams(accessToken?: string): Promise<ControlPlaneTeam[]> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed teams: ${res.status}`));\n const data = await res.json();\n return data.teams as ControlPlaneTeam[];\n },\n () => []\n );\n}\n\n/** GET /api/control-plane/teams/:id */\nexport async function getControlPlaneTeam(\n teamId: string,\n accessToken?: string\n): Promise<ControlPlaneTeamDetail> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/control-plane/teams/${encodeURIComponent(teamId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (!res.ok) throw new Error(await readApiError(res, `Failed to fetch deployed team: ${res.status}`));\n return res.json() as Promise<ControlPlaneTeamDetail>;\n },\n () => {\n throw new Error(`Deployed team not found: ${teamId}`);\n }\n );\n}\n\n/** POST /api/control-plane/deployments/workflow */\nexport async function deployWorkflowAsTeam(\n input: DeployWorkflowAsTeamInput,\n accessToken?: string,\n runId = globalThis.crypto?.randomUUID?.() ?? `control-plane-${Date.now()}`\n): Promise<ControlPlaneDeployment> {\n const res = await fetch(`${BASE}/control-plane/deployments/workflow`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken, {\n \"X-Paperclip-Run-Id\": runId,\n }),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to deploy workflow as team: ${res.status}`);\n }\n return res.json() as Promise<ControlPlaneDeployment>;\n}\n\n/** POST /api/runs */\nexport async function startRun(\n templateId: string,\n input: Record<string, unknown>,\n config?: Record<string, unknown>,\n accessToken?: string\n): Promise<WorkflowRun> {\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ templateId, input, config }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, input)\n );\n}\n\n/** POST /api/workflows/generate — NL description → workflow steps */\nexport async function generateWorkflow(\n description: string,\n llmConfigId?: string,\n accessToken?: string\n): Promise<WorkflowStep[]> {\n const res = await fetch(`${BASE}/workflows/generate`, {\n method: \"POST\",\n headers: buildJsonHeaders(accessToken),\n body: JSON.stringify({ description, llmConfigId }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Generation failed: ${res.status}`);\n }\n const data = await res.json();\n return data.steps as WorkflowStep[];\n}\n\n/**\n * POST /api/runs/file — start a file-triggered run via multipart upload.\n * @param templateId The workflow template to execute.\n * @param file The File object from a file input or drop event.\n * @param userId Optional user identifier forwarded as X-User-Id.\n */\nexport async function startRunWithFile(\n templateId: string,\n file: File,\n userId?: string,\n accessToken?: string\n): Promise<WorkflowRun> {\n const form = new FormData();\n form.append(\"templateId\", templateId);\n form.append(\"file\", file);\n\n const headers: Record<string, string> = {};\n if (userId) headers[\"X-User-Id\"] = userId;\n if (accessToken) headers.Authorization = `Bearer ${accessToken}`;\n\n return withMockApi(\n async () => {\n const res = await fetch(`${BASE}/runs/file`, { method: \"POST\", headers, body: form });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to start file run: ${res.status}`);\n }\n return res.json() as Promise<WorkflowRun>;\n },\n () => startMockRun(templateId, { fileName: file.name || \"mock-upload\" })\n );\n}\n\n/** POST /api/debug/step — AI debugger for failed steps */\nexport async function debugStep(\n stepId: string,\n error: string,\n output: Record<string, unknown>\n): Promise<{ explanation: string; suggestion: string }> {\n const res = await fetch(`${BASE}/debug/step`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ stepId, error, output }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Debug request failed: ${res.status}`);\n }\n return res.json() as Promise<{ explanation: string; suggestion: string }>;\n}\n\n// ---------------------------------------------------------------------------\n// Proposal Builder API\n// ---------------------------------------------------------------------------\n\nexport interface ProposalCrmRecord {\n id: string;\n accountName: string;\n contactName: string;\n contactEmail: string;\n dealValue: number;\n stage: string;\n owner: string;\n updatedAt: string;\n}\n\nexport interface ProposalTemplateOption {\n id: string;\n name: string;\n description: string;\n focus: string;\n}\n\nexport interface ProposalHistoryItem {\n id: string;\n accountName: string;\n status: \"Draft\" | \"Sent\" | \"Exported\";\n format: \"PDF\" | \"DOCX\";\n exportedAt: string;\n}\n\nexport interface ProposalUsageSummary {\n used: number;\n limit: number;\n}\n\nexport interface ProposalDraftResult {\n title: string;\n body: string;\n variableHints: string[];\n}\n\nexport interface ProposalContextResponse {\n records: ProposalCrmRecord[];\n templates: ProposalTemplateOption[];\n history: ProposalHistoryItem[];\n usage: ProposalUsageSummary;\n}\n\nexport interface CreateProposalRequest {\n crmRecordIds: string[];\n templateId: string;\n}\n\nexport interface CreateProposalResponse {\n jobId: string;\n status: \"queued\" | \"processing\";\n pollUrl?: string;\n}\n\nexport interface ProposalJobStatusResponse {\n jobId: string;\n status: \"queued\" | \"processing\" | \"completed\" | \"failed\";\n draft?: ProposalDraftResult;\n events: string[];\n usage?: ProposalUsageSummary;\n}\n\nconst PROPOSAL_CONTEXT_FALLBACK: ProposalContextResponse = {\n records: [\n {\n id: \"crm-001\",\n accountName: \"Northwind Logistics\",\n contactName: \"Jordan Lee\",\n contactEmail: \"jordan@northwind.example\",\n dealValue: 125000,\n stage: \"Negotiation\",\n owner: \"R. Bennett\",\n updatedAt: \"2026-04-18T14:10:00.000Z\",\n },\n {\n id: \"crm-002\",\n accountName: \"Summit BioSystems\",\n contactName: \"Amaya Patel\",\n contactEmail: \"amaya@summit.example\",\n dealValue: 78000,\n stage: \"Proposal Requested\",\n owner: \"D. Kim\",\n updatedAt: \"2026-04-18T16:35:00.000Z\",\n },\n {\n id: \"crm-003\",\n accountName: \"Atlas Retail Group\",\n contactName: \"Chris Romero\",\n contactEmail: \"chris@atlas.example\",\n dealValue: 212000,\n stage: \"Discovery\",\n owner: \"S. Flores\",\n updatedAt: \"2026-04-18T19:03:00.000Z\",\n },\n ],\n templates: [\n {\n id: \"tpl-enterprise-modern\",\n name: \"Enterprise Modern\",\n description: \"Executive summary and phased delivery model.\",\n focus: \"Enterprise rollout\",\n },\n {\n id: \"tpl-growth-velocity\",\n name: \"Growth Velocity\",\n description: \"Outcome-led narrative built around measurable upside.\",\n focus: \"Revenue acceleration\",\n },\n {\n id: \"tpl-technical-deep-dive\",\n name: \"Technical Deep Dive\",\n description: \"Security, architecture, and integration-heavy proposal framing.\",\n focus: \"Technical buyers\",\n },\n ],\n history: [\n {\n id: \"proposal-882\",\n accountName: \"Aster Cloud\",\n status: \"Exported\",\n format: \"PDF\",\n exportedAt: \"2026-04-15T13:10:00.000Z\",\n },\n {\n id: \"proposal-881\",\n accountName: \"Falcon Finance\",\n status: \"Sent\",\n format: \"DOCX\",\n exportedAt: \"2026-04-14T10:22:00.000Z\",\n },\n {\n id: \"proposal-880\",\n accountName: \"Pioneer Health\",\n status: \"Draft\",\n format: \"PDF\",\n exportedAt: \"2026-04-13T17:45:00.000Z\",\n },\n ],\n usage: { used: 4, limit: 5 },\n};\n\nconst PROPOSAL_JOB_FALLBACK: ProposalJobStatusResponse = {\n jobId: \"mock-proposal-job\",\n status: \"completed\",\n events: [\n \"CRM context validated\",\n \"Proposal brief generated\",\n \"Draft sections assembled\",\n \"Formatting pass complete\",\n ],\n draft: {\n title: \"AutoFlow Proposal Draft\",\n body:\n \"# Executive Summary\\n\\nAutoFlow can reduce manual proposal prep by 70%.\\n\\n## Solution Outline\\n\\n- CRM context ingestion\\n- AI-assisted draft generation\\n- Team review and export\\n\\n## Commercial Terms\\n\\nEstimated annual value: {{DealValue}}\",\n variableHints: [\"{{ClientName}}\", \"{{DealValue}}\", \"{{OwnerName}}\"],\n },\n usage: { used: 5, limit: 5 },\n};\n\nexport async function listProposalContext(accessToken?: string): Promise<ProposalContextResponse> {\n const res = await fetch(`${BASE}/proposals/context`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return PROPOSAL_CONTEXT_FALLBACK;\n }\n if (!res.ok) throw new Error(`Failed to fetch proposal context: ${res.status}`);\n const data = await res.json();\n return {\n records: (data.records ?? PROPOSAL_CONTEXT_FALLBACK.records) as ProposalCrmRecord[],\n templates: (data.templates ?? PROPOSAL_CONTEXT_FALLBACK.templates) as ProposalTemplateOption[],\n history: (data.history ?? PROPOSAL_CONTEXT_FALLBACK.history) as ProposalHistoryItem[],\n usage: (data.usage ?? PROPOSAL_CONTEXT_FALLBACK.usage) as ProposalUsageSummary,\n };\n}\n\nexport async function createProposalDraft(\n input: CreateProposalRequest,\n accessToken: string\n): Promise<CreateProposalResponse> {\n const res = await fetch(`${BASE}/proposals`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...buildAuthHeaders(accessToken),\n },\n body: JSON.stringify(input),\n });\n if (res.status === 404) {\n return { jobId: PROPOSAL_JOB_FALLBACK.jobId, status: \"queued\" };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to create proposal draft: ${res.status}`);\n }\n return res.json() as Promise<CreateProposalResponse>;\n}\n\nexport async function getProposalJobStatus(\n jobId: string,\n accessToken: string\n): Promise<ProposalJobStatusResponse> {\n const res = await fetch(`${BASE}/proposals/${encodeURIComponent(jobId)}`, {\n headers: buildAuthHeaders(accessToken),\n });\n if (res.status === 404) {\n return { ...PROPOSAL_JOB_FALLBACK, jobId };\n }\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to fetch proposal job status: ${res.status}`);\n }\n return res.json() as Promise<ProposalJobStatusResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// Memory API — persistent context memory store\n// ---------------------------------------------------------------------------\n\nexport interface MemoryEntry {\n id: string;\n userId: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n key: string;\n text: string;\n ttlSeconds?: number;\n createdAt: string;\n updatedAt: string;\n expiresAt?: string;\n}\n\nexport interface MemorySearchResult {\n entry: MemoryEntry;\n score: number;\n}\n\nexport interface MemoryStats {\n totalEntries: number;\n totalBytes: number;\n workflowCount: number;\n}\n\nexport interface WriteMemoryInput {\n key: string;\n text: string;\n workflowId?: string;\n workflowName?: string;\n agentId?: string;\n ttlSeconds?: number;\n}\n\nfunction getMemoryHeaders(accessToken: string, userId?: string): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n };\n if (userId) headers[\"X-User-Id\"] = userId;\n return headers;\n}\n\n/** GET /api/memory — list all entries for the current user */\nexport async function listMemoryEntries(\n accessToken: string,\n userId?: string,\n workflowId?: string\n): Promise<MemoryEntry[]> {\n const url = workflowId\n ? `${BASE}/memory?workflowId=${encodeURIComponent(workflowId)}`\n : `${BASE}/memory`;\n const res = await fetch(url, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory entries: ${res.status}`);\n const data = await res.json();\n return data.entries as MemoryEntry[];\n}\n\n/** GET /api/memory/search — keyword/semantic search */\nexport async function searchMemory(\n query: string,\n accessToken: string,\n userId?: string,\n agentId?: string\n): Promise<MemorySearchResult[]> {\n const params = new URLSearchParams({ q: query });\n if (agentId) params.set(\"agentId\", agentId);\n const res = await fetch(`${BASE}/memory/search?${params.toString()}`, {\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok) throw new Error(`Memory search failed: ${res.status}`);\n const data = await res.json();\n return data.results as MemorySearchResult[];\n}\n\n/** POST /api/memory — write (create or upsert) a memory entry */\nexport async function writeMemoryEntry(\n input: WriteMemoryInput,\n accessToken: string,\n userId?: string\n): Promise<MemoryEntry> {\n const res = await fetch(`${BASE}/memory`, {\n method: \"POST\",\n headers: getMemoryHeaders(accessToken, userId),\n body: JSON.stringify(input),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to write memory: ${res.status}`);\n }\n return res.json() as Promise<MemoryEntry>;\n}\n\n/** DELETE /api/memory/:id — delete a single entry */\nexport async function deleteMemoryEntry(id: string, accessToken: string, userId?: string): Promise<void> {\n const res = await fetch(`${BASE}/memory/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n headers: getMemoryHeaders(accessToken, userId),\n });\n if (!res.ok && res.status !== 404) throw new Error(`Failed to delete memory entry: ${res.status}`);\n}\n\n/** GET /api/memory/stats — usage stats */\nexport async function getMemoryStats(accessToken: string, userId?: string): Promise<MemoryStats> {\n const res = await fetch(`${BASE}/memory/stats`, { headers: getMemoryHeaders(accessToken, userId) });\n if (!res.ok) throw new Error(`Failed to fetch memory stats: ${res.status}`);\n return res.json() as Promise<MemoryStats>;\n}\n\n// ---------------------------------------------------------------------------\n// Approvals API — HITL approval inbox\n// ---------------------------------------------------------------------------\n\nexport interface ApprovalRequest {\n id: string;\n runId: string;\n templateName: string;\n stepId: string;\n stepName: string;\n assignee: string;\n message: string;\n timeoutMinutes: number;\n requestedAt: string;\n status: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\";\n resolvedAt?: string;\n comment?: string;\n}\n\n/** GET /api/approvals */\nexport async function listApprovals(\n accessToken: string,\n status?: \"pending\" | \"approved\" | \"rejected\" | \"timed_out\"\n): Promise<ApprovalRequest[]> {\n const url = status\n ? `${BASE}/approvals?status=${encodeURIComponent(status)}`\n : `${BASE}/approvals`;\n const res = await fetch(url, { headers: buildAuthHeaders(accessToken) });\n if (!res.ok) throw new Error(`Failed to fetch approvals: ${res.status}`);\n const data = await res.json();\n return data.approvals as ApprovalRequest[];\n}\n\n/** POST /api/approvals/:id/resolve */\nexport async function resolveApproval(\n id: string,\n decision: \"approved\" | \"rejected\",\n accessToken: string,\n comment?: string\n): Promise<void> {\n const res = await fetch(`${BASE}/approvals/${encodeURIComponent(id)}/resolve`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...buildAuthHeaders(accessToken) },\n body: JSON.stringify({ decision, comment }),\n });\n if (!res.ok) {\n const err = await res.json().catch(() => null);\n throw new Error(err?.error ?? `Failed to resolve approval: ${res.status}`);\n }\n}\n"}}}}