diff --git a/apps/docs/components/workflow-preview/format-references.tsx b/apps/docs/components/workflow-preview/format-references.tsx index 6a3c01ae21e..15bfeb5c049 100644 --- a/apps/docs/components/workflow-preview/format-references.tsx +++ b/apps/docs/components/workflow-preview/format-references.tsx @@ -15,12 +15,10 @@ export function formatReferences(text: string): ReactNode[] { const isReference = (part.startsWith('<') && part.endsWith('>')) || (part.startsWith('{{') && part.endsWith('}}')) return isReference ? ( - // biome-ignore lint/suspicious/noArrayIndexKey: static, never reordered {part} ) : ( - // biome-ignore lint/suspicious/noArrayIndexKey: static, never reordered {part} ) }) diff --git a/apps/sim/app/api/auth/oauth2/authorize/route.test.ts b/apps/sim/app/api/auth/oauth2/authorize/route.test.ts new file mode 100644 index 00000000000..894e543c3b2 --- /dev/null +++ b/apps/sim/app/api/auth/oauth2/authorize/route.test.ts @@ -0,0 +1,323 @@ +/** + * @vitest-environment node + */ +import { createMockRequest, dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockGetSession, + mockOAuth2LinkAccount, + mockCheckWorkspaceAccess, + mockGetCredentialActorContext, +} = vi.hoisted(() => ({ + mockGetSession: vi.fn(), + mockOAuth2LinkAccount: vi.fn(), + mockCheckWorkspaceAccess: vi.fn(), + mockGetCredentialActorContext: vi.fn(), +})) + +vi.mock('@sim/db', () => dbChainMock) + +vi.mock('@/lib/auth/auth', () => ({ + auth: { api: { oAuth2LinkAccount: mockOAuth2LinkAccount } }, + getSession: mockGetSession, +})) + +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + checkWorkspaceAccess: mockCheckWorkspaceAccess, +})) + +vi.mock('@/lib/credentials/access', () => ({ + getCredentialActorContext: mockGetCredentialActorContext, +})) + +vi.mock('@/lib/oauth/utils', () => ({ + getAllOAuthServices: vi.fn(() => [{ providerId: 'google-email', name: 'Gmail' }]), +})) + +import { GET } from '@/app/api/auth/oauth2/authorize/route' + +const BASE_URL = 'https://sim.test' +const WORKSPACE_ID = 'ws-1' +const USER_ID = 'user-1' +const CREDENTIAL_ID = 'cred-1' +const LINK_URL = 'https://provider.example/authorize?state=abc' + +function authorizeRequest(query: Record) { + const url = new URL(`${BASE_URL}/api/auth/oauth2/authorize`) + for (const [key, value] of Object.entries(query)) { + url.searchParams.set(key, value) + } + return createMockRequest('GET', undefined, {}, url.toString()) +} + +function oauthCredentialActor(overrides: Record = {}) { + return { + credential: { + id: CREDENTIAL_ID, + workspaceId: WORKSPACE_ID, + type: 'oauth', + providerId: 'google-email', + displayName: 'Work Gmail', + ...((overrides.credential as Record) ?? {}), + }, + member: null, + hasWorkspaceAccess: true, + canWriteWorkspace: true, + isAdmin: true, + ...Object.fromEntries(Object.entries(overrides).filter(([key]) => key !== 'credential')), + } +} + +describe('OAuth2 authorize route', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + process.env.NEXT_PUBLIC_APP_URL = BASE_URL + mockGetSession.mockResolvedValue({ user: { id: USER_ID } }) + mockCheckWorkspaceAccess.mockResolvedValue({ + hasAccess: true, + canWrite: true, + canAdmin: false, + workspace: { id: WORKSPACE_ID }, + }) + mockOAuth2LinkAccount.mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ url: LINK_URL }), + headers: { getSetCookie: () => ['better-auth.state=xyz; Path=/'] }, + }) + }) + + describe('plain connect (no credentialId)', () => { + it('creates a draft with credentialId null and redirects to the provider', async () => { + const response = await GET( + authorizeRequest({ providerId: 'google-email', workspaceId: WORKSPACE_ID }) + ) + + expect(response.headers.get('location')).toBe(LINK_URL) + expect(mockGetCredentialActorContext).not.toHaveBeenCalled() + expect(dbChainMockFns.values).toHaveBeenCalledWith( + expect.objectContaining({ + userId: USER_ID, + workspaceId: WORKSPACE_ID, + providerId: 'google-email', + credentialId: null, + }) + ) + expect(dbChainMockFns.onConflictDoUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + set: expect.objectContaining({ credentialId: null }), + }) + ) + }) + + it('numbers the draft display name when the default collides with an existing credential', async () => { + dbChainMockFns.where + .mockImplementationOnce(() => Promise.resolve([{ name: 'Justin' }])) + .mockImplementationOnce(() => Promise.resolve([{ displayName: "Justin's Gmail" }])) + + await GET(authorizeRequest({ providerId: 'google-email', workspaceId: WORKSPACE_ID })) + + expect(dbChainMockFns.values).toHaveBeenCalledWith( + expect.objectContaining({ displayName: "Justin's Gmail 2" }) + ) + }) + + it('nulls out credentialId in the upsert set so a stale reconnect draft cannot leak into a plain connect', async () => { + await GET(authorizeRequest({ providerId: 'google-email', workspaceId: WORKSPACE_ID })) + + const [{ set }] = dbChainMockFns.onConflictDoUpdate.mock.calls[0] + expect(set).toHaveProperty('credentialId', null) + }) + + it('redirects to login when unauthenticated', async () => { + mockGetSession.mockResolvedValue(null) + + const response = await GET( + authorizeRequest({ providerId: 'google-email', workspaceId: WORKSPACE_ID }) + ) + + expect(response.headers.get('location')).toContain('/login') + expect(dbChainMockFns.values).not.toHaveBeenCalled() + }) + + it('rejects without workspace write access', async () => { + mockCheckWorkspaceAccess.mockResolvedValue({ + hasAccess: true, + canWrite: false, + canAdmin: false, + workspace: { id: WORKSPACE_ID }, + }) + + const response = await GET( + authorizeRequest({ providerId: 'google-email', workspaceId: WORKSPACE_ID }) + ) + + expect(response.headers.get('location')).toBe( + `${BASE_URL}/workspace?error=workspace_access_denied` + ) + expect(dbChainMockFns.values).not.toHaveBeenCalled() + expect(mockOAuth2LinkAccount).not.toHaveBeenCalled() + }) + }) + + describe('reconnect (credentialId present)', () => { + it('creates a reconnect draft carrying credentialId in values and upsert set', async () => { + mockGetCredentialActorContext.mockResolvedValue(oauthCredentialActor()) + + const response = await GET( + authorizeRequest({ + providerId: 'google-email', + workspaceId: WORKSPACE_ID, + credentialId: CREDENTIAL_ID, + }) + ) + + expect(response.headers.get('location')).toBe(LINK_URL) + expect(mockGetCredentialActorContext).toHaveBeenCalledWith( + CREDENTIAL_ID, + USER_ID, + expect.objectContaining({ workspaceAccess: expect.anything() }) + ) + expect(dbChainMockFns.values).toHaveBeenCalledWith( + expect.objectContaining({ credentialId: CREDENTIAL_ID }) + ) + expect(dbChainMockFns.onConflictDoUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + set: expect.objectContaining({ credentialId: CREDENTIAL_ID }), + }) + ) + }) + + it("uses the credential's actual display name for the reconnect draft (audit accuracy)", async () => { + mockGetCredentialActorContext.mockResolvedValue( + oauthCredentialActor({ credential: { displayName: 'Renamed By User' } }) + ) + + await GET( + authorizeRequest({ + providerId: 'google-email', + workspaceId: WORKSPACE_ID, + credentialId: CREDENTIAL_ID, + }) + ) + + expect(dbChainMockFns.values).toHaveBeenCalledWith( + expect.objectContaining({ displayName: 'Renamed By User' }) + ) + }) + + it('rejects reconnect for custom-flow providers (trello/shopify) and writes no draft', async () => { + for (const providerId of ['trello', 'shopify']) { + const response = await GET( + authorizeRequest({ providerId, workspaceId: WORKSPACE_ID, credentialId: CREDENTIAL_ID }) + ) + + expect(response.headers.get('location')).toBe( + `${BASE_URL}/workspace?error=credential_reconnect_unsupported` + ) + } + expect(mockGetCredentialActorContext).not.toHaveBeenCalled() + expect(dbChainMockFns.values).not.toHaveBeenCalled() + expect(mockOAuth2LinkAccount).not.toHaveBeenCalled() + }) + + it('rejects when the caller is not a credential admin and writes no draft', async () => { + mockGetCredentialActorContext.mockResolvedValue(oauthCredentialActor({ isAdmin: false })) + + const response = await GET( + authorizeRequest({ + providerId: 'google-email', + workspaceId: WORKSPACE_ID, + credentialId: CREDENTIAL_ID, + }) + ) + + expect(response.headers.get('location')).toBe( + `${BASE_URL}/workspace?error=credential_access_denied` + ) + expect(dbChainMockFns.values).not.toHaveBeenCalled() + expect(mockOAuth2LinkAccount).not.toHaveBeenCalled() + }) + + it('rejects when the credential belongs to a different workspace', async () => { + mockGetCredentialActorContext.mockResolvedValue( + oauthCredentialActor({ credential: { workspaceId: 'ws-other' } }) + ) + + const response = await GET( + authorizeRequest({ + providerId: 'google-email', + workspaceId: WORKSPACE_ID, + credentialId: CREDENTIAL_ID, + }) + ) + + expect(response.headers.get('location')).toBe( + `${BASE_URL}/workspace?error=credential_access_denied` + ) + expect(dbChainMockFns.values).not.toHaveBeenCalled() + }) + + it('rejects when the credential does not exist', async () => { + mockGetCredentialActorContext.mockResolvedValue({ + credential: null, + member: null, + hasWorkspaceAccess: false, + canWriteWorkspace: false, + isAdmin: false, + }) + + const response = await GET( + authorizeRequest({ + providerId: 'google-email', + workspaceId: WORKSPACE_ID, + credentialId: 'cred-missing', + }) + ) + + expect(response.headers.get('location')).toBe( + `${BASE_URL}/workspace?error=credential_access_denied` + ) + expect(dbChainMockFns.values).not.toHaveBeenCalled() + }) + + it('rejects a non-oauth credential', async () => { + mockGetCredentialActorContext.mockResolvedValue( + oauthCredentialActor({ credential: { type: 'env_workspace' } }) + ) + + const response = await GET( + authorizeRequest({ + providerId: 'google-email', + workspaceId: WORKSPACE_ID, + credentialId: CREDENTIAL_ID, + }) + ) + + expect(response.headers.get('location')).toBe( + `${BASE_URL}/workspace?error=credential_access_denied` + ) + expect(dbChainMockFns.values).not.toHaveBeenCalled() + }) + + it('rejects when the query providerId does not match the credential provider', async () => { + mockGetCredentialActorContext.mockResolvedValue(oauthCredentialActor()) + + const response = await GET( + authorizeRequest({ + providerId: 'slack', + workspaceId: WORKSPACE_ID, + credentialId: CREDENTIAL_ID, + }) + ) + + expect(response.headers.get('location')).toBe( + `${BASE_URL}/workspace?error=credential_provider_mismatch` + ) + expect(dbChainMockFns.values).not.toHaveBeenCalled() + expect(mockOAuth2LinkAccount).not.toHaveBeenCalled() + }) + }) +}) diff --git a/apps/sim/app/api/auth/oauth2/authorize/route.ts b/apps/sim/app/api/auth/oauth2/authorize/route.ts index 65bb6a773b9..884762d3afd 100644 --- a/apps/sim/app/api/auth/oauth2/authorize/route.ts +++ b/apps/sim/app/api/auth/oauth2/authorize/route.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { pendingCredentialDraft, user } from '@sim/db/schema' +import { credential, pendingCredentialDraft, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, eq, lt } from 'drizzle-orm' @@ -9,6 +9,8 @@ import { parseRequest } from '@/lib/api/server' import { auth, getSession } from '@/lib/auth/auth' import { getBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getCredentialActorContext } from '@/lib/credentials/access' +import { defaultCredentialDisplayName } from '@/lib/credentials/display-name' import { getAllOAuthServices } from '@/lib/oauth/utils' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' @@ -30,20 +32,53 @@ async function createConnectDraft(params: { userId: string workspaceId: string providerId: string + credentialId?: string + /** Reconnect only: the credential's actual name, so audit records stay accurate. */ + displayName?: string }): Promise { - const { userId, workspaceId, providerId } = params - - const service = getAllOAuthServices().find((s) => s.providerId === providerId) - const serviceName = service?.name ?? providerId + const { userId, workspaceId, providerId, credentialId } = params + + let displayName = params.displayName + if (!displayName) { + const service = getAllOAuthServices().find((s) => s.providerId === providerId) + const serviceName = service?.name ?? providerId + + let userName: string | null = null + try { + const [row] = await db.select({ name: user.name }).from(user).where(eq(user.id, userId)) + userName = row?.name ?? null + } catch (error) { + // Cosmetic only — fall back to the "My {Service}" default + logger.warn('User name lookup failed for connect draft display name', { + userId, + workspaceId, + providerId, + error, + }) + } - let displayName = serviceName - try { - const [row] = await db.select({ name: user.name }).from(user).where(eq(user.id, userId)) - if (row?.name) { - displayName = `${row.name}'s ${serviceName}` + // Auto-number against existing workspace credentials so repeat connects for + // the same provider stay distinguishable — same behavior as the connect + // modal, which computes this client-side. Best effort: on failure the name + // simply skips deduplication. + let takenNames: ReadonlySet = new Set() + try { + const rows = await db + .select({ displayName: credential.displayName }) + .from(credential) + .where(and(eq(credential.workspaceId, workspaceId), eq(credential.type, 'oauth'))) + takenNames = new Set(rows.map((row) => row.displayName.toLowerCase())) + } catch (error) { + // Cosmetic only — proceed without collision numbering + logger.warn('Credential name lookup failed for connect draft deduplication', { + userId, + workspaceId, + providerId, + error, + }) } - } catch { - // Fall back to service name only + + displayName = defaultCredentialDisplayName(userName, serviceName, takenNames) } const now = new Date() @@ -61,6 +96,7 @@ async function createConnectDraft(params: { workspaceId, providerId, displayName, + credentialId: credentialId ?? null, expiresAt, createdAt: now, }) @@ -70,10 +106,18 @@ async function createConnectDraft(params: { pendingCredentialDraft.providerId, pendingCredentialDraft.workspaceId, ], - set: { displayName, expiresAt, createdAt: now }, + // credentialId must be written on BOTH paths: a plain connect that reuses a + // stale reconnect draft row would otherwise silently rebind the old + // credential instead of creating a new one. + set: { displayName, credentialId: credentialId ?? null, expiresAt, createdAt: now }, }) - logger.info('Created OAuth connect credential draft', { userId, workspaceId, providerId }) + logger.info('Created OAuth connect credential draft', { + userId, + workspaceId, + providerId, + credentialId: credentialId ?? null, + }) } /** @@ -92,7 +136,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => { const parsed = await parseRequest(authorizeOAuth2Contract, request, {}) if (!parsed.success) return parsed.response - const { providerId, workspaceId, callbackURL: requestedCallback } = parsed.data.query + const { + providerId, + workspaceId, + callbackURL: requestedCallback, + credentialId, + } = parsed.data.query const callbackURL = requestedCallback?.startsWith(`${baseUrl}/`) ? requestedCallback @@ -109,10 +158,65 @@ export const GET = withRouteHandler(async (request: NextRequest) => { return NextResponse.redirect(`${baseUrl}/workspace?error=workspace_access_denied`) } + let reconnectDisplayName: string | undefined + if (credentialId) { + // Trello and Shopify authorize through their own custom flows that bypass + // this endpoint, so a reconnect draft written here would linger unconsumed + // and could later be picked up by their token-store callbacks, silently + // rebinding the credential. Mirror the copilot tool and reject reconnect. + if (providerId === 'trello' || providerId === 'shopify') { + logger.warn('Reconnect not supported for custom-flow provider', { + userId, + workspaceId, + providerId, + credentialId, + }) + return NextResponse.redirect(`${baseUrl}/workspace?error=credential_reconnect_unsupported`) + } + + // Reconnect: the OAuth callback will rebind this credential to the fresh + // account, so require the same credential-admin access as the draft POST + // route — workspace write alone must not be enough to swap someone's tokens. + const actor = await getCredentialActorContext(credentialId, userId, { + workspaceAccess: access, + }) + if ( + !actor.credential || + actor.credential.workspaceId !== workspaceId || + actor.credential.type !== 'oauth' || + !actor.isAdmin + ) { + logger.warn('Credential admin access denied for OAuth2 reconnect', { + userId, + workspaceId, + providerId, + credentialId, + }) + return NextResponse.redirect(`${baseUrl}/workspace?error=credential_access_denied`) + } + if (actor.credential.providerId !== providerId) { + logger.warn('Provider mismatch for OAuth2 reconnect', { + userId, + workspaceId, + providerId, + credentialId, + credentialProviderId: actor.credential.providerId, + }) + return NextResponse.redirect(`${baseUrl}/workspace?error=credential_provider_mismatch`) + } + reconnectDisplayName = actor.credential.displayName + } + // Create the draft before initiating the link so it is guaranteed to exist // (and freshly clocked) when the OAuth callback's `account.create.after` // hook runs. If this throws, we never start the OAuth flow. - await createConnectDraft({ userId, workspaceId, providerId }) + await createConnectDraft({ + userId, + workspaceId, + providerId, + credentialId, + displayName: reconnectDisplayName, + }) const linkResponse = await auth.api.oAuth2LinkAccount({ body: { providerId, callbackURL }, diff --git a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts new file mode 100644 index 00000000000..c863c50b0d6 --- /dev/null +++ b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts @@ -0,0 +1,533 @@ +/** + * @vitest-environment node + */ +import { copilotHttpMock, copilotHttpMockFns } from '@sim/testing' +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockTransaction, + mockSelectRows, + mockCheckStorageQuota, + mockFilterForkableChatFiles, + mockListForkableChatFiles, + mockPlanChatFileCopies, + mockExecuteChatFileBlobCopies, + mockLoadCopilotChatMessages, + mockAppendCopilotChatMessages, + mockAssertActiveWorkspaceAccess, + mockFetchGo, + mockPublishStatusChanged, + mockCaptureServerEvent, + mockDeleteWhere, + mockRemoveChatResources, +} = vi.hoisted(() => ({ + mockTransaction: vi.fn(), + mockSelectRows: vi.fn(), + mockCheckStorageQuota: vi.fn(), + // Real (pure) cut semantics so tests drive selection through row.messageId: + // rows with a NULL/undefined messageId are kept in every fork. + mockFilterForkableChatFiles: vi.fn( + (rows: Array<{ messageId?: string | null }>, kept: ReadonlySet) => + rows.filter((row) => !row.messageId || kept.has(row.messageId)) + ), + mockListForkableChatFiles: vi.fn(), + mockPlanChatFileCopies: vi.fn(), + mockExecuteChatFileBlobCopies: vi.fn(), + mockLoadCopilotChatMessages: vi.fn(), + mockAppendCopilotChatMessages: vi.fn(), + mockAssertActiveWorkspaceAccess: vi.fn(), + mockFetchGo: vi.fn(), + mockPublishStatusChanged: vi.fn(), + mockCaptureServerEvent: vi.fn(), + mockDeleteWhere: vi.fn(), + mockRemoveChatResources: vi.fn(), +})) + +vi.mock('@sim/db', () => ({ + db: { + select: () => ({ + from: () => ({ + where: () => ({ + limit: () => mockSelectRows(), + }), + }), + }), + delete: () => ({ + where: mockDeleteWhere, + }), + transaction: mockTransaction, + }, +})) + +vi.mock('@sim/db/schema', () => ({ + copilotChats: { + id: 'copilotChats.id', + userId: 'copilotChats.userId', + type: 'copilotChats.type', + workspaceId: 'copilotChats.workspaceId', + title: 'copilotChats.title', + model: 'copilotChats.model', + resources: 'copilotChats.resources', + previewYaml: 'copilotChats.previewYaml', + planArtifact: 'copilotChats.planArtifact', + config: 'copilotChats.config', + }, + workspaceFiles: { + id: 'workspaceFiles.id', + }, +})) + +vi.mock('drizzle-orm', () => ({ + eq: vi.fn((field: unknown, value: unknown) => ({ type: 'eq', field, value })), + inArray: vi.fn((field: unknown, values: unknown) => ({ type: 'inArray', field, values })), +})) + +vi.mock('@/lib/copilot/resources/persistence', () => ({ + removeChatResources: mockRemoveChatResources, +})) + +vi.mock('@/lib/copilot/request/http', () => copilotHttpMock) + +vi.mock('@/lib/billing/storage', () => ({ + checkStorageQuota: mockCheckStorageQuota, +})) + +vi.mock('@/lib/copilot/chat/fork-chat-files', () => ({ + filterForkableChatFiles: mockFilterForkableChatFiles, + listForkableChatFiles: mockListForkableChatFiles, + planChatFileCopies: mockPlanChatFileCopies, + executeChatFileBlobCopies: mockExecuteChatFileBlobCopies, +})) + +vi.mock('@/lib/copilot/chat/lifecycle', () => ({ + loadCopilotChatMessages: mockLoadCopilotChatMessages, +})) + +vi.mock('@/lib/copilot/chat/messages-store', () => ({ + appendCopilotChatMessages: mockAppendCopilotChatMessages, +})) + +vi.mock('@/lib/copilot/chat-status', () => ({ + chatPubSub: { publishStatusChanged: mockPublishStatusChanged }, +})) + +vi.mock('@/lib/copilot/request/go/fetch', () => ({ + fetchGo: mockFetchGo, +})) + +vi.mock('@/lib/copilot/server/agent-url', () => ({ + getMothershipBaseURL: vi.fn().mockResolvedValue('http://mothership.test'), + getMothershipSourceEnvHeaders: vi.fn().mockReturnValue({}), +})) + +vi.mock('@/lib/core/config/env', () => ({ env: {} })) + +vi.mock('@/lib/posthog/server', () => ({ + captureServerEvent: mockCaptureServerEvent, +})) + +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + assertActiveWorkspaceAccess: mockAssertActiveWorkspaceAccess, + isWorkspaceAccessDeniedError: () => false, +})) + +import { POST } from '@/app/api/mothership/chats/[chatId]/fork/route' + +const OLD_FILE_ID = 'wf_oldfile' +const NEW_FILE_ID = 'wf_newfile' + +const parentRow = { + id: 'chat-1', + userId: 'user-1', + type: 'mothership', + workspaceId: 'ws-1', + title: 'Generate Logs', + model: 'claude-opus-4-8', + resources: [{ type: 'file', id: OLD_FILE_ID, title: 'cat.png' }], + previewYaml: null, + planArtifact: null, + config: null, +} + +const threeMessages = [ + { + id: 'msg-1', + role: 'user', + content: `See ![cat](/api/files/view/${OLD_FILE_ID})`, + timestamp: '2026-07-01T00:00:00.000Z', + }, + { + id: 'msg-2', + role: 'assistant', + content: 'Nice cat.', + timestamp: '2026-07-01T00:00:01.000Z', + }, + { + id: 'msg-3', + role: 'user', + content: 'A later message the fork must not keep.', + timestamp: '2026-07-01T00:00:02.000Z', + }, +] + +/** Chat rows inserted through the mock transaction, captured for title assertions. */ +let insertedChatRows: Array> = [] +/** tx.update(...).set(...) payloads, captured for resource-rewrite assertions. */ +let updatedChatRows: Array> = [] + +function makeTx() { + return { + insert: () => ({ + values: (v: Record) => { + insertedChatRows.push(v) + return { + returning: async () => [{ id: 'row-id', workspaceId: 'ws-1' }], + } + }, + }), + update: () => ({ + set: (v: Record) => { + updatedChatRows.push(v) + return { where: async () => undefined } + }, + }), + } +} + +function createRequest(chatId: string, body?: unknown) { + return new NextRequest(`http://localhost:3000/api/mothership/chats/${chatId}/fork`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body ?? { upToMessageId: 'msg-2' }), + }) +} + +function makeContext(chatId: string) { + return { params: Promise.resolve({ chatId }) } +} + +describe('POST /api/mothership/chats/[chatId]/fork', () => { + beforeEach(() => { + vi.clearAllMocks() + insertedChatRows = [] + updatedChatRows = [] + copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({ + userId: 'user-1', + isAuthenticated: true, + }) + mockSelectRows.mockResolvedValue([parentRow]) + mockListForkableChatFiles.mockResolvedValue([]) + mockCheckStorageQuota.mockResolvedValue({ allowed: true }) + mockLoadCopilotChatMessages.mockResolvedValue(threeMessages) + mockPlanChatFileCopies.mockResolvedValue({ + idMap: new Map(), + keyMap: new Map(), + blobTasks: [], + }) + mockExecuteChatFileBlobCopies.mockResolvedValue({ copied: 0, failed: 0, failedCopyIds: [] }) + mockAppendCopilotChatMessages.mockResolvedValue(undefined) + mockDeleteWhere.mockResolvedValue(undefined) + mockRemoveChatResources.mockResolvedValue(undefined) + mockAssertActiveWorkspaceAccess.mockResolvedValue(undefined) + mockFetchGo.mockResolvedValue({ ok: true }) + mockTransaction.mockImplementation(async (cb: (tx: unknown) => Promise) => + cb(makeTx()) + ) + }) + + it('rejects unauthenticated callers', async () => { + copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({ + userId: null, + isAuthenticated: false, + }) + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(401) + }) + + it('404s when the chat belongs to another user', async () => { + mockSelectRows.mockResolvedValue([{ ...parentRow, userId: 'someone-else' }]) + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(404) + expect(mockTransaction).not.toHaveBeenCalled() + }) + + it('404s for non-mothership chats', async () => { + mockSelectRows.mockResolvedValue([{ ...parentRow, type: 'copilot' }]) + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(404) + }) + + it('400s when upToMessageId is missing', async () => { + const res = await POST(createRequest('chat-1', {}), makeContext('chat-1')) + expect(res.status).toBe(400) + expect(mockTransaction).not.toHaveBeenCalled() + }) + + it('400s when upToMessageId is an empty string', async () => { + const res = await POST(createRequest('chat-1', { upToMessageId: '' }), makeContext('chat-1')) + expect(res.status).toBe(400) + expect(mockTransaction).not.toHaveBeenCalled() + }) + + it('400s when the message is not in the chat', async () => { + const res = await POST( + createRequest('chat-1', { upToMessageId: 'msg-unknown' }), + makeContext('chat-1') + ) + expect(res.status).toBe(400) + expect(mockTransaction).not.toHaveBeenCalled() + }) + + it('applies the timeline cut: kept message ids drive the file selection', async () => { + // Two uploads born pre-cut, one born post-cut, and one legacy row with no + // birth message. The single chat-owned read is cut in memory: everything + // but the post-cut row. + const preCutUpload = { id: 'wf_up', size: 1, context: 'mothership', messageId: 'msg-1' } + const secondPreCut = { id: 'wf_up2', size: 1, context: 'mothership', messageId: 'msg-2' } + const postCutUpload = { id: 'wf_late', size: 1, context: 'mothership', messageId: 'msg-3' } + const legacyRow = { id: 'wf_legacy', size: 1, context: 'mothership', messageId: null } + mockListForkableChatFiles.mockResolvedValue([ + preCutUpload, + secondPreCut, + postCutUpload, + legacyRow, + ]) + + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(200) + + // The cut runs over the single read with the kept slice (inclusive of + // msg-2, excluding msg-3). + expect(mockListForkableChatFiles).toHaveBeenCalledTimes(1) + const filterCall = mockFilterForkableChatFiles.mock.calls[0] + expect(filterCall[1]).toEqual(new Set(['msg-1', 'msg-2'])) + + // The copy plan receives the cut set — the pre-cut uploads plus the + // legacy no-birthdate row; the post-cut upload stays behind. + expect(mockPlanChatFileCopies.mock.calls[0][0].rows).toEqual([ + preCutUpload, + secondPreCut, + legacyRow, + ]) + + // The appended transcript is the same inclusive slice. + const appended = mockAppendCopilotChatMessages.mock.calls[0] + expect(appended[1].map((m: { id: string }) => m.id)).toEqual(['msg-1', 'msg-2']) + }) + + it('fails up front with the quota error when copied bytes would exceed the limit', async () => { + mockListForkableChatFiles.mockResolvedValue([ + { size: 600, workspaceId: 'ws-1' }, + { size: 400, workspaceId: 'ws-1' }, + ]) + mockCheckStorageQuota.mockResolvedValue({ allowed: false, error: 'Storage limit exceeded' }) + + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + + expect(res.status).toBe(400) + expect(mockCheckStorageQuota).toHaveBeenCalledWith('user-1', 1000) + expect(mockTransaction).not.toHaveBeenCalled() + expect(mockExecuteChatFileBlobCopies).not.toHaveBeenCalled() + }) + + it('excludes uncopyable rows (no workspaceId) from the quota sum', async () => { + // planChatFileCopies skips workspaceId-less legacy rows, so their bytes + // must not count against the gate. + mockListForkableChatFiles.mockResolvedValue([{ size: 600, workspaceId: 'ws-1' }, { size: 400 }]) + + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + + expect(res.status).toBe(200) + expect(mockCheckStorageQuota).toHaveBeenCalledWith('user-1', 600) + }) + + it('skips the quota check entirely when no chat-owned rows are in the cut', async () => { + // The chat owns one file, but it was born after the fork point. + mockListForkableChatFiles.mockResolvedValue([ + { id: 'wf_late', size: 500, context: 'mothership', messageId: 'msg-3' }, + ]) + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(200) + expect(mockCheckStorageQuota).not.toHaveBeenCalled() + }) + + it('forks the chat: copies kept uploads, rewrites references, clones agent state', async () => { + const blobTasks = [ + { + sourceKey: 'workspace/ws-1/old-cat.png', + targetKey: 'workspace/ws-1/new-cat.png', + context: 'mothership', + fileName: 'cat.png', + contentType: 'image/png', + }, + ] + mockListForkableChatFiles.mockResolvedValue([ + { size: 100, messageId: 'msg-1', workspaceId: 'ws-1' }, + ]) + mockPlanChatFileCopies.mockResolvedValue({ + idMap: new Map([[OLD_FILE_ID, NEW_FILE_ID]]), + keyMap: new Map([['workspace/ws-1/old-cat.png', 'workspace/ws-1/new-cat.png']]), + blobTasks, + }) + + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + const body = await res.json() + + expect(res.status).toBe(200) + expect(body.success).toBe(true) + expect(typeof body.id).toBe('string') + + expect(mockCheckStorageQuota).toHaveBeenCalledWith('user-1', 100) + + // The real rewriter runs: the kept message's view-URL points at the copy. + const appended = mockAppendCopilotChatMessages.mock.calls[0] + expect(appended[0]).toBe(body.id) + expect(appended[1][0].content).toBe(`See ![cat](/api/files/view/${NEW_FILE_ID})`) + + expect(mockExecuteChatFileBlobCopies).toHaveBeenCalledWith(blobTasks, { + userId: 'user-1', + workspaceId: 'ws-1', + }) + + const goCall = mockFetchGo.mock.calls[0] + expect(goCall[0]).toBe('http://mothership.test/api/chats/fork') + const goBody = JSON.parse(goCall[1].body) + // The copilot service only knows USER message ids, so the clone cut is the + // kept slice's last user message (msg-1), not the clicked assistant (msg-2). + expect(goBody).toEqual({ + sourceChatId: 'chat-1', + newChatId: body.id, + upToMessageId: 'msg-1', + userId: 'user-1', + }) + + expect(mockPublishStatusChanged).toHaveBeenCalledWith({ + workspaceId: 'ws-1', + chatId: body.id, + type: 'created', + }) + expect(mockCaptureServerEvent).toHaveBeenCalledWith( + 'user-1', + 'task_forked', + { workspace_id: 'ws-1', source_chat_id: 'chat-1' }, + { groups: { workspace: 'ws-1' } } + ) + + // Forks are titled "Fork | ". + expect(insertedChatRows[0].title).toBe('Fork | Generate Logs') + }) + + it('still succeeds when the copilot-service clone fails (best-effort)', async () => { + mockFetchGo.mockRejectedValue(new Error('mothership unreachable')) + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(200) + }) + + it('surfaces failed blob copies and cleans up their dead rows + resource chips', async () => { + mockExecuteChatFileBlobCopies.mockResolvedValue({ + copied: 1, + failed: 2, + failedCopyIds: ['wf_dead1', 'wf_dead2'], + }) + + const failedRes = await POST(createRequest('chat-1'), makeContext('chat-1')) + const body = await failedRes.json() + + expect(body.failedFileCopies).toBe(2) + + // The dead rows (committed, but no bytes behind them) are hard-deleted so + // they vanish from VFS listings and name resolution… + expect(mockDeleteWhere).toHaveBeenCalledWith({ + type: 'inArray', + field: 'workspaceFiles.id', + values: ['wf_dead1', 'wf_dead2'], + }) + // …and their resource chips are dropped from the new chat. + expect(mockRemoveChatResources).toHaveBeenCalledWith(body.id, [ + { type: 'file', id: 'wf_dead1', title: '' }, + { type: 'file', id: 'wf_dead2', title: '' }, + ]) + }) + + it('omits failedFileCopies and skips cleanup when every blob copies', async () => { + mockExecuteChatFileBlobCopies.mockResolvedValue({ copied: 3, failed: 0, failedCopyIds: [] }) + + const cleanRes = await POST(createRequest('chat-1'), makeContext('chat-1')) + + expect('failedFileCopies' in (await cleanRes.json())).toBe(false) + expect(mockDeleteWhere).not.toHaveBeenCalled() + expect(mockRemoveChatResources).not.toHaveBeenCalled() + }) + + it('copies pre-cut uploads and drops only post-cut ghosts', async () => { + // The source chat owns two more uploads (apple pre-cut, banana post-cut) + // beside the kept one, plus one shared workspace-file resource. The fork + // copies the kept upload AND the pre-cut apple; only the post-cut banana + // stays behind, so only its resource is dropped — not left pointing at + // the source chat. + mockSelectRows.mockResolvedValue([ + { + ...parentRow, + resources: [ + { type: 'file', id: OLD_FILE_ID, title: 'cat.png' }, + { type: 'file', id: 'wf_apple', title: 'apple.png' }, + { type: 'file', id: 'wf_banana', title: 'banana.png' }, + { type: 'file', id: 'wf_shared', title: 'shared.pdf' }, + { type: 'workflow', id: 'wflow-1', title: 'My flow' }, + ], + }, + ]) + // Every chat-owned file of the source chat in the single read; messageId + // drives the in-memory cut. + mockListForkableChatFiles.mockResolvedValue([ + { id: OLD_FILE_ID, size: 100, context: 'mothership', messageId: 'msg-1' }, + { id: 'wf_apple', size: 50, context: 'mothership', messageId: 'msg-1' }, + { id: 'wf_banana', size: 50, context: 'mothership', messageId: 'msg-3' }, + ]) + mockPlanChatFileCopies.mockResolvedValue({ + idMap: new Map([ + [OLD_FILE_ID, NEW_FILE_ID], + ['wf_apple', 'wf_apple_copy'], + ]), + keyMap: new Map(), + blobTasks: [], + }) + + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + + expect(res.status).toBe(200) + // The plan received the cut set: the kept upload + pre-cut apple only. + expect(mockPlanChatFileCopies.mock.calls[0][0].rows.map((r: { id: string }) => r.id)).toEqual([ + OLD_FILE_ID, + 'wf_apple', + ]) + expect(updatedChatRows).toHaveLength(1) + expect(updatedChatRows[0].resources).toEqual([ + { type: 'file', id: NEW_FILE_ID, title: 'cat.png' }, + { type: 'file', id: 'wf_apple_copy', title: 'apple.png' }, + { type: 'file', id: 'wf_shared', title: 'shared.pdf' }, + { type: 'workflow', id: 'wflow-1', title: 'My flow' }, + ]) + }) + + it('drops ghosts even when the fork copies no files at all', async () => { + // Fork cut before the chat's only upload arrived: a guard that skips the + // resources update when idMap is empty would leave the ghost in place. + mockSelectRows.mockResolvedValue([ + { + ...parentRow, + resources: [{ type: 'file', id: 'wf_banana', title: 'banana.png' }], + }, + ]) + mockListForkableChatFiles.mockResolvedValue([ + { id: 'wf_banana', size: 50, context: 'mothership', messageId: 'msg-3' }, + ]) + + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + + expect(res.status).toBe(200) + expect(updatedChatRows).toHaveLength(1) + expect(updatedChatRows[0].resources).toEqual([]) + }) +}) diff --git a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts index 44b63c7f163..e2e361f2222 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts @@ -1,13 +1,24 @@ import { db } from '@sim/db' -import { copilotChats } from '@sim/db/schema' +import { copilotChats, workspaceFiles } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { eq } from 'drizzle-orm' +import { eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { forkMothershipChatContract } from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' +import { checkStorageQuota } from '@/lib/billing/storage' +import { + executeChatFileBlobCopies, + filterForkableChatFiles, + listForkableChatFiles, + planChatFileCopies, +} from '@/lib/copilot/chat/fork-chat-files' import { loadCopilotChatMessages } from '@/lib/copilot/chat/lifecycle' import { appendCopilotChatMessages } from '@/lib/copilot/chat/messages-store' +import { + rewriteMessageFileRefs, + rewriteResourceFileRefs, +} from '@/lib/copilot/chat/rewrite-file-references' import { chatPubSub } from '@/lib/copilot/chat-status' import { fetchGo } from '@/lib/copilot/request/go/fetch' import { @@ -18,6 +29,7 @@ import { createNotFoundResponse, createUnauthorizedResponse, } from '@/lib/copilot/request/http' +import { removeChatResources } from '@/lib/copilot/resources/persistence' import type { MothershipResource } from '@/lib/copilot/resources/types' import { getMothershipBaseURL, getMothershipSourceEnvHeaders } from '@/lib/copilot/server/agent-url' import { env } from '@/lib/core/config/env' @@ -33,7 +45,16 @@ const logger = createLogger('ForkChatAPI') /** * POST /api/mothership/chats/[chatId]/fork * Creates a new chat branched from the given chat, keeping messages up to and - * including the specified message. Resources and copilot-side state are copied. + * including the specified message, along with the chat's uploads born + * at-or-before the fork point (a file travels iff the user message that + * carried it is kept). Resources and copilot-side state are copied. + * + * Every copied file gets a fresh row id and storage key, bytes are physically + * copied and counted against the storage quota, and every in-transcript file + * reference is re-pointed at the copies so the new chat survives deletion of + * the source chat. File resources whose chat-owned file was NOT copied + * (uploads born after the cut) are dropped from the new chat's resources + * rather than left as ghosts pointing at the source chat's files. */ export const POST = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { @@ -82,17 +103,49 @@ export const POST = withRouteHandler( } const forkedMessages = messages.slice(0, forkIdx + 1) - // Resources are stored as a jsonb array on the chat row — copy them directly. + // Single workspace_files read per fork: every chat-owned upload. The + // copied set is timeline-cut to the kept message slice in memory (files + // born after the fork point stay behind). + const chatOwnedFiles = await listForkableChatFiles(db, chatId) + const sourceFiles = filterForkableChatFiles( + chatOwnedFiles, + new Set(forkedMessages.map((m) => m.id)) + ) + // Sum only rows the plan will actually copy — planChatFileCopies skips + // rows with no workspaceId, so counting their bytes could reject a fork + // whose real copies fit within quota. + const totalFileBytes = sourceFiles.reduce( + (sum, row) => (row.workspaceId ? sum + row.size : sum), + 0 + ) + if (totalFileBytes > 0) { + const quotaCheck = await checkStorageQuota(userId, totalFileBytes) + if (!quotaCheck.allowed) { + return createBadRequestResponse(quotaCheck.error || 'Storage limit exceeded') + } + } + + // Resources are stored as a jsonb array on the chat row. They carry no + // timestamps, so they can't be timeline-cut like messages — instead, + // file resources whose chat-owned file is NOT copied (uploads born + // after the cut) are dropped in the rewrite below; everything else is + // copied. const parentResources = Array.isArray(parent.resources) ? (parent.resources as MothershipResource[]) : [] + // The source chat's chat-owned file ids (no cut) — the "is this + // resource a ghost?" test set for the rewrite. + const chatOwnedFileIds = new Set(chatOwnedFiles.map((row) => row.id)) + const newId = generateId() + // Strip a leading "Fork | " so titles don't stack prefixes when forking + // a forked chat. const baseTitle = (parent.title ?? 'New chat').replace(/^Fork \| /, '') const title = `Fork | ${baseTitle}` const now = new Date() - const newChat = await db.transaction(async (tx) => { + const result = await db.transaction(async (tx) => { const [row] = await tx .insert(copilotChats) .values({ @@ -114,16 +167,85 @@ export const POST = withRouteHandler( if (!row) return null - await appendCopilotChatMessages(newId, forkedMessages, { chatModel: parent.model }, tx) - return row + // File rows FK the new chat row, so the plan runs after the insert. + const { idMap, keyMap, blobTasks } = await planChatFileCopies({ + tx, + rows: sourceFiles, + newChatId: newId, + userId, + now, + }) + + const maps = { fileIds: idMap, fileKeys: keyMap } + const newChatResources = rewriteResourceFileRefs(parentResources, maps, chatOwnedFileIds) + // Skip the redundant update only when the rewrite changed nothing: + // no ids re-pointed AND no ghost resources dropped. (idMap and keyMap + // are populated in lockstep, so idMap alone decides the first half.) + if (idMap.size > 0 || newChatResources.length !== parentResources.length) { + await tx + .update(copilotChats) + .set({ resources: newChatResources }) + .where(eq(copilotChats.id, newId)) + } + + await appendCopilotChatMessages( + newId, + rewriteMessageFileRefs(forkedMessages, maps), + { chatModel: parent.model }, + tx + ) + return { row, blobTasks } }) - if (!newChat) { + if (!result) { return createInternalServerErrorResponse('Failed to create forked chat') } + const newChat = result.row + + const { copied, failed, failedCopyIds } = await executeChatFileBlobCopies(result.blobTasks, { + userId, + workspaceId: parent.workspaceId ?? undefined, + }) + if (failed > 0) { + // A failed blob copy leaves a committed row with no bytes behind it. + // Cleanly absent beats present-but-broken: hard-delete the dead rows + // (they vanish from the VFS listings and name resolution) and drop + // their resource chips from the new chat. Inline transcript embeds + // can't be healed — those 404 — which is what `failedFileCopies` in + // the response warns the user about. + try { + await db.delete(workspaceFiles).where(inArray(workspaceFiles.id, failedCopyIds)) + await removeChatResources( + newId, + failedCopyIds.map((id) => ({ type: 'file' as const, id, title: '' })) + ) + } catch (cleanupError) { + logger.error('Failed to clean up dead file rows after blob-copy failure', { + newChatId: newId, + failedCopyIds, + error: cleanupError, + }) + } + logger.warn('Some chat file blobs failed to copy during fork', { + chatId, + newChatId: newId, + copied, + failed, + }) + } // Clone copilot-service conversation state (messages, active_messages, memory files). // Best-effort: if the copilot service doesn't have a row for the source chat yet, skip. + // The service stamps MessageID only on USER messages (assistant rows carry + // Sim-local ids it has never seen), so hand it the kept slice's last user + // message — it clones through the end of that turn, matching this route's cut. + let goCutMessageId = upToMessageId + for (let i = forkedMessages.length - 1; i >= 0; i--) { + if (forkedMessages[i].role === 'user') { + goCutMessageId = forkedMessages[i].id + break + } + } try { const copilotHeaders: Record = { 'Content-Type': 'application/json' } if (env.COPILOT_API_KEY) { @@ -137,7 +259,7 @@ export const POST = withRouteHandler( body: JSON.stringify({ sourceChatId: chatId, newChatId: newId, - upToMessageId, + upToMessageId: goCutMessageId, userId, }), spanName: 'sim → go /api/chats/fork', @@ -168,7 +290,11 @@ export const POST = withRouteHandler( { groups: { workspace: parent.workspaceId ?? '' } } ) - return NextResponse.json({ success: true, id: newId }) + return NextResponse.json({ + success: true, + id: newId, + ...(failed > 0 ? { failedFileCopies: failed } : {}), + }) } catch (error) { if (isWorkspaceAccessDeniedError(error)) { return createForbiddenResponse('Workspace access denied') diff --git a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx index fa55168db32..bb9f2618984 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx @@ -18,6 +18,7 @@ import { getErrorMessage } from '@sim/utils/errors' import { useSession } from '@/lib/auth/auth-client' import type { OAuthReturnContext } from '@/lib/credentials/client-state' import { ADD_CONNECTOR_SEARCH_PARAM, writeOAuthReturnContext } from '@/lib/credentials/client-state' +import { defaultCredentialDisplayName } from '@/lib/credentials/display-name' import { getProviderIdFromServiceId, OAUTH_PROVIDERS, @@ -30,18 +31,6 @@ import { useConnectOAuthService } from '@/hooks/queries/oauth/oauth-connections' const logger = createLogger('ConnectOAuthModal') -/** Server-enforced max for `WorkspaceCredential.displayName` — see `lib/api/contracts/credentials.ts`. */ -const DISPLAY_NAME_MAX_LENGTH = 255 - -/** - * Reserved tail budget when truncating the username so the auto-numbering - * disambiguator (e.g. `" 9999"`) always fits within {@link DISPLAY_NAME_MAX_LENGTH}. - */ -const COLLISION_SUFFIX_RESERVATION = 5 - -/** Upper bound for the auto-numbering search — pathological if ever reached. */ -const MAX_COLLISION_INDEX = 10000 - const EMPTY_SCOPES: readonly string[] = [] type ServiceIcon = ComponentType<{ className?: string }> @@ -51,44 +40,6 @@ function isHiddenScope(scope: string): boolean { return scope.includes('userinfo.email') || scope.includes('userinfo.profile') } -/** - * Default credential display name. Produces `"{Name}'s {Service}"` when the - * user's name is known, falling back to `"My {Service}"` otherwise. The - * username is truncated so the full string (including any auto-numbering - * disambiguator) stays within {@link DISPLAY_NAME_MAX_LENGTH}. - * - * When the base name collides with an existing credential in `takenNames`, - * `" 2"`, `" 3"`, ... are appended until an unused name is found. Comparison - * is case-insensitive to match the duplicate-detection used elsewhere in the - * modal. - */ -function defaultDisplayName( - userName: string | null | undefined, - serviceName: string, - takenNames: ReadonlySet -): string { - const trimmed = userName?.trim() - let base: string - if (trimmed) { - const suffix = `'s ${serviceName}` - const nameBudget = Math.max( - 0, - DISPLAY_NAME_MAX_LENGTH - suffix.length - COLLISION_SUFFIX_RESERVATION - ) - const safeName = trimmed.length > nameBudget ? trimmed.slice(0, nameBudget) : trimmed - base = `${safeName}${suffix}` - } else { - base = `My ${serviceName}` - } - - if (!takenNames.has(base.toLowerCase())) return base - for (let n = 2; n < MAX_COLLISION_INDEX; n++) { - const candidate = `${base} ${n}` - if (!takenNames.has(candidate.toLowerCase())) return candidate - } - return base -} - /** * Resolves the display name + icon for an OAuth `provider`/`serviceId` pair, * preferring the most specific service entry and falling back to the base @@ -255,7 +206,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) { } if (!isConnect || prefilled.current || credentialsLoading) return prefilled.current = true - setDisplayName(defaultDisplayName(userName, providerName, takenNames)) + setDisplayName(defaultCredentialDisplayName(userName, providerName, takenNames)) setDescription('') setValidationError(null) setSubmitError(null) diff --git a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx index 91ebc37ea42..b2b4e22d1c6 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx @@ -10,19 +10,20 @@ import { ChipModalHeader, cn, Duplicate, + Split, ThumbsDown, ThumbsUp, Tooltip, toast, } from '@sim/emcn' -import { GitBranch } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' +import { isLiveAssistantMessageId } from '@/lib/copilot/chat/effective-transcript' import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context' import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback' import { useForkMothershipChat } from '@/hooks/queries/mothership-chats' import { useFolderStore } from '@/stores/folders/store' -const SPECIAL_TAGS = 'thinking|options|usage_upgrade|credential|mothership-error|file' +const SPECIAL_TAGS = 'thinking|options|usage_upgrade|credential|mothership-error|file|question' function toPlainText(raw: string): string { return ( @@ -153,6 +154,11 @@ export const MessageActions = memo(function MessageActions({ if (!chatId || !messageId || forkChat.isPending) return try { const result = await forkChat.mutateAsync({ chatId, upToMessageId: messageId }) + if (result.failedFileCopies) { + toast.warning( + `${result.failedFileCopies} file${result.failedFileCopies === 1 ? '' : 's'} could not be copied to the fork` + ) + } useFolderStore.getState().clearChatSelection() router.push(`/workspace/${params.workspaceId}/chat/${result.id}`) } catch { @@ -162,7 +168,10 @@ export const MessageActions = memo(function MessageActions({ const hasContent = Boolean(content) const canSubmitFeedback = Boolean(chatId && userQuery) - const canFork = false + // A live (just-streamed) assistant message carries a synthetic id that the + // persisted transcript doesn't know — forking it would 400. The button + // appears once the transcript refetch swaps in the persisted message id. + const canFork = Boolean(chatId && messageId && !isLiveAssistantMessageId(messageId)) if (!hasContent && !canSubmitFeedback && !canFork) return null return ( @@ -220,15 +229,15 @@ export const MessageActions = memo(function MessageActions({ - Fork from here + Branch in new chat )} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx index 1283dc8617e..bb898bd9920 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx @@ -304,6 +304,8 @@ const MARKDOWN_COMPONENTS = { interface ChatContentProps { content: string isStreaming?: boolean + /** Transcript-derived answers for this message's question card (renders the recap). */ + questionAnswers?: string[] onOptionSelect?: (id: string) => void onWorkspaceResourceSelect?: (resource: MothershipResource) => void onRevealStateChange?: (isRevealing: boolean) => void @@ -312,6 +314,7 @@ interface ChatContentProps { function ChatContentInner({ content, isStreaming = false, + questionAnswers, onOptionSelect, onWorkspaceResourceSelect, onRevealStateChange, @@ -513,6 +516,7 @@ function ChatContentInner({ ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts index f211a5f42e5..8151e32f11c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts @@ -2,4 +2,5 @@ export type { AgentGroupItem, NestedAgentGroup } from './agent-group' export { AgentGroup, CircleStop, isAgentGroupResolved } from './agent-group' export { ChatContent } from './chat-content' export { Options } from './options' +export { QuestionDisplay } from './question' export { PendingTagIndicator, parseSpecialTags, SpecialTags } from './special-tags' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/index.ts new file mode 100644 index 00000000000..5272577cfda --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/index.ts @@ -0,0 +1,5 @@ +export { + formatQuestionAnswerMessage, + parseQuestionAnswerMessage, + QuestionDisplay, +} from './question' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.test.ts new file mode 100644 index 00000000000..f3d7b9528ea --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.test.ts @@ -0,0 +1,86 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + formatQuestionAnswerMessage, + parseQuestionAnswerMessage, +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/question/question' +import type { QuestionItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' + +const QUESTIONS: QuestionItem[] = [ + { + type: 'single_select', + prompt: 'How should I handle the duplicates?', + options: [{ id: 'keep_newest', label: 'Keep the newest entry' }], + }, + { + type: 'single_select', + prompt: 'Delete 4 archived workflows?', + options: [ + { id: 'yes', label: 'Delete them' }, + { id: 'no', label: 'Cancel' }, + ], + }, + { + type: 'multi_select', + prompt: 'What time zone should the daily report run in?', + options: [ + { id: 'est', label: 'EST' }, + { id: 'pst', label: 'PST' }, + ], + }, +] + +describe('formatQuestionAnswerMessage', () => { + it('sends a prompt-answer line for a single question', () => { + expect(formatQuestionAnswerMessage([QUESTIONS[0]], ['Keep the newest entry'])).toBe( + 'How should I handle the duplicates? — Keep the newest entry' + ) + }) + + it('sends one prompt-answer line per question for multi-step batches', () => { + expect(formatQuestionAnswerMessage(QUESTIONS, ['Keep the newest entry', 'Cancel', 'EST'])).toBe( + 'How should I handle the duplicates? — Keep the newest entry\n' + + 'Delete 4 archived workflows? — Cancel\n' + + 'What time zone should the daily report run in? — EST' + ) + }) +}) + +describe('parseQuestionAnswerMessage', () => { + it('round-trips what formatQuestionAnswerMessage produces', () => { + const answers = ['Keep the newest entry', 'Cancel', 'EST, PST'] + const message = formatQuestionAnswerMessage(QUESTIONS, answers) + expect(parseQuestionAnswerMessage(QUESTIONS, message)).toEqual(answers) + }) + + it('round-trips a single question', () => { + const message = formatQuestionAnswerMessage([QUESTIONS[0]], ['Merge them']) + expect(parseQuestionAnswerMessage([QUESTIONS[0]], message)).toEqual(['Merge them']) + }) + + it('rejects an unrelated user message (dismissed card, typed something else)', () => { + expect(parseQuestionAnswerMessage([QUESTIONS[0]], 'actually, show me the logs')).toBeNull() + }) + + it('rejects when the line count does not match the question count', () => { + const partial = formatQuestionAnswerMessage(QUESTIONS.slice(0, 2), ['A', 'B']) + expect(parseQuestionAnswerMessage(QUESTIONS, partial)).toBeNull() + }) + + it('rejects when a line pairs with the wrong prompt', () => { + const swapped = + 'Delete 4 archived workflows? — Cancel\n' + + 'How should I handle the duplicates? — Keep the newest entry\n' + + 'What time zone should the daily report run in? — EST' + expect(parseQuestionAnswerMessage(QUESTIONS, swapped)).toBeNull() + }) + + it('preserves em-dashes inside the answer text', () => { + const message = formatQuestionAnswerMessage([QUESTIONS[0]], ['newest — but keep backups']) + expect(parseQuestionAnswerMessage([QUESTIONS[0]], message)).toEqual([ + 'newest — but keep backups', + ]) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx new file mode 100644 index 00000000000..2381993aa05 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx @@ -0,0 +1,477 @@ +'use client' + +import { useState } from 'react' +import { + ArrowRight, + Button, + Check, + ChevronLeft, + ChevronRight, + checkboxIconVariants, + checkboxVariants, + cn, + X, +} from '@sim/emcn' +import type { QuestionItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' + +/** + * Builds the single user message sent after the final question is answered: + * one `Prompt — Answer` line per question, for lone questions too. The uniform + * shape is what lets the chat pair this message back to its question card + * (see parseQuestionAnswerMessage) and render the card as the user turn + * instead of echoing a duplicate bubble. + */ +export function formatQuestionAnswerMessage(questions: QuestionItem[], answers: string[]): string { + return questions.map((q, i) => `${q.prompt} — ${answers[i] ?? ''}`).join('\n') +} + +/** + * Strictly matches a user message against a question batch's answer format: + * exactly one `Prompt — Answer` line per question, in order. Returns the + * answers, or null when the message is not this batch's answer — a dismissed + * card followed by an unrelated typed message must not match. + */ +export function parseQuestionAnswerMessage( + questions: QuestionItem[], + content: string +): string[] | null { + const lines = content.split('\n') + if (lines.length !== questions.length) return null + const answers: string[] = [] + for (const [i, question] of questions.entries()) { + const prefix = `${question.prompt} — ` + if (!lines[i].startsWith(prefix)) return null + answers.push(lines[i].slice(prefix.length)) + } + return answers +} + +const OPTION_ROW_CLASSES = + 'flex items-center gap-2 border-[var(--divider)] px-2 py-2 text-left transition-colors' + +/** Ghost icon-button chrome shared by the stepper chevrons and the dismiss X. */ +const ICON_BUTTON_CLASSES = 'relative size-[14px] flex-shrink-0 p-0' + +/** Leading number slot matching the suggested follow-ups rows. */ +function RowNumber({ value }: { value: number }) { + return ( +
+ {value} +
+ ) +} + +/** + * Leading checkbox slot for multi_select rows. Purely presentational — it + * reuses the emcn Checkbox chrome via its exported variants, but the row + * button (or the free-text input) owns the interaction, so nesting a real + * Radix checkbox button inside the row button is avoided. + */ +function RowCheckbox({ checked, disabled }: { checked: boolean; disabled?: boolean }) { + return ( +
+ + {checked && ( + + )} + +
+ ) +} + +type QuestionPhase = 'active' | 'answered' | 'dismissed' + +interface QuestionDisplayProps { + data: QuestionItem[] + /** + * Answers resolved from the transcript (the paired user message that + * answered this card). When present the card renders as the answered recap + * — it IS the user turn; the paired message bubble is hidden by the chat. + */ + answers?: string[] + /** Sends the combined answer as a user message; undefined renders the div inert. */ + onSelect?: (message: string) => void +} + +/** + * Inline renderer for the `` special tag: a chat-inline div with the + * user input's chrome, the current question's prompt at the top left, dismiss + * (and a `‹ N of M ›` stepper for multi-step batches) at the top right, and + * suggested-action option rows beneath, always followed by a "Something else" + * row that reads as a plain option until clicked and then becomes the focused + * text box. `single_select` answers and advances on click (or on submitting + * typed text); `multi_select` rows toggle checkboxes and an option-styled + * Submit row confirms the step. Answering the last question sends one + * combined user message and collapses the div to a question/answer recap. + */ +export function QuestionDisplay({ + data, + answers: transcriptAnswers, + onSelect, +}: QuestionDisplayProps) { + const disabled = !onSelect + const [phase, setPhase] = useState('active') + const [step, setStep] = useState(0) + const [selectedByStep, setSelectedByStep] = useState(() => data.map(() => [])) + const [customByStep, setCustomByStep] = useState(() => data.map(() => '')) + const [freeText, setFreeText] = useState('') + // The "Something else" row reads as a plain option until clicked, then + // becomes the focused text box (and reverts when left empty). + const [freeTextEditing, setFreeTextEditing] = useState(false) + // multi_select only: whether the typed "Something else" text is included in + // the answer. Unchecking keeps the text; it just stops counting. + const [customCheckedByStep, setCustomCheckedByStep] = useState(() => + data.map(() => false) + ) + + // The typed text that actually joins a step's answer: multi_select customs + // only count while checked; single_select customs always count. + const customFor = (i: number, customs: string[]): string => + data[i].type === 'multi_select' && !(customCheckedByStep[i] ?? false) ? '' : (customs[i] ?? '') + + const containerClasses = + 'rounded-2xl border border-[var(--border-1)] bg-[var(--white)] px-2.5 py-2 dark:bg-[var(--surface-4)]' + + // Transcript answers win over local state: they survive reloads (local + // phase does not) and keep live + rehydrated renders identical. + const localAnswers = + phase === 'answered' + ? data.map((question, i) => + answerFor(question, selectedByStep[i] ?? [], customFor(i, customByStep)) + ) + : null + const recapAnswers = transcriptAnswers ?? localAnswers + if (data.length > 0 && recapAnswers) { + return ( +
+ {data.map((question, i) => ( +
+

{question.prompt}

+

{recapAnswers[i]}

+
+ ))} +
+ ) + } + + if (data.length === 0 || phase === 'dismissed') return null + + const question = data[step] + const isLast = step === data.length - 1 + const options = question.options + const selected = selectedByStep[step] ?? [] + const isMulti = question.type === 'multi_select' + + const commitCustom = (): string[] => { + const next = [...customByStep] + next[step] = freeText.trim() + setCustomByStep(next) + return next + } + + const goToStep = (next: number) => { + commitCustom() + setStep(next) + const prefill = customByStep[next] ?? '' + setFreeText(prefill) + setFreeTextEditing(prefill.trim().length > 0) + } + + const finishStep = (selections: string[][], customs: string[]) => { + if (!isLast) { + setStep(step + 1) + const prefill = customs[step + 1] ?? '' + setFreeText(prefill) + setFreeTextEditing(prefill.trim().length > 0) + return + } + setPhase('answered') + onSelect?.( + formatQuestionAnswerMessage( + data, + data.map((q, i) => answerFor(q, selections[i] ?? [], customFor(i, customs))) + ) + ) + } + + const handleSingleSelect = (label: string) => { + const selections = [...selectedByStep] + selections[step] = [label] + setSelectedByStep(selections) + const customs = [...customByStep] + customs[step] = '' + setCustomByStep(customs) + setFreeText('') + finishStep(selections, customs) + } + + const handleMultiToggle = (label: string) => { + const selections = [...selectedByStep] + const current = selections[step] ?? [] + selections[step] = current.includes(label) + ? current.filter((l) => l !== label) + : [...current, label] + setSelectedByStep(selections) + } + + /** multi_select confirm: commits selections and/or typed text, then advances. */ + const submitMultiStep = () => { + finishStep(selectedByStep, commitCustom()) + } + + /** Sets whether the typed "Something else" text counts — never touches the text. */ + const setCustomChecked = (checked: boolean) => { + const next = [...customCheckedByStep] + next[step] = checked + setCustomCheckedByStep(next) + } + + /** single_select free-text arrow: the typed text IS the answer. */ + const submitSingleFreeText = () => { + const customs = commitCustom() + const selections = [...selectedByStep] + selections[step] = [] + setSelectedByStep(selections) + finishStep(selections, customs) + } + + const stepAnswered = (i: number): boolean => { + if ((selectedByStep[i]?.length ?? 0) > 0) return true + const text = i === step ? freeText : (customByStep[i] ?? '') + if (text.trim().length === 0) return false + return data[i].type === 'multi_select' ? (customCheckedByStep[i] ?? false) : true + } + + const canSubmitStep = !disabled && (isMulti ? stepAnswered(step) : freeText.trim().length > 0) + + return ( +
+
+

+ {question.prompt} +

+
+ {data.length > 1 && ( +
+ + + {step + 1} of {data.length} + + +
+ )} + {!disabled && ( + + )} +
+
+
+ {options.map((option, i) => { + const isSelected = selected.includes(option.label) + return ( + + ) + })} + {freeTextEditing ? ( +
0 && 'border-t')}> + {isMulti ? ( + // Checked from the moment the row is clicked into; blur with + // nothing typed reverts to the plain option row. A real button + // (the editing row is a div, so no nesting hazard) so the box + // can be toggled even after typing — unchecking keeps the text, + // it just stops counting toward the answer. +
+ +
+ ) : ( + + )} + setFreeText(e.target.value)} + onBlur={() => { + if (freeText.trim().length === 0) { + setFreeTextEditing(false) + if (isMulti) setCustomChecked(false) + } + }} + onKeyDown={(e) => { + if (e.key === 'Escape') { + e.currentTarget.blur() + return + } + if (e.key === 'Enter' && canSubmitStep) { + e.preventDefault() + if (isMulti) { + submitMultiStep() + } else { + submitSingleFreeText() + } + } + }} + aria-label={question.prompt} + className='min-w-0 flex-1 border-0 bg-transparent p-0 text-[var(--text-body)] text-sm outline-none disabled:cursor-not-allowed' + /> + {!isMulti && ( + + )} +
+ ) : ( + + )} + {isMulti && ( + + )} +
+
+ ) +} + +/** + * A step's combined answer: selected option labels in option order, with the + * typed "Something else" entry appended last. single_select carries at most + * one selection, so this collapses to the chosen label or the typed text. + */ +function answerFor(question: QuestionItem, selected: string[], custom: string): string { + const ordered = question.options + .map((option) => option.label) + .filter((label) => selected.includes(label)) + const parts = custom.trim() ? [...ordered, custom.trim()] : ordered + return parts.join(', ') +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts index c45b9199c31..16dbf2783e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/index.ts @@ -6,6 +6,10 @@ export type { MothershipErrorTagData, OptionsTagData, ParsedSpecialContent, + QuestionItem, + QuestionOption, + QuestionTagData, + QuestionType, RuntimeSpecialTagName, UsageUpgradeAction, UsageUpgradeTagData, @@ -17,9 +21,12 @@ export { PendingTagIndicator, parseFileTag, parseJsonTagBody, + parseLastQuestionTag, + parseQuestionTagBody, parseSpecialTags, parseTagAttributes, parseTextTagBody, + QUESTION_TYPES, SpecialTags, USAGE_UPGRADE_ACTIONS, WORKSPACE_RESOURCE_TAG_TYPES, diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts new file mode 100644 index 00000000000..638e3c3a747 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -0,0 +1,157 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + parseQuestionTagBody, + parseSpecialTags, +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' + +const SINGLE_SELECT = { + type: 'single_select', + prompt: 'How should I handle the duplicate emails?', + options: [ + { id: 'keep_newest', label: 'Keep the newest entry' }, + { id: 'merge', label: 'Merge fields into one row' }, + ], +} + +const YES_NO = { + type: 'single_select', + prompt: 'Delete 4 archived workflows?', + options: [ + { id: 'yes', label: 'Delete them' }, + { id: 'no', label: 'Cancel' }, + ], +} + +const MULTI_SELECT = { + type: 'multi_select', + prompt: 'Which channels should the report go to?', + options: [ + { id: 'slack', label: 'Slack' }, + { id: 'email', label: 'Email' }, + { id: 'sheet', label: 'Google Sheet' }, + ], +} + +describe('parseQuestionTagBody', () => { + it('normalizes a single object body to a one-element array', () => { + expect(parseQuestionTagBody(JSON.stringify(SINGLE_SELECT))).toEqual([SINGLE_SELECT]) + }) + + it('preserves array order for multi-step bodies', () => { + const parsed = parseQuestionTagBody(JSON.stringify([SINGLE_SELECT, YES_NO, MULTI_SELECT])) + expect(parsed).toEqual([SINGLE_SELECT, YES_NO, MULTI_SELECT]) + }) + + it('accepts multi_select questions', () => { + expect(parseQuestionTagBody(JSON.stringify(MULTI_SELECT))).toEqual([MULTI_SELECT]) + }) + + it('rejects single_select without options', () => { + expect(parseQuestionTagBody(JSON.stringify({ type: 'single_select', prompt: 'Pick' }))).toBe( + null + ) + }) + + it('rejects empty options', () => { + expect( + parseQuestionTagBody(JSON.stringify({ type: 'single_select', prompt: 'Sure?', options: [] })) + ).toBe(null) + }) + + it('rejects the removed text and confirm types', () => { + expect(parseQuestionTagBody(JSON.stringify({ type: 'text', prompt: 'What time zone?' }))).toBe( + null + ) + expect(parseQuestionTagBody(JSON.stringify({ ...YES_NO, type: 'confirm' }))).toBe(null) + }) + + it('strips agent-supplied catch-all options (the card provides its own)', () => { + const withOther = { + ...SINGLE_SELECT, + options: [...SINGLE_SELECT.options, { id: 'other', label: 'Something else' }], + } + expect(parseQuestionTagBody(JSON.stringify(withOther))).toEqual([SINGLE_SELECT]) + }) + + it('rejects a question whose every option is a catch-all', () => { + const onlyOther = { + type: 'single_select', + prompt: 'Pick one', + options: [ + { id: 'a', label: 'Other' }, + { id: 'b', label: 'None of the above' }, + ], + } + expect(parseQuestionTagBody(JSON.stringify(onlyOther))).toBe(null) + }) + + it('rejects an empty prompt', () => { + expect(parseQuestionTagBody(JSON.stringify({ ...SINGLE_SELECT, prompt: ' ' }))).toBe(null) + }) + + it('rejects a malformed option', () => { + expect( + parseQuestionTagBody(JSON.stringify({ ...SINGLE_SELECT, options: [{ id: 'keep_newest' }] })) + ).toBe(null) + }) + + it('rejects an array containing one invalid question', () => { + expect(parseQuestionTagBody(JSON.stringify([SINGLE_SELECT, { type: 'single_select' }]))).toBe( + null + ) + }) + + it('rejects empty arrays and non-JSON bodies', () => { + expect(parseQuestionTagBody('[]')).toBe(null) + expect(parseQuestionTagBody('not json')).toBe(null) + }) +}) + +describe('parseSpecialTags with ', () => { + it('extracts a complete question tag interleaved with text', () => { + const content = `Before the tag. ${JSON.stringify(SINGLE_SELECT)} After the tag.` + const { segments, hasPendingTag } = parseSpecialTags(content, false) + expect(hasPendingTag).toBe(false) + expect(segments).toEqual([ + { type: 'text', content: 'Before the tag. ' }, + { type: 'question', data: [SINGLE_SELECT] }, + { type: 'text', content: ' After the tag.' }, + ]) + }) + + it('extracts a multi-step array body as one segment', () => { + const content = `${JSON.stringify([SINGLE_SELECT, YES_NO, MULTI_SELECT])}` + const { segments } = parseSpecialTags(content, false) + expect(segments).toEqual([{ type: 'question', data: [SINGLE_SELECT, YES_NO, MULTI_SELECT] }]) + }) + + it('flags an unclosed question tag as pending while streaming', () => { + const { segments, hasPendingTag } = parseSpecialTags( + 'Thinking about it. [{"type":"single_sel', + true + ) + expect(hasPendingTag).toBe(true) + expect(segments).toEqual([{ type: 'text', content: 'Thinking about it. ' }]) + }) + + it('strips a trailing partial opening tag while streaming', () => { + const { segments, hasPendingTag } = parseSpecialTags('Let me ask. { + const { segments, hasPendingTag } = parseSpecialTags( + 'Before. {"type":"single_select"} After.', + false + ) + expect(hasPendingTag).toBe(false) + expect(segments).toEqual([ + { type: 'text', content: 'Before. ' }, + { type: 'text', content: ' After.' }, + ]) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.tsx index 5fa56531a0b..a23bc0427d5 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.tsx @@ -5,8 +5,9 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockUseUserPermissionsContext } = vi.hoisted(() => ({ +const { mockUseUserPermissionsContext, mockUseWorkspaceCredential } = vi.hoisted(() => ({ mockUseUserPermissionsContext: vi.fn(), + mockUseWorkspaceCredential: vi.fn(), })) vi.mock('@/app/workspace/[workspaceId]/providers/workspace-permissions-provider', () => ({ @@ -17,8 +18,15 @@ vi.mock('next/navigation', () => ({ useParams: () => ({ workspaceId: 'workspace-1' }), })) +vi.mock('@/hooks/queries/credentials', () => ({ + useWorkspaceCredential: mockUseWorkspaceCredential, +})) + import type { CredentialTagData } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' -import { SpecialTags } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' +import { + parseSpecialTags, + SpecialTags, +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' /** * Minimal dependency-free render harness (the repo has no `@testing-library/react`). Mounts the @@ -38,6 +46,7 @@ describe('CredentialDisplay link tag', () => { beforeEach(() => { vi.clearAllMocks() mockUseUserPermissionsContext.mockReturnValue({ canEdit: true }) + mockUseWorkspaceCredential.mockReturnValue({ data: null }) }) it('does not render an anchor for a javascript: scheme value', () => { @@ -88,4 +97,64 @@ describe('CredentialDisplay link tag', () => { expect(container.querySelector('a')).toBeNull() act(() => root.unmount()) }) + + it('does not query a credential for a plain connect URL', () => { + const { root } = renderCredentialLink({ + type: 'link', + provider: 'github', + value: 'https://github.com/login/oauth/authorize?client_id=abc', + }) + + expect(mockUseWorkspaceCredential).toHaveBeenCalledWith(undefined) + act(() => root.unmount()) + }) + + it('labels a reconnect URL with the credential display name', () => { + mockUseWorkspaceCredential.mockReturnValue({ + data: { id: 'cred-1', displayName: "Justin's Gmail" }, + }) + const { container, root } = renderCredentialLink({ + type: 'link', + provider: 'google-email', + value: + 'https://sim.test/api/auth/oauth2/authorize?providerId=google-email&workspaceId=ws-1&credentialId=cred-1', + }) + + expect(mockUseWorkspaceCredential).toHaveBeenCalledWith('cred-1') + expect(container.textContent).toContain("Reconnect Justin's Gmail") + act(() => root.unmount()) + }) + + it('falls back to the provider label while the reconnect credential is unresolved', () => { + const { container, root } = renderCredentialLink({ + type: 'link', + provider: 'google-email', + value: + 'https://sim.test/api/auth/oauth2/authorize?providerId=google-email&workspaceId=ws-1&credentialId=cred-1', + }) + + expect(container.textContent).toContain('Reconnect google-email') + act(() => root.unmount()) + }) +}) + +describe('parseSpecialTags sim_key placeholder', () => { + it('accepts a value-less {"type":"sim_key"} tag as a credential segment', () => { + const { segments } = parseSpecialTags('{"type":"sim_key"}', false) + const credential = segments.find((s) => s.type === 'credential') + expect(credential).toEqual({ type: 'credential', data: { type: 'sim_key' } }) + }) + + it('still accepts the legacy {"redacted":true} form as a value-less sim_key placeholder', () => { + const { segments } = parseSpecialTags( + '{"type":"sim_key","redacted":true}', + false + ) + const credential = segments.find((s) => s.type === 'credential') + expect(credential?.type).toBe('credential') + if (credential?.type === 'credential') { + expect(credential.data.type).toBe('sim_key') + expect(credential.data.value).toBeUndefined() + } + }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index b303f7522bb..1068710b81d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -19,11 +19,13 @@ import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils' import { isSafeHttpUrl } from '@/lib/core/utils/urls' import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth' import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon' +import { QuestionDisplay } from '@/app/workspace/[workspaceId]/home/components/message-content/components/question' import type { ChatMessageContext, MothershipResource, } from '@/app/workspace/[workspaceId]/home/types' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { useWorkspaceCredential } from '@/hooks/queries/credentials' import { usePersonalEnvironment, useSavePersonalEnvironment, @@ -74,7 +76,6 @@ export interface CredentialTagData { value?: string type: CredentialTagType provider?: string - redacted?: boolean /** * Env-var key name to save the pasted secret under (secret_input only), * e.g. "OPENAI_API_KEY". @@ -96,6 +97,30 @@ export interface FileTagData { content: string } +export const QUESTION_TYPES = ['single_select', 'multi_select'] as const + +export type QuestionType = (typeof QUESTION_TYPES)[number] + +export interface QuestionOption { + id: string + label: string +} + +/** + * One question in a `` tag: a single_select or multi_select with at + * least one real option. The card always appends its own free-text "Something + * else" row, so agent-supplied catch-all options ("Other", "Something else", + * ...) are stripped during parsing. + */ +export interface QuestionItem { + type: QuestionType + prompt: string + options: QuestionOption[] +} + +/** Normalized `` payload: single-object bodies become a one-element array. */ +export type QuestionTagData = QuestionItem[] + export const WORKSPACE_RESOURCE_TAG_TYPES = ['workflow', 'table', 'file'] as const export type WorkspaceResourceTagType = (typeof WORKSPACE_RESOURCE_TAG_TYPES)[number] @@ -115,6 +140,7 @@ export type ContentSegment = | { type: 'credential'; data: CredentialTagData } | { type: 'mothership-error'; data: MothershipErrorTagData } | { type: 'workspace_resource'; data: WorkspaceResourceTagData } + | { type: 'question'; data: QuestionTagData } export type RuntimeSpecialTagName = | 'thinking' @@ -123,6 +149,7 @@ export type RuntimeSpecialTagName = | 'mothership-error' | 'file' | 'workspace_resource' + | 'question' export interface ParsedSpecialContent { segments: ContentSegment[] @@ -136,6 +163,7 @@ const RUNTIME_SPECIAL_TAG_NAMES = [ 'mothership-error', 'file', 'workspace_resource', + 'question', ] as const const SPECIAL_TAG_NAMES = [ @@ -145,6 +173,7 @@ const SPECIAL_TAG_NAMES = [ 'credential', 'mothership-error', 'workspace_resource', + 'question', ] as const function isRecord(value: unknown): value is Record { @@ -191,7 +220,11 @@ function isCredentialTagData(value: unknown): value is CredentialTagData { } return typeof value.name === 'string' && value.name.trim().length > 0 } - if (value.redacted === true) return value.value === undefined || typeof value.value === 'string' + // A sim_key chip is platform-filled: the model only marks where the workspace + // API key belongs (it never holds the value) and Sim injects it from the tool + // result, so the tag is valid with or without a `value`. Every other rendered + // type (e.g. link) needs a string value to render. + if (value.type === 'sim_key') return true return typeof value.value === 'string' } @@ -222,6 +255,83 @@ function isWorkspaceResourceTagData(value: unknown): value is WorkspaceResourceT return id.length > 0 } +function isQuestionOption(value: unknown): value is QuestionOption { + if (!isRecord(value)) return false + return typeof value.id === 'string' && typeof value.label === 'string' +} + +/** + * Catch-all labels the agent must not supply as options — the card renders + * its own free-text "Something else" row. Matching options are stripped; a + * question left with no real options is invalid. + */ +const SELF_PROVIDED_OPTION_LABELS = new Set([ + 'other', + 'others', + 'something else', + 'none of the above', + 'none of these', +]) + +function isQuestionItem(value: unknown): value is QuestionItem { + if (!isRecord(value)) return false + if ( + typeof value.type !== 'string' || + !(QUESTION_TYPES as readonly string[]).includes(value.type) + ) { + return false + } + if (typeof value.prompt !== 'string' || value.prompt.trim().length === 0) return false + return ( + Array.isArray(value.options) && + value.options.length > 0 && + value.options.every(isQuestionOption) + ) +} + +/** Strips agent-supplied catch-all options; null when none remain. */ +function sanitizeQuestionItem(item: QuestionItem): QuestionItem | null { + const options = item.options.filter( + (option) => !SELF_PROVIDED_OPTION_LABELS.has(option.label.trim().toLowerCase()) + ) + if (options.length === 0) return null + return options.length === item.options.length ? item : { ...item, options } +} + +/** + * Parses a `` tag body. Accepts a single question object or a + * non-empty array of them; single objects are normalized to a one-element + * array so the renderer only handles the array shape. + */ +/** + * Extracts the last complete `` tag payload from raw message + * content. Used by the chat list to pair an assistant question card with the + * user message that answered it. + */ +export function parseLastQuestionTag(content: string): QuestionTagData | null { + const matches = content.match(/([\s\S]*?)<\/question>/g) + if (!matches || matches.length === 0) return null + const last = matches[matches.length - 1] + return parseQuestionTagBody(last.slice(''.length, -''.length)) +} + +export function parseQuestionTagBody(body: string): QuestionTagData | null { + try { + const parsed = JSON.parse(body) as unknown + const items = Array.isArray(parsed) ? parsed : [parsed] + if (items.length === 0 || !items.every(isQuestionItem)) return null + const sanitized: QuestionItem[] = [] + for (const item of items) { + const clean = sanitizeQuestionItem(item) + if (!clean) return null + sanitized.push(clean) + } + return sanitized + } catch { + return null + } +} + export function parseJsonTagBody( body: string, isExpectedShape: (value: unknown) => value is T @@ -270,6 +380,7 @@ function parseSpecialTagData( | { type: 'credential'; data: CredentialTagData } | { type: 'mothership-error'; data: MothershipErrorTagData } | { type: 'workspace_resource'; data: WorkspaceResourceTagData } + | { type: 'question'; data: QuestionTagData } | null { if (tagName === 'thinking') { const content = parseTextTagBody(body) @@ -301,6 +412,11 @@ function parseSpecialTagData( return data ? { type: 'workspace_resource', data } : null } + if (tagName === 'question') { + const data = parseQuestionTagBody(body) + return data ? { type: 'question', data } : null + } + return null } @@ -393,6 +509,8 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS interface SpecialTagsProps { segment: Exclude + /** Transcript-derived answers for this message's question card (renders the recap). */ + questionAnswers?: string[] onOptionSelect?: (id: string) => void onWorkspaceResourceSelect?: (resource: MothershipResource) => void } @@ -403,6 +521,7 @@ interface SpecialTagsProps { */ export function SpecialTags({ segment, + questionAnswers, onOptionSelect, onWorkspaceResourceSelect, }: SpecialTagsProps) { @@ -419,6 +538,10 @@ export function SpecialTags({ return case 'workspace_resource': return + case 'question': + return ( + + ) default: return null } @@ -730,36 +853,58 @@ function SecretInputDisplay({ data }: { data: CredentialTagData }) { ) } -function CredentialDisplay({ data }: { data: CredentialTagData }) { +function CredentialLinkDisplay({ data }: { data: CredentialTagData }) { const { canEdit } = useUserPermissionsContext() + // A connect URL carrying a credentialId re-authorizes that existing + // credential in place (reconnect) rather than creating a new one. + const reconnectCredentialId = useMemo(() => { + if (!data.value) return undefined + try { + return new URL(data.value).searchParams.get('credentialId') ?? undefined + } catch { + return undefined + } + }, [data.value]) + const { data: reconnectCredential } = useWorkspaceCredential(reconnectCredentialId) + + // Connecting a credential mutates the workspace — hide it from read-only members. + if (!data.provider || !canEdit) return null + // The connect link value comes from the streamed model output, so only + // render it as a clickable link when it resolves to a real http(s) URL. + if (!data.value || !isSafeHttpUrl(data.value)) return null + const Icon = getCredentialIcon(data.provider) ?? LockIcon + const label = reconnectCredentialId + ? `Reconnect ${reconnectCredential?.displayName ?? data.provider}` + : `Connect ${data.provider}` + return ( + + {createElement(Icon, { className: 'size-[16px] shrink-0' })} + {label} + + + ) +} + +function CredentialDisplay({ data }: { data: CredentialTagData }) { if (data.type === 'secret_input') { return } if (data.type === 'link') { - // Connecting a credential mutates the workspace — hide it from read-only members. - if (!data.provider || !canEdit) return null - // The connect link value comes from the streamed model output, so only - // render it as a clickable link when it resolves to a real http(s) URL. - if (!data.value || !isSafeHttpUrl(data.value)) return null - const Icon = getCredentialIcon(data.provider) ?? LockIcon - return ( - - {createElement(Icon, { className: 'size-[16px] shrink-0' })} - Connect {data.provider} - - - ) + return } if (data.type === 'sim_key') { - return + // SecretReveal masks itself when there's no value, so a value-less tag (the + // model's placeholder / persisted form) renders masked and a Sim-filled tag + // reveals the key + copy button — no separate "redacted" flag needed. + return } return null @@ -777,7 +922,7 @@ function UsageUpgradeDisplay({ data }: { data: UsageUpgradeTagData }) { const buttonLabel = data.action === 'upgrade_plan' ? 'Upgrade Plan' : 'Increase Limit' return ( -
+
void onPhaseChange?: (phase: MessagePhase) => void } @@ -659,6 +661,7 @@ function MessageContentInner({ blocks, fallbackContent, isStreaming = false, + questionAnswers, onOptionSelect, onPhaseChange, }: MessageContentProps) { @@ -724,6 +727,7 @@ function MessageContentInner({ segmentIndex: i, segmentCount: segments.length, })} + questionAnswers={questionAnswers} onOptionSelect={onOptionSelect} onWorkspaceResourceSelect={onWorkspaceResourceSelect} onRevealStateChange={ diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts index 6a06125c07a..dae69811183 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts @@ -30,6 +30,9 @@ const TOOL_ICONS: Record = { glob: FolderCode, grep: Search, read: File, + mv: FolderCode, + cp: Layout, + mkdir: FolderCode, search_online: Search, scrape_page: Search, get_page_contents: Search, @@ -38,6 +41,7 @@ const TOOL_ICONS: Record = { manage_skill: Asterisk, user_memory: Database, function_execute: TerminalWindow, + run_code: TerminalWindow, superagent: Blimp, user_table: TableIcon, workspace_file: File, @@ -51,12 +55,16 @@ const TOOL_ICONS: Record = { auth: Integration, knowledge: Database, knowledge_base: Database, + search_knowledge_base: Database, table: TableIcon, + query_user_table: TableIcon, scheduled_task: Calendar, job: Calendar, agent: AgentIcon, custom_tool: Wrench, research: Search, + scout: Search, + search: Search, context_compaction: Asterisk, open_resource: Eye, file: File, diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index 841538b2e17..c85ef9aa7db 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -20,7 +20,11 @@ import { MessageContent, type MessagePhase, } from '@/app/workspace/[workspaceId]/home/components/message-content' -import { PendingTagIndicator } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' +import { parseQuestionAnswerMessage } from '@/app/workspace/[workspaceId]/home/components/message-content/components/question' +import { + PendingTagIndicator, + parseLastQuestionTag, +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' import { QueuedMessages } from '@/app/workspace/[workspaceId]/home/components/queued-messages' import { UserInput, @@ -163,6 +167,8 @@ interface AssistantMessageRowProps { message: ChatMessage isStreaming: boolean precedingUserContent?: string + /** Transcript-derived answers for this message's question card (renders the recap). */ + questionAnswers?: string[] rowClassName: string onOptionSelect?: (id: string) => void onAnimatingChange?: (animating: boolean) => void @@ -172,6 +178,7 @@ const AssistantMessageRow = memo(function AssistantMessageRow({ message, isStreaming, precedingUserContent, + questionAnswers, rowClassName, onOptionSelect, onAnimatingChange, @@ -197,7 +204,11 @@ const AssistantMessageRow = memo(function AssistantMessageRow({ return null } - const showActions = phase === 'settled' && (message.content || hasAnyBlocks) + // A message that ends with a question card is an input surface, not a + // reactable assistant turn: no copy/thumbs row beneath the card, whether + // the card is awaiting answers or collapsed to its recap. + const endsWithQuestion = trimmedContent.endsWith('') + const showActions = phase === 'settled' && !endsWithQuestion && (message.content || hasAnyBlocks) return (
@@ -205,6 +216,7 @@ const AssistantMessageRow = memo(function AssistantMessageRow({ blocks={blocks} fallbackContent={message.content} isStreaming={isStreaming} + questionAnswers={questionAnswers} onOptionSelect={onOptionSelect} onPhaseChange={setPhase} /> @@ -303,6 +315,29 @@ export function MothershipChat({ return out }, [messages]) + /** + * Pairs each assistant question card with the user message that answered it + * (strict `Prompt — Answer` match). The paired user message is hidden — the + * answered card IS the user turn — and the assistant row renders the card + * as a recap with these answers, both live and after reload. + */ + const questionPairing = useMemo(() => { + const answersByIndex: Array = [] + const hiddenUserByIndex: Array = [] + for (const [index, message] of messages.entries()) { + if (message.role !== 'assistant' || !message.content?.includes('')) continue + const next = messages[index + 1] + if (!next || next.role !== 'user' || !next.content) continue + const questions = parseLastQuestionTag(message.content) + if (!questions) continue + const answers = parseQuestionAnswerMessage(questions, next.content) + if (!answers) continue + answersByIndex[index] = answers + hiddenUserByIndex[index + 1] = true + } + return { answersByIndex, hiddenUserByIndex } + }, [messages]) + /** * Always keep the last row in the rendered window. It is the live/streaming * row; unmounting it (by scrolling far enough up that it leaves the overscan @@ -424,19 +459,22 @@ export function MothershipChat({ style={{ transform: `translateY(${virtualItem.start}px)` }} > {msg.role === 'user' ? ( - + questionPairing.hiddenUserByIndex[index] ? null : ( + + ) ) : ( = new Set([ Redeploy.id, ]) -export const FOLDER_TOOL_NAMES: Set = new Set([ManageFolder.id]) +export const FOLDER_TOOL_NAMES: Set = new Set([ManageFolder.id, 'mkdir', 'mv']) export const WORKFLOW_MUTATION_TOOL_NAMES: Set = new Set([ - MoveWorkflow.id, - RenameWorkflow.id, + 'mv', + 'cp', DeleteWorkflow.id, + // Removed legacy tools, kept while their grace-period executors remain. + 'move_workflow', + 'rename_workflow', ]) export type StreamPayload = Record @@ -289,6 +290,30 @@ export function resolveStreamingToolDisplayTitle( return toolTitle ? `Finding ${toolTitle}` : undefined } + if (name === 'mv') { + const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle') + if (!toolTitle) return undefined + // Same rename-vs-move derivation as the settled title: single source with + // only the leaf changing reads as a rename. + const multiSource = /"sources"\s*:\s*\[\s*"[^"]*"\s*,/.test(streamingArgs) + const firstSource = streamingArgs.match(/"sources"\s*:\s*\[\s*"([^"]*)"/m)?.[1] + const destination = matchStreamingStringArg(streamingArgs, 'destination') + const verb = multiSource + ? 'Moving' + : mvDisplayVerb(firstSource ? decodeStreamingString(firstSource) : undefined, destination) + return `${verb} ${toolTitle}` + } + + if (name === 'cp') { + const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle') + return toolTitle ? `Duplicating ${toolTitle}` : undefined + } + + if (name === 'mkdir') { + const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle') + return toolTitle ? `Creating ${toolTitle}` : undefined + } + if (name === ScrapePage.id) { const url = matchStreamingStringArg(streamingArgs, 'url') return url ? `Scraping ${url}` : undefined @@ -364,12 +389,15 @@ export function resolveStreamingToolDisplayTitle( } if (name === ManageFolder.id) { + // create/rename/move are string literals: the live tool only offers delete + // (mkdir/mv replaced the rest), but grace-period checkpoint resumes and + // transcript replays still stream the legacy operations. return resolveOperationDisplayTitle( matchStreamingStringArg(streamingArgs, 'operation'), { - [ManageFolderOperation.create]: 'Creating folder', - [ManageFolderOperation.rename]: 'Renaming folder', - [ManageFolderOperation.move]: 'Moving folder', + create: 'Creating folder', + rename: 'Renaming folder', + move: 'Moving folder', [ManageFolderOperation.delete]: 'Deleting folder', }, 'Folder action' diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index 20bc2513dab..2fb75a58b94 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -157,6 +157,8 @@ export const SUBAGENT_LABELS: Record = { knowledge: 'Knowledge Agent', table: 'Table Agent', custom_tool: 'Custom Tool Agent', + scout: 'Scout Agent', + search: 'Search Agent', superagent: 'Superagent', run: 'Run Agent', agent: 'Tools Agent', diff --git a/apps/sim/blocks/registry-maps.minimal.ts b/apps/sim/blocks/registry-maps.minimal.ts index 585da915b1c..454d23fabb0 100644 --- a/apps/sim/blocks/registry-maps.minimal.ts +++ b/apps/sim/blocks/registry-maps.minimal.ts @@ -9,6 +9,7 @@ import { EvaluatorBlock } from '@/blocks/blocks/evaluator' import { FileV5Block } from '@/blocks/blocks/file' import { FunctionBlock } from '@/blocks/blocks/function' import { GenericWebhookBlock } from '@/blocks/blocks/generic_webhook' +import { GmailV2Block } from '@/blocks/blocks/gmail' import { GuardrailsBlock } from '@/blocks/blocks/guardrails' import { HumanInTheLoopBlock } from '@/blocks/blocks/human_in_the_loop' import { ImapBlock } from '@/blocks/blocks/imap' @@ -23,6 +24,7 @@ import { RssBlock } from '@/blocks/blocks/rss' import { ScheduleBlock } from '@/blocks/blocks/schedule' import { SearchBlock } from '@/blocks/blocks/search' import { SimWorkspaceEventBlock } from '@/blocks/blocks/sim_workspace_event' +import { SlackBlock } from '@/blocks/blocks/slack' import { StartTriggerBlock } from '@/blocks/blocks/start_trigger' import { TableBlock } from '@/blocks/blocks/table' import { TranslateBlock } from '@/blocks/blocks/translate' @@ -39,7 +41,9 @@ import type { BlockConfig, BlockMeta } from '@/blocks/types' * ~268. The set is drawn from the canonical, toolbar-visible blocks * (`category: 'blocks'` / `'triggers'`, not `hideFromToolbar`, always the latest * version — never a superseded one). The ~247 `category: 'tools'` integrations - * are excluded (that is the heavy graph minimal mode exists to skip), and so are + * are excluded (that is the heavy graph minimal mode exists to skip) — except + * `slack` and `gmail_v2`, kept as the two everyday integrations (their tools are + * likewise included in `tools/registry.minimal.ts`) — and so are * a few heavy or rarely-core-dev blocks pruned by hand: `mothership` and `pi` * (chunkiest configs), the media blocks `tts` / `stt_v2` / `image_generator_v2` * / `video_generator_v3`, and the `circleback` meeting-notetaker integration @@ -58,6 +62,7 @@ export const BLOCK_REGISTRY: Record = { file_v5: FileV5Block, function: FunctionBlock, generic_webhook: GenericWebhookBlock, + gmail_v2: GmailV2Block, guardrails: GuardrailsBlock, human_in_the_loop: HumanInTheLoopBlock, imap: ImapBlock, @@ -72,6 +77,7 @@ export const BLOCK_REGISTRY: Record = { schedule: ScheduleBlock, search: SearchBlock, sim_workspace_event: SimWorkspaceEventBlock, + slack: SlackBlock, start_trigger: StartTriggerBlock, table: TableBlock, translate: TranslateBlock, diff --git a/apps/sim/hooks/queries/mothership-chats.ts b/apps/sim/hooks/queries/mothership-chats.ts index 1d802c13d93..606dc0020e3 100644 --- a/apps/sim/hooks/queries/mothership-chats.ts +++ b/apps/sim/hooks/queries/mothership-chats.ts @@ -675,12 +675,12 @@ export function useCreateMothershipChat(workspaceId?: string) { async function forkChat(params: { chatId: string upToMessageId: string -}): Promise<{ id: string }> { +}): Promise<{ id: string; failedFileCopies?: number }> { const data = await requestJson(forkMothershipChatContract, { params: { chatId: params.chatId }, body: { upToMessageId: params.upToMessageId }, }) - return { id: data.id } + return { id: data.id, failedFileCopies: data.failedFileCopies } } export function useForkMothershipChat(workspaceId?: string) { diff --git a/apps/sim/lib/api/contracts/mothership-chats.ts b/apps/sim/lib/api/contracts/mothership-chats.ts index 0165c512bd3..99bff9146c5 100644 --- a/apps/sim/lib/api/contracts/mothership-chats.ts +++ b/apps/sim/lib/api/contracts/mothership-chats.ts @@ -276,6 +276,13 @@ export const forkMothershipChatContract = defineRouteContract({ schema: z.object({ success: z.literal(true), id: z.string(), + /** + * Present (and > 0) when some file blobs could not be byte-copied: the + * new chat exists and its transcript references those copies, but their + * bytes are missing (blob copies are best-effort, post-transaction). + * Callers should surface a warning. + */ + failedFileCopies: z.number().optional(), }), }, }) diff --git a/apps/sim/lib/api/contracts/oauth-connections.ts b/apps/sim/lib/api/contracts/oauth-connections.ts index f451f555c1f..4fd6e09a0b8 100644 --- a/apps/sim/lib/api/contracts/oauth-connections.ts +++ b/apps/sim/lib/api/contracts/oauth-connections.ts @@ -195,6 +195,7 @@ export const authorizeOAuth2QuerySchema = z.object({ providerId: z.string().min(1, 'providerId is required'), workspaceId: workspaceIdSchema, callbackURL: z.string().min(1).optional(), + credentialId: z.string().min(1).optional(), }) export const authorizeOAuth2Contract = defineRouteContract({ diff --git a/apps/sim/lib/copilot/chat/effective-transcript.test.ts b/apps/sim/lib/copilot/chat/effective-transcript.test.ts index 285743d37ac..8f69d4fd581 100644 --- a/apps/sim/lib/copilot/chat/effective-transcript.test.ts +++ b/apps/sim/lib/copilot/chat/effective-transcript.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest' import { buildEffectiveChatTranscript, getLiveAssistantMessageId, + isLiveAssistantMessageId, } from '@/lib/copilot/chat/effective-transcript' import { normalizeMessage } from '@/lib/copilot/chat/persisted-message' import { @@ -261,3 +262,11 @@ describe('buildEffectiveChatTranscript', () => { ) }) }) + +describe('isLiveAssistantMessageId', () => { + it('recognizes the synthetic live-assistant id and nothing else', () => { + expect(isLiveAssistantMessageId(getLiveAssistantMessageId('stream-1'))).toBe(true) + expect(isLiveAssistantMessageId('f620fceb-4e9d-4e7f-ab7f-890a2a823564')).toBe(false) + expect(isLiveAssistantMessageId('')).toBe(false) + }) +}) diff --git a/apps/sim/lib/copilot/chat/effective-transcript.ts b/apps/sim/lib/copilot/chat/effective-transcript.ts index ae971047208..8673a6d27dd 100644 --- a/apps/sim/lib/copilot/chat/effective-transcript.ts +++ b/apps/sim/lib/copilot/chat/effective-transcript.ts @@ -34,6 +34,16 @@ export function getLiveAssistantMessageId(streamId: string): string { return `live-assistant:${streamId}` } +/** + * True for the synthetic id of a streaming/just-streamed assistant message. + * These ids exist only in the client's effective transcript — never in the + * persisted one — so message-scoped server actions (e.g. fork) must not be + * offered until the transcript refetch swaps in the persisted message id. + */ +export function isLiveAssistantMessageId(messageId: string): boolean { + return messageId.startsWith('live-assistant:') +} + function asPayloadRecord(value: unknown): Record | undefined { return isRecordLike(value) ? value : undefined } diff --git a/apps/sim/lib/copilot/chat/fork-chat-files.test.ts b/apps/sim/lib/copilot/chat/fork-chat-files.test.ts new file mode 100644 index 00000000000..5d27ce67475 --- /dev/null +++ b/apps/sim/lib/copilot/chat/fork-chat-files.test.ts @@ -0,0 +1,210 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockGenerateKey, mockDownloadFile, mockUploadFile, mockIncrementStorageUsage } = vi.hoisted( + () => ({ + mockGenerateKey: vi.fn(), + mockDownloadFile: vi.fn(), + mockUploadFile: vi.fn(), + mockIncrementStorageUsage: vi.fn(), + }) +) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ + generateWorkspaceFileKey: mockGenerateKey, +})) + +vi.mock('@/lib/uploads/core/storage-service', () => ({ + downloadFile: mockDownloadFile, + uploadFile: mockUploadFile, +})) + +vi.mock('@/lib/billing/storage', () => ({ + incrementStorageUsage: mockIncrementStorageUsage, +})) + +import { + executeChatFileBlobCopies, + type ForkableChatFileRow, + filterForkableChatFiles, + planChatFileCopies, +} from '@/lib/copilot/chat/fork-chat-files' + +const NOW = new Date('2026-07-08T00:00:00.000Z') + +function makeRow(overrides: Partial = {}): ForkableChatFileRow { + return { + id: 'wf_source', + key: 'workspace/ws-1/1-cat.png', + userId: 'user-1', + workspaceId: 'ws-1', + folderId: null, + context: 'mothership', + chatId: 'chat-1', + messageId: 'msg-1', + originalName: 'cat.png', + displayName: 'cat.png', + contentType: 'image/png', + size: 100, + deletedAt: null, + uploadedAt: new Date('2026-06-01T00:00:00.000Z'), + updatedAt: new Date('2026-06-01T00:00:00.000Z'), + ...overrides, + } as ForkableChatFileRow +} + +describe('planChatFileCopies', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGenerateKey.mockReturnValue('workspace/ws-1/2-cat.png') + }) + + it('copies a row under the fork with a fresh id + key and the SAME message_id', async () => { + const inserted: Array> = [] + const tx = { + insert: () => ({ + values: async (v: Record | Record[]) => { + inserted.push(...(Array.isArray(v) ? v : [v])) + }, + }), + } + + const { idMap, keyMap, blobTasks } = await planChatFileCopies({ + tx: tx as never, + rows: [makeRow()], + newChatId: 'chat-fork', + userId: 'user-1', + now: NOW, + }) + + expect(inserted).toHaveLength(1) + const copy = inserted[0] + expect(copy.id).not.toBe('wf_source') + expect(String(copy.id)).toMatch(/^wf_/) + expect(copy.key).toBe('workspace/ws-1/2-cat.png') + expect(copy.chatId).toBe('chat-fork') + expect(copy.messageId).toBe('msg-1') + expect(copy.displayName).toBe('cat.png') + expect(copy.deletedAt).toBeNull() + + expect(idMap.get('wf_source')).toBe(copy.id) + expect(keyMap.get('workspace/ws-1/1-cat.png')).toBe('workspace/ws-1/2-cat.png') + expect(blobTasks).toEqual([ + { + copyId: copy.id, + sourceKey: 'workspace/ws-1/1-cat.png', + targetKey: 'workspace/ws-1/2-cat.png', + context: 'mothership', + fileName: 'cat.png', + contentType: 'image/png', + }, + ]) + }) + + it('skips legacy rows with no workspaceId instead of failing the fork', async () => { + const inserted: Array> = [] + const tx = { + insert: () => ({ + values: async (v: Record | Record[]) => { + inserted.push(...(Array.isArray(v) ? v : [v])) + }, + }), + } + + const { idMap, blobTasks } = await planChatFileCopies({ + tx: tx as never, + rows: [makeRow({ workspaceId: null })], + newChatId: 'chat-fork', + userId: 'user-1', + now: NOW, + }) + + expect(inserted).toHaveLength(0) + expect(idMap.size).toBe(0) + expect(blobTasks).toHaveLength(0) + }) +}) + +describe('executeChatFileBlobCopies', () => { + const task = { + copyId: 'wf_copy', + sourceKey: 'workspace/ws-1/1-cat.png', + targetKey: 'workspace/ws-1/2-cat.png', + context: 'mothership' as const, + fileName: 'cat.png', + contentType: 'image/png', + } + + beforeEach(() => { + vi.clearAllMocks() + mockDownloadFile.mockResolvedValue(Buffer.from('0123456789')) + mockUploadFile.mockResolvedValue(undefined) + mockIncrementStorageUsage.mockResolvedValue(undefined) + }) + + it('copies bytes to the new key and counts them against the storage quota', async () => { + const result = await executeChatFileBlobCopies([task], { + userId: 'user-1', + workspaceId: 'ws-1', + }) + + expect(result).toEqual({ copied: 1, failed: 0, failedCopyIds: [] }) + expect(mockUploadFile).toHaveBeenCalledWith( + expect.objectContaining({ + customKey: 'workspace/ws-1/2-cat.png', + preserveKey: true, + }) + ) + // No `metadata` in the upload call — passing it would insert a second row. + expect(mockUploadFile.mock.calls[0][0].metadata).toBeUndefined() + expect(mockIncrementStorageUsage).toHaveBeenCalledWith('user-1', 10, 'ws-1') + }) + + it('is best-effort: a failed download skips the file, counts nothing, and reports its copy id', async () => { + mockDownloadFile.mockRejectedValueOnce(new Error('blob missing')) + + const result = await executeChatFileBlobCopies( + [task, { ...task, copyId: 'wf_copy2', targetKey: 'workspace/ws-1/3-cat.png' }], + { + userId: 'user-1', + workspaceId: 'ws-1', + } + ) + + // The first task's download failed — its copy id comes back for row cleanup. + expect(result).toEqual({ copied: 1, failed: 1, failedCopyIds: ['wf_copy'] }) + expect(mockIncrementStorageUsage).toHaveBeenCalledTimes(1) + }) + + it('copies every task even when the batch exceeds the concurrency bound', async () => { + const tasks = Array.from({ length: 9 }, (_, i) => ({ + ...task, + copyId: `wf_copy${i}`, + targetKey: `workspace/ws-1/${i}-cat.png`, + })) + + const result = await executeChatFileBlobCopies(tasks, { userId: 'user-1' }) + + expect(result.copied).toBe(9) + expect(mockUploadFile).toHaveBeenCalledTimes(9) + expect(new Set(mockUploadFile.mock.calls.map((c) => c[0].customKey)).size).toBe(9) + }) +}) + +describe('filterForkableChatFiles', () => { + it('keeps rows born in the kept slice plus NULL-birthdate legacy rows', () => { + const preCut = makeRow({ id: 'wf_pre', messageId: 'msg-1' }) + const postCut = makeRow({ id: 'wf_post', messageId: 'msg-3' }) + const legacy = makeRow({ id: 'wf_legacy', messageId: null }) + const secondPreCut = makeRow({ id: 'wf_pre2', messageId: 'msg-2' }) + + const kept = filterForkableChatFiles( + [preCut, postCut, legacy, secondPreCut], + new Set(['msg-1', 'msg-2']) + ) + + expect(kept.map((r) => r.id)).toEqual(['wf_pre', 'wf_legacy', 'wf_pre2']) + }) +}) diff --git a/apps/sim/lib/copilot/chat/fork-chat-files.ts b/apps/sim/lib/copilot/chat/fork-chat-files.ts new file mode 100644 index 00000000000..ce076052711 --- /dev/null +++ b/apps/sim/lib/copilot/chat/fork-chat-files.ts @@ -0,0 +1,223 @@ +import { workspaceFiles } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { generateShortId } from '@sim/utils/id' +import { and, eq, isNull } from 'drizzle-orm' +import { incrementStorageUsage } from '@/lib/billing/storage' +import { mapWithConcurrency } from '@/lib/core/utils/concurrency' +import type { DbOrTx } from '@/lib/db/types' +import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { downloadFile, uploadFile } from '@/lib/uploads/core/storage-service' +import type { StorageContext } from '@/lib/uploads/shared/types' +import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' + +const logger = createLogger('ForkChatFiles') + +/** + * The chat-owned storage context a fork copies: user uploads (`mothership`). + * A fork is a self-contained snapshot — bytes included (every copied row gets + * a fresh storage key; live rows can't share a key because of the + * `workspace_files_key_active_unique` index, and serve/view lookups resolve by + * key) — so the new chat survives deletion of the source chat. The copied set + * is timeline-cut to the fork point ({@link filterForkableChatFiles}). + * Shared workspace `files/` (`context='workspace'`) is workspace-owned, not + * chat-owned — both chats reference it in place and it is never copied. + */ +export const FORKABLE_CHAT_FILE_CONTEXT: StorageContext = 'mothership' + +/** Max concurrent blob byte-copies during a chat fork. */ +const CHAT_BLOB_COPY_CONCURRENCY = 4 + +export type ForkableChatFileRow = typeof workspaceFiles.$inferSelect + +/** One blob byte-copy to run after the fork transaction commits. */ +export interface ChatBlobCopyTask { + /** The copied `workspace_files` row's id — used to delete the row if the blob copy fails. */ + copyId: string + sourceKey: string + targetKey: string + context: StorageContext + fileName: string + contentType: string +} + +export interface PlanChatFileCopiesResult { + /** source `workspace_files.id` -> copy id (rewrites view-URLs, attachment ids, resource ids). */ + idMap: Map + /** source storage key -> copy storage key (rewrites serve-URLs, attachment keys). */ + keyMap: Map + /** Blob duplications to run after the transaction commits. */ + blobTasks: ChatBlobCopyTask[] +} + +/** + * Every live chat-owned file row (no timeline cut): the ghost test set for the + * resource-chip rewrite and the superset a fork cuts down in memory via + * {@link filterForkableChatFiles} — one `workspace_files` read serves both. + * Also used pre-transaction to sum sizes for the storage-quota gate. + */ +export async function listForkableChatFiles( + db: DbOrTx, + chatId: string +): Promise { + return db + .select() + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.chatId, chatId), + eq(workspaceFiles.context, FORKABLE_CHAT_FILE_CONTEXT), + isNull(workspaceFiles.deletedAt) + ) + ) +} + +/** + * The rows a fork copies out of the chat's owned files: those whose + * `message_id` is at-or-before the fork point (i.e. in the kept message + * slice). Rows with a NULL `message_id` — uploads that predate messageId + * stamping — are included in every fork of their chat: we can't know when + * they arrived, and copying them beats forking with broken references. Pure + * filter so the route reads `workspace_files` once per fork + * ({@link listForkableChatFiles}). + */ +export function filterForkableChatFiles( + rows: ForkableChatFileRow[], + keptMessageIds: ReadonlySet +): ForkableChatFileRow[] { + return rows.filter((row) => !row.messageId || keptMessageIds.has(row.messageId)) +} + +/** + * Insert copy rows for the kept chat-owned files under the new chat id (fresh + * `wf_` id + fresh storage key; `message_id` carries over verbatim so the copy + * matches the same message in the forked transcript; display names carry over + * verbatim because their uniqueness is per-chat and the new chat is an empty + * namespace). Returns the old->new id/key maps that drive the reference + * rewrite, plus the blob byte-copies to run post-commit. Runs inside the fork + * transaction so a failed insert rolls the whole fork back; blob I/O is + * deferred to {@link executeChatFileBlobCopies}. Modeled on the workspace-fork + * copy (`lib/workspaces/fork/copy/copy-files.ts`), adapted for chat-scoped rows. + */ +export async function planChatFileCopies(params: { + tx: DbOrTx + rows: ForkableChatFileRow[] + newChatId: string + userId: string + now: Date +}): Promise { + const { tx, rows, newChatId, userId, now } = params + const idMap = new Map() + const keyMap = new Map() + const blobTasks: ChatBlobCopyTask[] = [] + const copyRows: (typeof workspaceFiles.$inferInsert)[] = [] + + for (const row of rows) { + if (!row.workspaceId) { + logger.warn('Skipping chat file with no workspaceId during fork', { fileId: row.id }) + continue + } + const copyId = `wf_${generateShortId()}` + const targetKey = generateWorkspaceFileKey(row.workspaceId, row.originalName) + copyRows.push({ + ...row, + id: copyId, + key: targetKey, + chatId: newChatId, + userId, + deletedAt: null, + uploadedAt: now, + updatedAt: now, + }) + idMap.set(row.id, copyId) + keyMap.set(row.key, targetKey) + blobTasks.push({ + copyId, + sourceKey: row.key, + targetKey, + context: row.context as StorageContext, + fileName: row.originalName, + contentType: row.contentType, + }) + } + + // Ids and keys are generated client-side, so one multi-row insert suffices — + // no per-row round trips while the fork transaction is held open. + if (copyRows.length > 0) { + await tx.insert(workspaceFiles).values(copyRows) + } + + return { idMap, keyMap, blobTasks } +} + +/** + * Copy each planned blob to its new key, best-effort: a failed copy logs a + * warning and is skipped (the fork keeps its transcript; that one file is + * missing) rather than failing the whole fork. Runs a bounded worker pool + * ({@link CHAT_BLOB_COPY_CONCURRENCY}) — media-heavy chats must not pay 2N + * serial storage round-trips, but unbounded fan-out would buffer every file + * in memory at once. Each successfully copied file increments the + * storage-usage counter by its actual byte length. Failed tasks' copy-row ids + * are returned so the caller can delete the dead rows (row exists, blob + * doesn't) instead of leaving them listed in the VFS and resources with + * nothing behind them. + */ +export async function executeChatFileBlobCopies( + blobTasks: ChatBlobCopyTask[], + params: { userId: string; workspaceId?: string } +): Promise<{ copied: number; failed: number; failedCopyIds: string[] }> { + let copied = 0 + const failedCopyIds: string[] = [] + + const copyOne = async (task: ChatBlobCopyTask): Promise => { + try { + // No replay guard here, unlike the workspace-fork copy this is modeled + // on: that path persists its task list in a trigger.dev job payload + // (replayable), while these tasks exist only in this request's memory + // and target keys are freshly minted per request — a HEAD check could + // never find an earlier attempt's object. + const buffer = await downloadFile({ + key: task.sourceKey, + context: task.context, + maxBytes: MAX_FILE_SIZE, + }) + // No `metadata` here on purpose: passing it would make uploadFile insert + // its own workspace_files row (without chatId), colliding with the row + // the transaction already created for this key. + await uploadFile({ + file: buffer, + fileName: task.fileName, + contentType: task.contentType, + context: task.context, + customKey: task.targetKey, + preserveKey: true, + }) + copied += 1 + try { + // Forked bytes COUNT against the storage quota, deliberately diverging + // from the workspace-fork copy path + // (lib/workspaces/fork/copy/copy-files.ts), which copies blobs without + // counting them. A chat fork stores a second physical copy of every + // kept upload, so the counter must reflect it. Do not "fix" this back + // to the workspace-fork precedent. + await incrementStorageUsage(params.userId, buffer.length, params.workspaceId) + } catch (error) { + logger.error('Failed to update storage tracking for forked chat file', { + targetKey: task.targetKey, + error: getErrorMessage(error), + }) + } + } catch (error) { + failedCopyIds.push(task.copyId) + logger.warn('Failed to copy chat file blob during fork', { + sourceKey: task.sourceKey, + targetKey: task.targetKey, + error: getErrorMessage(error), + }) + } + } + + await mapWithConcurrency(blobTasks, CHAT_BLOB_COPY_CONCURRENCY, copyOne) + + return { copied, failed: failedCopyIds.length, failedCopyIds } +} diff --git a/apps/sim/lib/copilot/chat/payload.ts b/apps/sim/lib/copilot/chat/payload.ts index e718b33cce3..317f65b83bd 100644 --- a/apps/sim/lib/copilot/chat/payload.ts +++ b/apps/sim/lib/copilot/chat/payload.ts @@ -286,7 +286,8 @@ export async function buildCopilotRequestPayload( f.key, filename, mediaType, - f.size + f.size, + userMessageId ) // Encode the read path per the percent-encoded VFS convention (matches // files/ and the uploads glob output). The materialize_file `fileName` diff --git a/apps/sim/lib/copilot/chat/persisted-message.test.ts b/apps/sim/lib/copilot/chat/persisted-message.test.ts index afe43cf6a08..2b9e5d52e9e 100644 --- a/apps/sim/lib/copilot/chat/persisted-message.test.ts +++ b/apps/sim/lib/copilot/chat/persisted-message.test.ts @@ -90,10 +90,10 @@ describe('persisted-message', () => { const persisted = buildPersistedAssistantMessage(result) expect(persisted.content).not.toContain('sk-sim-secret-123') - expect(persisted.content).toContain('"redacted":true') + expect(persisted.content).toContain('{"type":"sim_key"}') const textBlock = persisted.contentBlocks?.find((b) => b.type === 'text') expect(textBlock?.content).not.toContain('sk-sim-secret-123') - expect(textBlock?.content).toContain('"redacted":true') + expect(textBlock?.content).toContain('{"type":"sim_key"}') }) it('redacts sim_key credential tags split across streamed text chunks', () => { @@ -119,7 +119,7 @@ describe('persisted-message', () => { expect(persisted.contentBlocks).toBeDefined() const joined = (persisted.contentBlocks ?? []).map((b) => b.content ?? '').join('') expect(joined).not.toContain('sk-sim-secret-12345') - expect(joined).toContain('"redacted":true') + expect(joined).toContain('{"type":"sim_key"}') }) it('redacts the api key from a persisted generate_api_key tool result output', () => { diff --git a/apps/sim/lib/copilot/chat/rewrite-file-references.ts b/apps/sim/lib/copilot/chat/rewrite-file-references.ts new file mode 100644 index 00000000000..021c6c19a2c --- /dev/null +++ b/apps/sim/lib/copilot/chat/rewrite-file-references.ts @@ -0,0 +1,99 @@ +import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message' +import type { MothershipResource } from '@/lib/copilot/resources/types' +import { rewriteForkContentRefs } from '@/ee/workspace-forking/lib/remap/remap-content-refs' + +/** + * Old->new translation tables produced while copying a chat's files + * (`planChatFileCopies`): row ids (view-URLs, attachment ids, resource ids, + * context chips) and storage keys (serve-URLs, attachment keys). + */ +export interface ChatFileRefMaps { + fileIds: ReadonlyMap + fileKeys: ReadonlyMap +} + +function hasMappings(maps: ChatFileRefMaps): boolean { + return maps.fileIds.size > 0 || maps.fileKeys.size > 0 +} + +function rewriteText(text: string, maps: ChatFileRefMaps): string { + return rewriteForkContentRefs(text, { fileIds: maps.fileIds, fileKeys: maps.fileKeys }) +} + +/** + * Re-point every file reference in a copied transcript at the copied files, so + * the fork is self-contained (it survives the original chat's deletion). + * Rewrites: free-text URLs in `content` and text content blocks (serve/view/ + * in-app/`sim:file` forms, via the shared fork grammar), attachment chip + * ids+keys, and `@`-mention context chip file ids. References to anything not + * in the maps (shared workspace files, workflows, other chats) pass through + * unchanged. Pure; returns the input array untouched when there is nothing to + * rewrite. + * + * Pass-through cannot leave the fork pointing at uncopied CHAT-OWNED files: + * a message can only reference files that existed when it was written, every + * chat-owned file is stamped with the user message of the turn it was born in + * (NULL-stamped legacy rows are copied into every fork), and the fork copies + * every chat-owned file born at-or-before the cut — so any chat-owned file a + * kept message references is always in the maps. The only reachable leftovers + * are files soft-deleted before the fork, whose links are equally dead in the + * source chat. + */ +export function rewriteMessageFileRefs( + messages: PersistedMessage[], + maps: ChatFileRefMaps +): PersistedMessage[] { + if (!hasMappings(maps)) return messages + return messages.map((message) => { + const rewritten: PersistedMessage = { + ...message, + content: rewriteText(message.content, maps), + } + if (message.contentBlocks?.length) { + rewritten.contentBlocks = message.contentBlocks.map((block) => + block.content ? { ...block, content: rewriteText(block.content, maps) } : block + ) + } + if (message.fileAttachments?.length) { + rewritten.fileAttachments = message.fileAttachments.map((att) => ({ + ...att, + id: maps.fileIds.get(att.id) ?? att.id, + key: maps.fileKeys.get(att.key) ?? att.key, + })) + } + if (message.contexts?.length) { + rewritten.contexts = message.contexts.map((ctx) => + ctx.fileId ? { ...ctx, fileId: maps.fileIds.get(ctx.fileId) ?? ctx.fileId } : ctx + ) + } + return rewritten + }) +} + +/** + * Re-point `file`-typed resource entries (the chat's attached-resources list + * stores raw `workspace_files.id`s) at the copied files. Non-file resources + * (workflows, tables, knowledge bases…) reference shared workspace entities + * and pass through unchanged. + * + * `dropFileIds` is the source chat's chat-owned file ids (no timeline cut). + * A file resource pointing at one of these that was NOT copied is a ghost in + * the new chat — its file stays behind (an upload born after the cut) — so it + * is dropped rather than left pointing at the source chat's file. Shared + * workspace files are not chat-owned, never appear in the set, and pass + * through unchanged. + */ +export function rewriteResourceFileRefs( + resources: MothershipResource[], + maps: ChatFileRefMaps, + dropFileIds?: ReadonlySet +): MothershipResource[] { + if (!hasMappings(maps) && !dropFileIds?.size) return resources + return resources.flatMap((resource) => { + if (resource.type !== 'file') return [resource] + const copyId = maps.fileIds.get(resource.id) + if (copyId) return [{ ...resource, id: copyId }] + if (dropFileIds?.has(resource.id)) return [] + return [resource] + }) +} diff --git a/apps/sim/lib/copilot/chat/sim-key-redaction.test.ts b/apps/sim/lib/copilot/chat/sim-key-redaction.test.ts index 70fe637e6d2..0d2e28b8537 100644 --- a/apps/sim/lib/copilot/chat/sim-key-redaction.test.ts +++ b/apps/sim/lib/copilot/chat/sim-key-redaction.test.ts @@ -7,12 +7,21 @@ import type { ChatMessage } from '@/app/workspace/[workspaceId]/home/types' import { captureRevealedSimKeys, extractRevealedSimKeys, + extractRevealedSimKeysFromBlocks, restoreRevealedSimKeysForMessage, + toolResultForModel, } from './sim-key-redaction' const credential = (value: string) => `${JSON.stringify({ value, type: 'sim_key' })}` const redacted = `${JSON.stringify({ type: 'sim_key', redacted: true })}` +// The value-less placeholder the model now emits (no `redacted` flag). +const placeholder = `${JSON.stringify({ type: 'sim_key' })}` + +const apiKeyBlock = (key: string) => ({ + type: 'tool_call' as const, + toolCall: { name: 'generate_api_key', result: { success: true, output: { id: 'k1', key } } }, +}) describe('sim-key-redaction', () => { describe('extractRevealedSimKeys', () => { @@ -28,6 +37,55 @@ describe('sim-key-redaction', () => { }) }) + describe('toolResultForModel', () => { + it('reduces a successful generate_api_key result to only its status message', () => { + const data = { + id: 'k1', + name: 'prod', + key: 'sk-sim-secret', + workspaceId: 'ws-1', + message: 'API key "prod" created.', + } + expect(toolResultForModel('generate_api_key', data)).toBe('API key "prod" created.') + }) + + it('leaves other tools untouched', () => { + const data = { key: 'not-a-secret', ok: true } + expect(toolResultForModel('read', data)).toBe(data) + }) + + it('passes generate_api_key errors through (no key to withhold)', () => { + const data = { error: 'name is required' } + expect(toolResultForModel('generate_api_key', data)).toBe(data) + expect(toolResultForModel('generate_api_key', undefined)).toBe(undefined) + }) + }) + + describe('extractRevealedSimKeysFromBlocks', () => { + it('pulls generate_api_key output keys in block order', () => { + expect( + extractRevealedSimKeysFromBlocks([apiKeyBlock('sk-sim-A'), apiKeyBlock('sk-sim-B')]) + ).toEqual(['sk-sim-A', 'sk-sim-B']) + }) + + it('skips redacted markers and unrelated tools', () => { + const blocks = [ + apiKeyBlock('[REDACTED]'), + { + type: 'tool_call' as const, + toolCall: { name: 'read', result: { success: true, output: { key: 'sk-x' } } }, + }, + apiKeyBlock('sk-sim-A'), + ] + expect(extractRevealedSimKeysFromBlocks(blocks)).toEqual(['sk-sim-A']) + }) + + it('returns nothing for empty/undefined block lists', () => { + expect(extractRevealedSimKeysFromBlocks(undefined)).toEqual([]) + expect(extractRevealedSimKeysFromBlocks([])).toEqual([]) + }) + }) + describe('captureRevealedSimKeys', () => { it('records new keys under each provided key', () => { const cache = new Map() @@ -59,6 +117,21 @@ describe('sim-key-redaction', () => { captureRevealedSimKeys(cache, ['msg-1'], 'plain assistant text') expect(cache.has('msg-1')).toBe(false) }) + + it('sources the key from the generate_api_key tool result (model text is a redacted placeholder)', () => { + const cache = new Map() + captureRevealedSimKeys(cache, ['msg-1', 'req-1'], `Here is your key: ${redacted}`, [ + apiKeyBlock('sk-sim-fromtool'), + ]) + expect(cache.get('msg-1')).toEqual(['sk-sim-fromtool']) + expect(cache.get('req-1')).toEqual(['sk-sim-fromtool']) + }) + + it('prefers tool-result keys over any inline content values', () => { + const cache = new Map() + captureRevealedSimKeys(cache, ['msg-1'], credential('sk-content'), [apiKeyBlock('sk-tool')]) + expect(cache.get('msg-1')).toEqual(['sk-tool']) + }) }) describe('restoreRevealedSimKeysForMessage', () => { @@ -76,6 +149,32 @@ describe('sim-key-redaction', () => { expect(restored.contentBlocks?.[0].content).toContain('"sk-sim-A"') }) + it('fills a value-less {"type":"sim_key"} placeholder (no redacted flag needed)', () => { + const cache = new Map([['msg-1', ['sk-sim-A']]]) + const msg: ChatMessage = { + id: 'msg-1', + role: 'assistant', + content: `Here is your key: ${placeholder} save it.`, + contentBlocks: [{ type: 'text', content: `Here is your key: ${placeholder} save it.` }], + } + const restored = restoreRevealedSimKeysForMessage(msg, cache) + expect(restored.content).toContain('"sk-sim-A"') + expect(restored.contentBlocks?.[0].content).toContain('"sk-sim-A"') + }) + + it('fills value-less and redacted placeholders positionally in one message', () => { + const cache = new Map([['msg-1', ['sk-sim-A', 'sk-sim-B']]]) + const msg: ChatMessage = { + id: 'msg-1', + role: 'assistant', + content: `first ${placeholder} second ${redacted}`, + } + const restored = restoreRevealedSimKeysForMessage(msg, cache) + expect(restored.content).toBe( + `first ${credential('sk-sim-A')} second ${credential('sk-sim-B')}` + ) + }) + it('substitutes multiple keys in stream order', () => { const cache = new Map([['msg-1', ['sk-sim-A', 'sk-sim-B']]]) const msg: ChatMessage = { diff --git a/apps/sim/lib/copilot/chat/sim-key-redaction.ts b/apps/sim/lib/copilot/chat/sim-key-redaction.ts index d5aba0f302d..cb59a9c56df 100644 --- a/apps/sim/lib/copilot/chat/sim-key-redaction.ts +++ b/apps/sim/lib/copilot/chat/sim-key-redaction.ts @@ -1,3 +1,4 @@ +import { isRecordLike } from '@sim/utils/object' import type { PersistedContentBlock } from '@/lib/copilot/chat/persisted-message' import { MothershipStreamV1EventType, @@ -23,17 +24,15 @@ import type { ChatMessage, ContentBlock } from '@/app/workspace/[workspaceId]/ho */ const CREDENTIAL_TAG_PATTERN = /([\s\S]*?)<\/credential>/g -const REDACTED_TAG_PATTERN = /[^<]*"redacted"\s*:\s*true[^<]*<\/credential>/ const SIM_KEY_TYPE = 'sim_key' -const REDACTED_SIM_KEY_TAG = `${JSON.stringify({ - type: SIM_KEY_TYPE, - redacted: true, -})}` +// The persisted / secret-stripped form of a sim_key tag: value-less, which is +// exactly how the UI renders the masked state. No `redacted` flag needed — a +// sim_key chip is masked iff it has no value. +const VALUELESS_SIM_KEY_TAG = `${JSON.stringify({ type: SIM_KEY_TYPE })}` interface CredentialTagBody { type?: unknown value?: unknown - redacted?: unknown } function parseCredentialBody(body: string): CredentialTagBody | null { @@ -44,22 +43,34 @@ function parseCredentialBody(body: string): CredentialTagBody | null { } } -function hasRedactedSimKeyTag(content: string | undefined): boolean { - return typeof content === 'string' && REDACTED_TAG_PATTERN.test(content) +/** + * True when `content` holds a `sim_key` credential tag that still needs its + * value filled in — i.e. any value-less `sim_key` tag: the model's + * `{"type":"sim_key"}` placeholder, the persisted form, or a legacy + * `{"type":"sim_key","redacted":true}` tag. All are recognized by the absence + * of a `value`. + */ +function hasFillableSimKeyTag(content: string | undefined): boolean { + if (typeof content !== 'string' || !content.includes('')) return false + for (const match of content.matchAll(CREDENTIAL_TAG_PATTERN)) { + const parsed = parseCredentialBody(match[1]) + if (parsed?.type === SIM_KEY_TYPE && parsed.value === undefined) return true + } + return false } // Write side --------------------------------------------------------------- /** - * Replace every revealed `` tag in `content` with a - * placeholder marked `redacted: true`. Other credential types (e.g. OAuth - * `link`) and malformed bodies pass through unchanged. + * Replace every `` tag in `content` with the + * value-less placeholder, so a revealed key is never persisted. Other credential + * types (e.g. OAuth `link`) and malformed bodies pass through unchanged. */ export function redactSensitiveContent(content: T): T { if (typeof content !== 'string' || !content.includes('')) return content return content.replace(CREDENTIAL_TAG_PATTERN, (match, body: string) => { const parsed = parseCredentialBody(body) - return parsed?.type === SIM_KEY_TYPE ? REDACTED_SIM_KEY_TAG : match + return parsed?.type === SIM_KEY_TYPE ? VALUELESS_SIM_KEY_TAG : match }) as T } @@ -83,6 +94,23 @@ export function redactToolCallResult( } } +/** + * The model-facing result of `generate_api_key`. The generated key is a + * client-only artifact — it rides the SSE tool result to the browser and renders + * as the `sim_key` chip — so the model (and the persisted conversation) must + * never receive it. Rather than subtract the secret from the full payload, the + * model's result IS the status: on success it gets only the tool's message (no + * key, no id/name/workspaceId); a failure passes through so the model still sees + * the error. Every other tool's terminal data is returned unchanged. + */ +export function toolResultForModel(toolName: string | undefined, data: unknown): unknown { + if (toolName !== GenerateApiKey.id) return data + if (!isRecordLike(data)) return data + const record = data + if (typeof record.key !== 'string') return data + return record.message +} + function isMergeableAssistantTextBlock(block: PersistedContentBlock): boolean { return ( block.type === MothershipStreamV1EventType.text && @@ -156,34 +184,76 @@ export type RevealedSimKeysByMessage = Map /** * Scan an assembled assistant message for `` tags - * and return their values in stream order, skipping anything already redacted. + * and return their values in stream order; value-less (masked/placeholder) tags + * carry no string value and are skipped. */ export function extractRevealedSimKeys(content: string): string[] { if (!content || !content.includes('')) return [] const values: string[] = [] for (const match of content.matchAll(CREDENTIAL_TAG_PATTERN)) { const parsed = parseCredentialBody(match[1]) - if (parsed?.type === SIM_KEY_TYPE && !parsed.redacted && typeof parsed.value === 'string') { + if (parsed?.type === SIM_KEY_TYPE && typeof parsed.value === 'string') { values.push(parsed.value) } } return values } +/** Minimal shape of a rendered/streamed block carrying a tool result. */ +interface ToolResultBlockLike { + toolCall?: { name?: string; result?: unknown } | null +} + +/** + * Pull the freshly-generated key(s) out of `generate_api_key` tool results in + * block order. This is the authoritative source for the `sim_key` chip now that + * the model never sees (or emits) the value — it only emits a redacted + * placeholder, and the real value rides in the tool result on the live stream. + * `[REDACTED]` outputs (post-persist/refetch) are skipped so a reloaded + * transcript doesn't cache the masked marker over a live value. + */ +export function extractRevealedSimKeysFromBlocks( + blocks: ReadonlyArray | undefined +): string[] { + if (!blocks?.length) return [] + const values: string[] = [] + for (const block of blocks) { + const toolCall = block.toolCall + if (!toolCall || toolCall.name !== GenerateApiKey.id) continue + const result = toolCall.result + if (!isRecordLike(result)) continue + const output = result.output + if (!isRecordLike(output)) continue + const key = output.key + if (typeof key === 'string' && key.length > 0 && key !== REDACTED_MARKER) { + values.push(key) + } + } + return values +} + /** * Extend the cache entries for the given keys with any newly-revealed values. * Each key in `keys` is written the same array — passing both the live-stream * id and the persisted `requestId` lets the post-finalize refetch hit the * cache after the message is renamed to its real UUID. The longest captured * list wins so a rerun that surfaces fewer values can't shrink the entry. + * + * Values are sourced from the `generate_api_key` tool results (`blocks`) first — + * that is where the key now lives, since the model only emits a redacted + * placeholder tag — falling back to any inline `sim_key` tag values in + * `content` for backward compatibility with pre-change transcripts. */ export function captureRevealedSimKeys( cache: RevealedSimKeysByMessage, keys: ReadonlyArray, - content: string + content: string, + blocks?: ReadonlyArray ): void { - if (!content.includes('')) return - const next = extractRevealedSimKeys(content) + const fromBlocks = extractRevealedSimKeysFromBlocks(blocks) + // extractRevealedSimKeys already returns [] when `content` has no tag, so no + // separate includes() guard is needed. + const next = fromBlocks.length > 0 ? fromBlocks : extractRevealedSimKeys(content) if (next.length === 0) return for (const key of keys) { if (!key) continue @@ -208,7 +278,10 @@ function restoreInString( let changed = false const next = content.replace(CREDENTIAL_TAG_PATTERN, (match, body: string) => { const parsed = parseCredentialBody(body) - if (parsed?.type === SIM_KEY_TYPE && parsed.redacted === true) { + // Any value-less sim_key tag is a fill slot — the model's placeholder, the + // persisted form, or a legacy `{"redacted":true}` tag. Already-filled tags + // carry a `value` and are left untouched (idempotent). + if (parsed?.type === SIM_KEY_TYPE && parsed.value === undefined) { const value = revealedValues[cursor] cursor += 1 if (typeof value === 'string') { @@ -235,8 +308,8 @@ export function restoreRevealedSimKeysForMessage( cache.get(message.id) ?? (message.requestId ? cache.get(message.requestId) : undefined) if (!revealed || revealed.length === 0) return message if ( - !hasRedactedSimKeyTag(message.content) && - !message.contentBlocks?.some((b) => hasRedactedSimKeyTag(b.content)) + !hasFillableSimKeyTag(message.content) && + !message.contentBlocks?.some((b) => hasFillableSimKeyTag(b.content)) ) { return message } @@ -245,7 +318,7 @@ export function restoreRevealedSimKeysForMessage( let blocksChanged = false let blockCursor = 0 const nextBlocks: ContentBlock[] | undefined = message.contentBlocks?.map((block) => { - if (!hasRedactedSimKeyTag(block.content)) return block + if (!hasFillableSimKeyTag(block.content)) return block const restored = restoreInString(block.content as string, revealed, blockCursor) blockCursor = restored.cursor if (!restored.changed) return block diff --git a/apps/sim/lib/copilot/generated/metrics-v1.ts b/apps/sim/lib/copilot/generated/metrics-v1.ts index fefc4534ea3..06c74839c47 100644 --- a/apps/sim/lib/copilot/generated/metrics-v1.ts +++ b/apps/sim/lib/copilot/generated/metrics-v1.ts @@ -17,6 +17,8 @@ export const Metric = { CopilotCacheAttempted: 'copilot.cache.attempted', CopilotCacheHit: 'copilot.cache.hit', CopilotCacheWrite: 'copilot.cache.write', + CopilotChatBlobBytes: 'copilot.chat.blob.bytes', + CopilotChatBlobCount: 'copilot.chat.blob.count', CopilotFileReadDuration: 'copilot.file.read.duration', CopilotFileReadSize: 'copilot.file.read.size', CopilotMessagesSerializeDuration: 'copilot.messages.serialize.duration', @@ -28,6 +30,8 @@ export const Metric = { CopilotVfsMaterializeDuration: 'copilot.vfs.materialize.duration', GenAiClientCacheTokenUsage: 'gen_ai.client.cache.token.usage', GenAiClientTokenUsage: 'gen_ai.client.token.usage', + LlmClientCompactions: 'llm.client.compactions', + LlmClientContextTokens: 'llm.client.context_tokens', LlmClientErrors: 'llm.client.errors', LlmClientOutputCutoff: 'llm.client.output_cutoff', LlmClientStreamDuration: 'llm.client.stream.duration', @@ -42,6 +46,8 @@ export const MetricValues: readonly MetricValue[] = [ 'copilot.cache.attempted', 'copilot.cache.hit', 'copilot.cache.write', + 'copilot.chat.blob.bytes', + 'copilot.chat.blob.count', 'copilot.file.read.duration', 'copilot.file.read.size', 'copilot.messages.serialize.duration', @@ -53,6 +59,8 @@ export const MetricValues: readonly MetricValue[] = [ 'copilot.vfs.materialize.duration', 'gen_ai.client.cache.token.usage', 'gen_ai.client.token.usage', + 'llm.client.compactions', + 'llm.client.context_tokens', 'llm.client.errors', 'llm.client.output_cutoff', 'llm.client.stream.duration', diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 4a4c24a1594..6cf35b7fdcc 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -11,9 +11,9 @@ export interface ToolCatalogEntry { | 'auth' | 'check_deployment_status' | 'complete_scheduled_task' + | 'cp' | 'crawl_website' | 'create_file' - | 'create_file_folder' | 'create_workflow' | 'create_workspace_mcp_server' | 'delete_file' @@ -49,7 +49,6 @@ export interface ToolCatalogEntry { | 'grep' | 'knowledge' | 'knowledge_base' - | 'list_file_folders' | 'list_integration_tools' | 'list_user_workspaces' | 'list_workspace_mcp_servers' @@ -63,30 +62,29 @@ export interface ToolCatalogEntry { | 'manage_skill' | 'materialize_file' | 'media' - | 'move_file' - | 'move_file_folder' - | 'move_workflow' + | 'mkdir' + | 'mv' | 'oauth_get_auth_link' | 'oauth_request_access' | 'open_resource' | 'promote_to_live' | 'query_logs' + | 'query_user_table' | 'read' | 'redeploy' - | 'rename_file' - | 'rename_file_folder' - | 'rename_workflow' - | 'research' | 'respond' | 'restore_resource' | 'run' | 'run_block' + | 'run_code' | 'run_from_block' | 'run_workflow' | 'run_workflow_until_block' | 'scheduled_task' | 'scrape_page' + | 'search' | 'search_documentation' + | 'search_knowledge_base' | 'search_library_docs' | 'search_online' | 'search_patterns' @@ -98,7 +96,6 @@ export interface ToolCatalogEntry { | 'update_deployment_version' | 'update_scheduled_task_history' | 'update_workspace_mcp_server' - | 'user_memory' | 'user_table' | 'workflow' | 'workspace_file' @@ -109,9 +106,9 @@ export interface ToolCatalogEntry { | 'auth' | 'check_deployment_status' | 'complete_scheduled_task' + | 'cp' | 'crawl_website' | 'create_file' - | 'create_file_folder' | 'create_workflow' | 'create_workspace_mcp_server' | 'delete_file' @@ -147,7 +144,6 @@ export interface ToolCatalogEntry { | 'grep' | 'knowledge' | 'knowledge_base' - | 'list_file_folders' | 'list_integration_tools' | 'list_user_workspaces' | 'list_workspace_mcp_servers' @@ -161,30 +157,29 @@ export interface ToolCatalogEntry { | 'manage_skill' | 'materialize_file' | 'media' - | 'move_file' - | 'move_file_folder' - | 'move_workflow' + | 'mkdir' + | 'mv' | 'oauth_get_auth_link' | 'oauth_request_access' | 'open_resource' | 'promote_to_live' | 'query_logs' + | 'query_user_table' | 'read' | 'redeploy' - | 'rename_file' - | 'rename_file_folder' - | 'rename_workflow' - | 'research' | 'respond' | 'restore_resource' | 'run' | 'run_block' + | 'run_code' | 'run_from_block' | 'run_workflow' | 'run_workflow_until_block' | 'scheduled_task' | 'scrape_page' + | 'search' | 'search_documentation' + | 'search_knowledge_base' | 'search_library_docs' | 'search_online' | 'search_patterns' @@ -196,12 +191,11 @@ export interface ToolCatalogEntry { | 'update_deployment_version' | 'update_scheduled_task_history' | 'update_workspace_mcp_server' - | 'user_memory' | 'user_table' | 'workflow' | 'workspace_file' parameters: unknown - requiredPermission?: 'admin' | 'read' | 'write' + requiredPermission?: 'admin' | 'write' resultSchema?: unknown route: 'client' | 'go' | 'sim' | 'subagent' subagentId?: @@ -211,9 +205,9 @@ export interface ToolCatalogEntry { | 'file' | 'knowledge' | 'media' - | 'research' | 'run' | 'scheduled_task' + | 'search' | 'superagent' | 'table' | 'workflow' @@ -282,6 +276,36 @@ export const CompleteScheduledTask: ToolCatalogEntry = { }, } +export const Cp: ToolCatalogEntry = { + id: 'cp', + name: 'cp', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + destination: { + type: 'string', + description: + 'Target path under workflows/. An existing folder (or a path ending in "/") duplicates sources into it keeping their names; otherwise the last segment names the copy and the preceding segments are the target folder (created automatically when missing).', + }, + sources: { + type: 'array', + description: + 'Canonical workflow VFS paths to duplicate, e.g. ["workflows/My%20Workflow"]. Copy paths verbatim from glob/grep/read output.', + items: { type: 'string' }, + }, + toolTitle: { + type: 'string', + description: + 'Target-only UI phrase for the action row, e.g. "My Workflow" or "Template to Archive", not a full sentence like "Copying My Workflow".', + }, + }, + required: ['sources', 'destination', 'toolTitle'], + }, + requiredPermission: 'write', +} + export const CrawlWebsite: ToolCatalogEntry = { id: 'crawl_website', name: 'crawl_website', @@ -333,7 +357,7 @@ export const CreateFile: ToolCatalogEntry = { files: { type: 'array', description: - 'Files to create or overwrite. Parent folders must already exist for create mode.', + 'Files to create or overwrite. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -375,29 +399,6 @@ export const CreateFile: ToolCatalogEntry = { capabilities: ['file_output'], } -export const CreateFileFolder: ToolCatalogEntry = { - id: 'create_file_folder', - name: 'create_file_folder', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - path: { - type: 'string', - description: - 'Canonical folder VFS path to create, e.g. "files/Images" or "files/Reports/2026".', - }, - workspaceId: { - type: 'string', - description: 'Optional workspace ID. Defaults to the current workspace.', - }, - }, - required: ['path'], - }, - requiredPermission: 'write', -} - export const CreateWorkflow: ToolCatalogEntry = { id: 'create_workflow', name: 'create_workflow', @@ -911,7 +912,7 @@ export const DownloadToWorkspaceFile: ToolCatalogEntry = { files: { type: 'array', description: - 'Files to create or overwrite. Parent folders must already exist for create mode.', + 'Files to create or overwrite. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -1186,7 +1187,8 @@ export const Ffmpeg: ToolCatalogEntry = { properties: { files: { type: 'array', - description: 'File outputs. Parent folders must already exist for create mode.', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -1350,7 +1352,8 @@ export const FunctionExecute: ToolCatalogEntry = { properties: { files: { type: 'array', - description: 'File outputs. Parent folders must already exist for create mode.', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -1515,7 +1518,8 @@ export const GenerateAudio: ToolCatalogEntry = { properties: { files: { type: 'array', - description: 'File outputs. Parent folders must already exist for create mode.', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -1649,7 +1653,8 @@ export const GenerateImage: ToolCatalogEntry = { properties: { files: { type: 'array', - description: 'File outputs. Parent folders must already exist for create mode.', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -1807,7 +1812,8 @@ export const GenerateVideo: ToolCatalogEntry = { properties: { files: { type: 'array', - description: 'File outputs. Parent folders must already exist for create mode.', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -2296,23 +2302,6 @@ export const KnowledgeBase: ToolCatalogEntry = { }, } -export const ListFileFolders: ToolCatalogEntry = { - id: 'list_file_folders', - name: 'list_file_folders', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - workspaceId: { - type: 'string', - description: 'Optional workspace ID. Defaults to the current workspace.', - }, - }, - }, - requiredPermission: 'read', -} - export const ListIntegrationTools: ToolCatalogEntry = { id: 'list_integration_tools', name: 'list_integration_tools', @@ -2501,35 +2490,16 @@ export const ManageFolder: ToolCatalogEntry = { parameters: { type: 'object', properties: { - destinationPath: { - type: 'string', - description: - 'Destination parent folder\'s VFS path for move/create. Omit (or pass "workflows") to target the workspace root.', - }, folderId: { type: 'string', description: 'Target folder ID, used as a fallback when path is not given. Readable from a contained workflow\'s meta.json "folderId".', }, - name: { - type: 'string', - description: - 'Folder name. Required for rename (the new name); for create when you pass a destination parent instead of a full path.', - }, - operation: { - type: 'string', - description: 'The operation to perform.', - enum: ['create', 'rename', 'move', 'delete'], - }, - parentId: { - type: 'string', - description: - 'Destination parent folder ID, used as a fallback when destinationPath is not given.', - }, + operation: { type: 'string', description: 'The operation to perform.', enum: ['delete'] }, path: { type: 'string', description: - 'Target folder\'s VFS path (e.g. "workflows/Marketing/Q3 Campaigns"), per-segment percent-encoded like every VFS path. Identifies the folder for rename/move/delete; for create it is the new folder\'s full path (its parent must already exist).', + 'Target folder\'s VFS path (e.g. "workflows/Marketing/Q3 Campaigns"), per-segment percent-encoded like every VFS path.', }, }, required: ['operation'], @@ -2746,72 +2716,57 @@ export const Media: ToolCatalogEntry = { internal: true, } -export const MoveFile: ToolCatalogEntry = { - id: 'move_file', - name: 'move_file', +export const Mkdir: ToolCatalogEntry = { + id: 'mkdir', + name: 'mkdir', route: 'sim', mode: 'async', parameters: { type: 'object', properties: { - destinationPath: { - type: 'string', - description: - 'Canonical target folder path, e.g. "files/Images". Omit or pass "files" for root.', - }, paths: { type: 'array', - description: 'Canonical workspace file VFS paths to move, e.g. ["files/photo.png"].', + description: + 'Canonical folder VFS paths to create, e.g. ["files/Reports/2026"]. Missing parent segments are created automatically.', items: { type: 'string' }, }, - }, - required: ['paths'], - }, - requiredPermission: 'write', -} - -export const MoveFileFolder: ToolCatalogEntry = { - id: 'move_file_folder', - name: 'move_file_folder', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - destinationPath: { + toolTitle: { type: 'string', description: - 'Canonical target parent folder path, e.g. "files/Archive". Omit or pass "files" for root.', - }, - path: { - type: 'string', - description: 'Canonical folder VFS path to move, e.g. "files/Reports/2026".', + 'Target-only UI phrase for the action row, e.g. "Reports/2026" or "2 folders", not a full sentence like "Creating Reports".', }, }, - required: ['path'], + required: ['paths', 'toolTitle'], }, requiredPermission: 'write', } -export const MoveWorkflow: ToolCatalogEntry = { - id: 'move_workflow', - name: 'move_workflow', +export const Mv: ToolCatalogEntry = { + id: 'mv', + name: 'mv', route: 'sim', mode: 'async', parameters: { type: 'object', properties: { - folderId: { + destination: { type: 'string', - description: 'Target folder ID. Omit or pass empty string to move to workspace root.', + description: + 'Target path. A path ending in "/" (or naming an existing folder) moves sources into it keeping their names — always use the trailing "/" form when targeting a folder. Otherwise the last segment is the new name and the preceding segments are the target folder (created automatically when missing).', }, - workflowIds: { + sources: { type: 'array', - description: 'The workflow IDs to move.', + description: + 'Canonical VFS paths to move or rename, e.g. ["files/draft.md"]. All sources must share one category. Copy paths verbatim from glob/grep/read output.', items: { type: 'string' }, }, + toolTitle: { + type: 'string', + description: + 'Target-only UI phrase for the action row, e.g. "draft.md to Reports" or "3 files to Images", not a full sentence like "Moving draft.md".', + }, }, - required: ['workflowIds'], + required: ['sources', 'destination', 'toolTitle'], }, requiredPermission: 'write', } @@ -2824,6 +2779,11 @@ export const OauthGetAuthLink: ToolCatalogEntry = { parameters: { type: 'object', properties: { + credentialId: { + type: 'string', + description: + 'Optional. The id of an EXISTING credential (from environment/credentials.json) to reconnect/re-authorize in place. Only when the user explicitly asks to reconnect or repair that credential — never for adding another account.', + }, providerName: { type: 'string', description: @@ -3015,6 +2975,55 @@ export const QueryLogs: ToolCatalogEntry = { }, } +export const QueryUserTable: ToolCatalogEntry = { + id: 'query_user_table', + name: 'query_user_table', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + args: { + type: 'object', + description: 'Arguments for the operation', + properties: { + filter: { type: 'object', description: 'MongoDB-style filter for query_rows' }, + limit: { + type: 'number', + description: 'Maximum rows to return (optional, default 100, max 1000 per call)', + }, + offset: { + type: 'number', + description: 'Number of rows to skip (optional for query_rows, default 0)', + }, + rowId: { type: 'string', description: 'Row ID (required for get_row)' }, + sort: { + type: 'object', + description: + "Sort specification as { field: 'asc' | 'desc' } (optional for query_rows)", + }, + tableId: { type: 'string', description: 'Table ID (required for all operations)' }, + }, + }, + operation: { + type: 'string', + description: 'The read operation to perform', + enum: ['get', 'get_schema', 'get_row', 'query_rows'], + }, + }, + required: ['operation', 'args'], + }, + resultSchema: { + type: 'object', + properties: { + data: { type: 'object', description: 'Operation-specific result payload.' }, + message: { type: 'string', description: 'Human-readable outcome summary.' }, + success: { type: 'boolean', description: 'Whether the operation succeeded.' }, + }, + required: ['success', 'message'], + }, +} + export const Read: ToolCatalogEntry = { id: 'read', name: 'read', @@ -3115,87 +3124,6 @@ export const Redeploy: ToolCatalogEntry = { requiredPermission: 'admin', } -export const RenameFile: ToolCatalogEntry = { - id: 'rename_file', - name: 'rename_file', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - newName: { - type: 'string', - description: - 'New filename including extension, e.g. "draft_v2.md". Use move_file to move files between folders.', - }, - path: { - type: 'string', - description: 'Canonical workspace file VFS path to rename, e.g. "files/Reports/draft.md".', - }, - }, - required: ['path', 'newName'], - }, - resultSchema: { - type: 'object', - properties: { - data: { type: 'object', description: 'Contains id and the new name.' }, - message: { type: 'string', description: 'Human-readable outcome.' }, - success: { type: 'boolean', description: 'Whether the rename succeeded.' }, - }, - required: ['success', 'message'], - }, - requiredPermission: 'write', -} - -export const RenameFileFolder: ToolCatalogEntry = { - id: 'rename_file_folder', - name: 'rename_file_folder', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - name: { type: 'string', description: 'New folder name.' }, - path: { - type: 'string', - description: 'Canonical folder VFS path to rename, e.g. "files/Reports/Old".', - }, - }, - required: ['path', 'name'], - }, - requiredPermission: 'write', -} - -export const RenameWorkflow: ToolCatalogEntry = { - id: 'rename_workflow', - name: 'rename_workflow', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - name: { type: 'string', description: 'The new name for the workflow.' }, - workflowId: { type: 'string', description: 'The workflow ID to rename.' }, - }, - required: ['workflowId', 'name'], - }, - requiredPermission: 'write', -} - -export const Research: ToolCatalogEntry = { - id: 'research', - name: 'research', - route: 'subagent', - mode: 'async', - parameters: { - properties: { topic: { description: 'The topic to research.', type: 'string' } }, - required: ['topic'], - type: 'object', - }, - subagentId: 'research', - internal: true, -} - export const Respond: ToolCatalogEntry = { id: 'respond', name: 'respond', @@ -3293,6 +3221,99 @@ export const RunBlock: ToolCatalogEntry = { clientExecutable: true, } +export const RunCode: ToolCatalogEntry = { + id: 'run_code', + name: 'run_code', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + code: { + type: 'string', + description: + 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with access to pre-installed CLI tools and workspace env vars as $VAR_NAME.', + }, + inputs: { + type: 'object', + description: + 'Workspace resources to mount into the sandbox. Copy paths verbatim from glob/read/grep output — they are percent-encoded per segment (spaces are %20, an in-name slash is %2F; parentheses and dots stay literal). Both the encoded path and the plain name resolve, so copy the returned path exactly rather than retyping or decoding it.', + properties: { + directories: { + type: 'array', + description: + 'Workspace folders to mount recursively into the sandbox, including nested files and empty folders.', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: + 'Canonical VFS folder path, e.g. "files/Reports". By default this mounts at "/home/user/{path}".', + }, + sandboxPath: { + type: 'string', + description: + 'Optional full sandbox directory path override. Omit to mount at /home/user/{path}.', + }, + }, + required: ['path'], + }, + }, + files: { + type: 'array', + description: 'Workspace files to mount into the sandbox.', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: + 'Canonical VFS file path, e.g. "files/Reports/sales.csv". By default this mounts at "/home/user/{path}".', + }, + sandboxPath: { + type: 'string', + description: + 'Full sandbox path to mount at, e.g. /home/user/inputs/data.csv. STRONGLY RECOMMENDED whenever the file name has spaces or special characters: the default mount path is the percent-ENCODED canonical path (e.g. /home/user/files/Q4%20Sales%20(Final).csv), which code using the human-readable name will not find. Set a simple sandboxPath and read exactly that.', + }, + }, + required: ['path'], + }, + }, + tables: { + type: 'array', + description: 'Workspace tables to mount as CSV files.', + items: { + type: 'object', + properties: { + path: { type: 'string', description: 'Canonical VFS table path when available.' }, + sandboxPath: { + type: 'string', + description: 'Optional full sandbox path for the mounted CSV.', + }, + tableId: { type: 'string', description: 'Workspace table ID.' }, + }, + }, + }, + }, + }, + language: { + type: 'string', + description: 'Execution language.', + enum: ['javascript', 'python', 'shell'], + }, + title: { + type: 'string', + description: + 'Short user-visible label for this execution, e.g. "Sum June invoices" or "Verify email formats".', + }, + }, + required: ['code'], + }, + requiredPermission: 'write', + capabilities: ['file_input', 'directory_input', 'table_input'], +} + export const RunFromBlock: ToolCatalogEntry = { id: 'run_from_block', name: 'run_from_block', @@ -3456,6 +3477,26 @@ export const ScrapePage: ToolCatalogEntry = { }, } +export const Search: ToolCatalogEntry = { + id: 'search', + name: 'search', + route: 'subagent', + mode: 'async', + parameters: { + properties: { + task: { + description: + "One short scoping sentence — the search agent has full conversation context. Example: 'find current Stripe metered-billing API limits' or 'count how many rows in the leads table have invalid emails'.", + type: 'string', + }, + }, + required: ['task'], + type: 'object', + }, + subagentId: 'search', + internal: true, +} + export const SearchDocumentation: ToolCatalogEntry = { id: 'search_documentation', name: 'search_documentation', @@ -3471,6 +3512,49 @@ export const SearchDocumentation: ToolCatalogEntry = { }, } +export const SearchKnowledgeBase: ToolCatalogEntry = { + id: 'search_knowledge_base', + name: 'search_knowledge_base', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + args: { + type: 'object', + description: 'Arguments for the operation', + properties: { + knowledgeBaseId: { + type: 'string', + description: 'Knowledge base ID (required for all operations)', + }, + query: { type: 'string', description: "Search query text (required for 'query')" }, + topK: { + type: 'number', + description: 'Number of results to return (1-50, default: 5)', + default: 5, + }, + }, + }, + operation: { + type: 'string', + description: 'The read operation to perform', + enum: ['get', 'query', 'list_tags'], + }, + }, + required: ['operation', 'args'], + }, + resultSchema: { + type: 'object', + properties: { + data: { type: 'object', description: 'Operation-specific result payload.' }, + message: { type: 'string', description: 'Human-readable outcome summary.' }, + success: { type: 'boolean', description: 'Whether the operation succeeded.' }, + }, + required: ['success', 'message'], + }, +} + export const SearchLibraryDocs: ToolCatalogEntry = { id: 'search_library_docs', name: 'search_library_docs', @@ -3755,50 +3839,6 @@ export const UpdateWorkspaceMcpServer: ToolCatalogEntry = { requiredPermission: 'admin', } -export const UserMemory: ToolCatalogEntry = { - id: 'user_memory', - name: 'user_memory', - route: 'go', - mode: 'sync', - parameters: { - type: 'object', - properties: { - confidence: { - type: 'number', - description: 'Confidence level 0-1 (default 1.0 for explicit, 0.8 for inferred)', - }, - correct_value: { - type: 'string', - description: - "The correct value to replace the wrong one (for 'correct' operation). Requires `key` (the memory to replace).", - }, - key: { - type: 'string', - description: "Unique key for the memory (e.g., 'preferred_model', 'slack_credential')", - }, - limit: { type: 'number', description: 'Number of results for search (default 10)' }, - memory_type: { - type: 'string', - description: "Type of memory: 'preference', 'entity', 'history', or 'correction'", - enum: ['preference', 'entity', 'history', 'correction'], - }, - operation: { - type: 'string', - description: "Operation: 'add', 'search', 'delete', 'correct', or 'list'", - enum: ['add', 'search', 'delete', 'correct', 'list'], - }, - query: { type: 'string', description: 'Search query to find relevant memories' }, - source: { - type: 'string', - description: "Source: 'explicit' (user told you) or 'inferred' (you observed)", - enum: ['explicit', 'inferred'], - }, - value: { type: 'string', description: 'Value to remember' }, - }, - required: ['operation'], - }, -} - export const UserTable: ToolCatalogEntry = { id: 'user_table', name: 'user_table', @@ -4380,21 +4420,13 @@ export const ManageCustomToolOperationValues = [ ] as const export const ManageFolderOperation = { - create: 'create', - rename: 'rename', - move: 'move', delete: 'delete', } as const export type ManageFolderOperation = (typeof ManageFolderOperation)[keyof typeof ManageFolderOperation] -export const ManageFolderOperationValues = [ - ManageFolderOperation.create, - ManageFolderOperation.rename, - ManageFolderOperation.move, - ManageFolderOperation.delete, -] as const +export const ManageFolderOperationValues = [ManageFolderOperation.delete] as const export const ManageMcpToolOperation = { add: 'add', @@ -4461,22 +4493,36 @@ export const MaterializeFileOperationValues = [ MaterializeFileOperation.import, ] as const -export const UserMemoryOperation = { - add: 'add', - search: 'search', - delete: 'delete', - correct: 'correct', - list: 'list', +export const QueryUserTableOperation = { + get: 'get', + getSchema: 'get_schema', + getRow: 'get_row', + queryRows: 'query_rows', +} as const + +export type QueryUserTableOperation = + (typeof QueryUserTableOperation)[keyof typeof QueryUserTableOperation] + +export const QueryUserTableOperationValues = [ + QueryUserTableOperation.get, + QueryUserTableOperation.getSchema, + QueryUserTableOperation.getRow, + QueryUserTableOperation.queryRows, +] as const + +export const SearchKnowledgeBaseOperation = { + get: 'get', + query: 'query', + listTags: 'list_tags', } as const -export type UserMemoryOperation = (typeof UserMemoryOperation)[keyof typeof UserMemoryOperation] +export type SearchKnowledgeBaseOperation = + (typeof SearchKnowledgeBaseOperation)[keyof typeof SearchKnowledgeBaseOperation] -export const UserMemoryOperationValues = [ - UserMemoryOperation.add, - UserMemoryOperation.search, - UserMemoryOperation.delete, - UserMemoryOperation.correct, - UserMemoryOperation.list, +export const SearchKnowledgeBaseOperationValues = [ + SearchKnowledgeBaseOperation.get, + SearchKnowledgeBaseOperation.query, + SearchKnowledgeBaseOperation.listTags, ] as const export const UserTableOperation = { @@ -4569,9 +4615,9 @@ export const TOOL_CATALOG: Record = { [Auth.id]: Auth, [CheckDeploymentStatus.id]: CheckDeploymentStatus, [CompleteScheduledTask.id]: CompleteScheduledTask, + [Cp.id]: Cp, [CrawlWebsite.id]: CrawlWebsite, [CreateFile.id]: CreateFile, - [CreateFileFolder.id]: CreateFileFolder, [CreateWorkflow.id]: CreateWorkflow, [CreateWorkspaceMcpServer.id]: CreateWorkspaceMcpServer, [DeleteFile.id]: DeleteFile, @@ -4607,7 +4653,6 @@ export const TOOL_CATALOG: Record = { [Grep.id]: Grep, [Knowledge.id]: Knowledge, [KnowledgeBase.id]: KnowledgeBase, - [ListFileFolders.id]: ListFileFolders, [ListIntegrationTools.id]: ListIntegrationTools, [ListUserWorkspaces.id]: ListUserWorkspaces, [ListWorkspaceMcpServers.id]: ListWorkspaceMcpServers, @@ -4621,30 +4666,29 @@ export const TOOL_CATALOG: Record = { [ManageSkill.id]: ManageSkill, [MaterializeFile.id]: MaterializeFile, [Media.id]: Media, - [MoveFile.id]: MoveFile, - [MoveFileFolder.id]: MoveFileFolder, - [MoveWorkflow.id]: MoveWorkflow, + [Mkdir.id]: Mkdir, + [Mv.id]: Mv, [OauthGetAuthLink.id]: OauthGetAuthLink, [OauthRequestAccess.id]: OauthRequestAccess, [OpenResource.id]: OpenResource, [PromoteToLive.id]: PromoteToLive, [QueryLogs.id]: QueryLogs, + [QueryUserTable.id]: QueryUserTable, [Read.id]: Read, [Redeploy.id]: Redeploy, - [RenameFile.id]: RenameFile, - [RenameFileFolder.id]: RenameFileFolder, - [RenameWorkflow.id]: RenameWorkflow, - [Research.id]: Research, [Respond.id]: Respond, [RestoreResource.id]: RestoreResource, [Run.id]: Run, [RunBlock.id]: RunBlock, + [RunCode.id]: RunCode, [RunFromBlock.id]: RunFromBlock, [RunWorkflow.id]: RunWorkflow, [RunWorkflowUntilBlock.id]: RunWorkflowUntilBlock, [ScheduledTask.id]: ScheduledTask, [ScrapePage.id]: ScrapePage, + [Search.id]: Search, [SearchDocumentation.id]: SearchDocumentation, + [SearchKnowledgeBase.id]: SearchKnowledgeBase, [SearchLibraryDocs.id]: SearchLibraryDocs, [SearchOnline.id]: SearchOnline, [SearchPatterns.id]: SearchPatterns, @@ -4656,7 +4700,6 @@ export const TOOL_CATALOG: Record = { [UpdateDeploymentVersion.id]: UpdateDeploymentVersion, [UpdateScheduledTaskHistory.id]: UpdateScheduledTaskHistory, [UpdateWorkspaceMcpServer.id]: UpdateWorkspaceMcpServer, - [UserMemory.id]: UserMemory, [UserTable.id]: UserTable, [Workflow.id]: Workflow, [WorkspaceFile.id]: WorkspaceFile, diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index dcaea0db6ea..f9bb621223e 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -61,6 +61,33 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + cp: { + parameters: { + type: 'object', + properties: { + destination: { + type: 'string', + description: + 'Target path under workflows/. An existing folder (or a path ending in "/") duplicates sources into it keeping their names; otherwise the last segment names the copy and the preceding segments are the target folder (created automatically when missing).', + }, + sources: { + type: 'array', + description: + 'Canonical workflow VFS paths to duplicate, e.g. ["workflows/My%20Workflow"]. Copy paths verbatim from glob/grep/read output.', + items: { + type: 'string', + }, + }, + toolTitle: { + type: 'string', + description: + 'Target-only UI phrase for the action row, e.g. "My Workflow" or "Template to Archive", not a full sentence like "Copying My Workflow".', + }, + }, + required: ['sources', 'destination', 'toolTitle'], + }, + resultSchema: undefined, + }, crawl_website: { parameters: { type: 'object', @@ -117,7 +144,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { files: { type: 'array', description: - 'Files to create or overwrite. Parent folders must already exist for create mode.', + 'Files to create or overwrite. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -162,24 +189,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - create_file_folder: { - parameters: { - type: 'object', - properties: { - path: { - type: 'string', - description: - 'Canonical folder VFS path to create, e.g. "files/Images" or "files/Reports/2026".', - }, - workspaceId: { - type: 'string', - description: 'Optional workspace ID. Defaults to the current workspace.', - }, - }, - required: ['path'], - }, - resultSchema: undefined, - }, create_workflow: { parameters: { type: 'object', @@ -726,7 +735,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { files: { type: 'array', description: - 'Files to create or overwrite. Parent folders must already exist for create mode.', + 'Files to create or overwrite. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -996,7 +1005,8 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { properties: { files: { type: 'array', - description: 'File outputs. Parent folders must already exist for create mode.', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -1163,7 +1173,8 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { properties: { files: { type: 'array', - description: 'File outputs. Parent folders must already exist for create mode.', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -1324,7 +1335,8 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { properties: { files: { type: 'array', - description: 'File outputs. Parent folders must already exist for create mode.', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -1461,7 +1473,8 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { properties: { files: { type: 'array', - description: 'File outputs. Parent folders must already exist for create mode.', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -1619,7 +1632,8 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { properties: { files: { type: 'array', - description: 'File outputs. Parent folders must already exist for create mode.', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', items: { type: 'object', properties: { @@ -2105,18 +2119,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - list_file_folders: { - parameters: { - type: 'object', - properties: { - workspaceId: { - type: 'string', - description: 'Optional workspace ID. Defaults to the current workspace.', - }, - }, - }, - resultSchema: undefined, - }, list_integration_tools: { parameters: { properties: { @@ -2300,35 +2302,20 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { parameters: { type: 'object', properties: { - destinationPath: { - type: 'string', - description: - 'Destination parent folder\'s VFS path for move/create. Omit (or pass "workflows") to target the workspace root.', - }, folderId: { type: 'string', description: 'Target folder ID, used as a fallback when path is not given. Readable from a contained workflow\'s meta.json "folderId".', }, - name: { - type: 'string', - description: - 'Folder name. Required for rename (the new name); for create when you pass a destination parent instead of a full path.', - }, operation: { type: 'string', description: 'The operation to perform.', - enum: ['create', 'rename', 'move', 'delete'], - }, - parentId: { - type: 'string', - description: - 'Destination parent folder ID, used as a fallback when destinationPath is not given.', + enum: ['delete'], }, path: { type: 'string', description: - 'Target folder\'s VFS path (e.g. "workflows/Marketing/Q3 Campaigns"), per-segment percent-encoded like every VFS path. Identifies the folder for rename/move/delete; for create it is the new folder\'s full path (its parent must already exist).', + 'Target folder\'s VFS path (e.g. "workflows/Marketing/Q3 Campaigns"), per-segment percent-encoded like every VFS path.', }, }, required: ['operation'], @@ -2532,62 +2519,52 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - move_file: { + mkdir: { parameters: { type: 'object', properties: { - destinationPath: { - type: 'string', - description: - 'Canonical target folder path, e.g. "files/Images". Omit or pass "files" for root.', - }, paths: { type: 'array', - description: 'Canonical workspace file VFS paths to move, e.g. ["files/photo.png"].', + description: + 'Canonical folder VFS paths to create, e.g. ["files/Reports/2026"]. Missing parent segments are created automatically.', items: { type: 'string', }, }, - }, - required: ['paths'], - }, - resultSchema: undefined, - }, - move_file_folder: { - parameters: { - type: 'object', - properties: { - destinationPath: { + toolTitle: { type: 'string', description: - 'Canonical target parent folder path, e.g. "files/Archive". Omit or pass "files" for root.', - }, - path: { - type: 'string', - description: 'Canonical folder VFS path to move, e.g. "files/Reports/2026".', + 'Target-only UI phrase for the action row, e.g. "Reports/2026" or "2 folders", not a full sentence like "Creating Reports".', }, }, - required: ['path'], + required: ['paths', 'toolTitle'], }, resultSchema: undefined, }, - move_workflow: { + mv: { parameters: { type: 'object', properties: { - folderId: { + destination: { type: 'string', - description: 'Target folder ID. Omit or pass empty string to move to workspace root.', + description: + 'Target path. A path ending in "/" (or naming an existing folder) moves sources into it keeping their names — always use the trailing "/" form when targeting a folder. Otherwise the last segment is the new name and the preceding segments are the target folder (created automatically when missing).', }, - workflowIds: { + sources: { type: 'array', - description: 'The workflow IDs to move.', + description: + 'Canonical VFS paths to move or rename, e.g. ["files/draft.md"]. All sources must share one category. Copy paths verbatim from glob/grep/read output.', items: { type: 'string', }, }, + toolTitle: { + type: 'string', + description: + 'Target-only UI phrase for the action row, e.g. "draft.md to Reports" or "3 files to Images", not a full sentence like "Moving draft.md".', + }, }, - required: ['workflowIds'], + required: ['sources', 'destination', 'toolTitle'], }, resultSchema: undefined, }, @@ -2595,6 +2572,11 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { parameters: { type: 'object', properties: { + credentialId: { + type: 'string', + description: + 'Optional. The id of an EXISTING credential (from environment/credentials.json) to reconnect/re-authorize in place. Only when the user explicitly asks to reconnect or repair that credential — never for adding another account.', + }, providerName: { type: 'string', description: @@ -2783,6 +2765,68 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + query_user_table: { + parameters: { + type: 'object', + properties: { + args: { + type: 'object', + description: 'Arguments for the operation', + properties: { + filter: { + type: 'object', + description: 'MongoDB-style filter for query_rows', + }, + limit: { + type: 'number', + description: 'Maximum rows to return (optional, default 100, max 1000 per call)', + }, + offset: { + type: 'number', + description: 'Number of rows to skip (optional for query_rows, default 0)', + }, + rowId: { + type: 'string', + description: 'Row ID (required for get_row)', + }, + sort: { + type: 'object', + description: + "Sort specification as { field: 'asc' | 'desc' } (optional for query_rows)", + }, + tableId: { + type: 'string', + description: 'Table ID (required for all operations)', + }, + }, + }, + operation: { + type: 'string', + description: 'The read operation to perform', + enum: ['get', 'get_schema', 'get_row', 'query_rows'], + }, + }, + required: ['operation', 'args'], + }, + resultSchema: { + type: 'object', + properties: { + data: { + type: 'object', + description: 'Operation-specific result payload.', + }, + message: { + type: 'string', + description: 'Human-readable outcome summary.', + }, + success: { + type: 'boolean', + description: 'Whether the operation succeeded.', + }, + }, + required: ['success', 'message'], + }, + }, read: { parameters: { type: 'object', @@ -2889,89 +2933,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - rename_file: { - parameters: { - type: 'object', - properties: { - newName: { - type: 'string', - description: - 'New filename including extension, e.g. "draft_v2.md". Use move_file to move files between folders.', - }, - path: { - type: 'string', - description: - 'Canonical workspace file VFS path to rename, e.g. "files/Reports/draft.md".', - }, - }, - required: ['path', 'newName'], - }, - resultSchema: { - type: 'object', - properties: { - data: { - type: 'object', - description: 'Contains id and the new name.', - }, - message: { - type: 'string', - description: 'Human-readable outcome.', - }, - success: { - type: 'boolean', - description: 'Whether the rename succeeded.', - }, - }, - required: ['success', 'message'], - }, - }, - rename_file_folder: { - parameters: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'New folder name.', - }, - path: { - type: 'string', - description: 'Canonical folder VFS path to rename, e.g. "files/Reports/Old".', - }, - }, - required: ['path', 'name'], - }, - resultSchema: undefined, - }, - rename_workflow: { - parameters: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'The new name for the workflow.', - }, - workflowId: { - type: 'string', - description: 'The workflow ID to rename.', - }, - }, - required: ['workflowId', 'name'], - }, - resultSchema: undefined, - }, - research: { - parameters: { - properties: { - topic: { - description: 'The topic to research.', - type: 'string', - }, - }, - required: ['topic'], - type: 'object', - }, - resultSchema: undefined, - }, respond: { parameters: { additionalProperties: true, @@ -3062,6 +3023,99 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + run_code: { + parameters: { + type: 'object', + properties: { + code: { + type: 'string', + description: + 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with access to pre-installed CLI tools and workspace env vars as $VAR_NAME.', + }, + inputs: { + type: 'object', + description: + 'Workspace resources to mount into the sandbox. Copy paths verbatim from glob/read/grep output — they are percent-encoded per segment (spaces are %20, an in-name slash is %2F; parentheses and dots stay literal). Both the encoded path and the plain name resolve, so copy the returned path exactly rather than retyping or decoding it.', + properties: { + directories: { + type: 'array', + description: + 'Workspace folders to mount recursively into the sandbox, including nested files and empty folders.', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: + 'Canonical VFS folder path, e.g. "files/Reports". By default this mounts at "/home/user/{path}".', + }, + sandboxPath: { + type: 'string', + description: + 'Optional full sandbox directory path override. Omit to mount at /home/user/{path}.', + }, + }, + required: ['path'], + }, + }, + files: { + type: 'array', + description: 'Workspace files to mount into the sandbox.', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: + 'Canonical VFS file path, e.g. "files/Reports/sales.csv". By default this mounts at "/home/user/{path}".', + }, + sandboxPath: { + type: 'string', + description: + 'Full sandbox path to mount at, e.g. /home/user/inputs/data.csv. STRONGLY RECOMMENDED whenever the file name has spaces or special characters: the default mount path is the percent-ENCODED canonical path (e.g. /home/user/files/Q4%20Sales%20(Final).csv), which code using the human-readable name will not find. Set a simple sandboxPath and read exactly that.', + }, + }, + required: ['path'], + }, + }, + tables: { + type: 'array', + description: 'Workspace tables to mount as CSV files.', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Canonical VFS table path when available.', + }, + sandboxPath: { + type: 'string', + description: 'Optional full sandbox path for the mounted CSV.', + }, + tableId: { + type: 'string', + description: 'Workspace table ID.', + }, + }, + }, + }, + }, + }, + language: { + type: 'string', + description: 'Execution language.', + enum: ['javascript', 'python', 'shell'], + }, + title: { + type: 'string', + description: + 'Short user-visible label for this execution, e.g. "Sum June invoices" or "Verify email formats".', + }, + }, + required: ['code'], + }, + resultSchema: undefined, + }, run_from_block: { parameters: { type: 'object', @@ -3209,6 +3263,20 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + search: { + parameters: { + properties: { + task: { + description: + "One short scoping sentence — the search agent has full conversation context. Example: 'find current Stripe metered-billing API limits' or 'count how many rows in the leads table have invalid emails'.", + type: 'string', + }, + }, + required: ['task'], + type: 'object', + }, + resultSchema: undefined, + }, search_documentation: { parameters: { type: 'object', @@ -3226,6 +3294,56 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + search_knowledge_base: { + parameters: { + type: 'object', + properties: { + args: { + type: 'object', + description: 'Arguments for the operation', + properties: { + knowledgeBaseId: { + type: 'string', + description: 'Knowledge base ID (required for all operations)', + }, + query: { + type: 'string', + description: "Search query text (required for 'query')", + }, + topK: { + type: 'number', + description: 'Number of results to return (1-50, default: 5)', + default: 5, + }, + }, + }, + operation: { + type: 'string', + description: 'The read operation to perform', + enum: ['get', 'query', 'list_tags'], + }, + }, + required: ['operation', 'args'], + }, + resultSchema: { + type: 'object', + properties: { + data: { + type: 'object', + description: 'Operation-specific result payload.', + }, + message: { + type: 'string', + description: 'Human-readable outcome summary.', + }, + success: { + type: 'boolean', + description: 'Whether the operation succeeded.', + }, + }, + required: ['success', 'message'], + }, + }, search_library_docs: { parameters: { type: 'object', @@ -3505,55 +3623,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - user_memory: { - parameters: { - type: 'object', - properties: { - confidence: { - type: 'number', - description: 'Confidence level 0-1 (default 1.0 for explicit, 0.8 for inferred)', - }, - correct_value: { - type: 'string', - description: - "The correct value to replace the wrong one (for 'correct' operation). Requires `key` (the memory to replace).", - }, - key: { - type: 'string', - description: "Unique key for the memory (e.g., 'preferred_model', 'slack_credential')", - }, - limit: { - type: 'number', - description: 'Number of results for search (default 10)', - }, - memory_type: { - type: 'string', - description: "Type of memory: 'preference', 'entity', 'history', or 'correction'", - enum: ['preference', 'entity', 'history', 'correction'], - }, - operation: { - type: 'string', - description: "Operation: 'add', 'search', 'delete', 'correct', or 'list'", - enum: ['add', 'search', 'delete', 'correct', 'list'], - }, - query: { - type: 'string', - description: 'Search query to find relevant memories', - }, - source: { - type: 'string', - description: "Source: 'explicit' (user told you) or 'inferred' (you observed)", - enum: ['explicit', 'inferred'], - }, - value: { - type: 'string', - description: 'Value to remember', - }, - }, - required: ['operation'], - }, - resultSchema: undefined, - }, user_table: { parameters: { type: 'object', diff --git a/apps/sim/lib/copilot/generated/trace-attributes-v1.ts b/apps/sim/lib/copilot/generated/trace-attributes-v1.ts index c5024ee3571..a892ff3561d 100644 --- a/apps/sim/lib/copilot/generated/trace-attributes-v1.ts +++ b/apps/sim/lib/copilot/generated/trace-attributes-v1.ts @@ -89,6 +89,12 @@ export const TraceAttr = { ChatArtifactKeys: 'chat.artifact_keys', ChatArtifactsBytes: 'chat.artifacts_bytes', ChatAuthType: 'chat.auth_type', + ChatBlobOp: 'chat.blob.op', + ChatBlobSite: 'chat.blob.site', + ChatBlobBytesOffloaded: 'chat.blob_bytes_offloaded', + ChatBlobBytesResolved: 'chat.blob_bytes_resolved', + ChatBlobsOffloaded: 'chat.blobs_offloaded', + ChatBlobsResolved: 'chat.blobs_resolved', ChatContextCount: 'chat.context_count', ChatContextUsage: 'chat.context_usage', ChatContinuationMessagesBefore: 'chat.continuation.messages_before', @@ -420,6 +426,7 @@ export const TraceAttr = { GenAiUsageCacheCreationTokens: 'gen_ai.usage.cache_creation_tokens', GenAiUsageCacheReadInputTokens: 'gen_ai.usage.cache_read.input_tokens', GenAiUsageCacheReadTokens: 'gen_ai.usage.cache_read_tokens', + GenAiUsageContextTokens: 'gen_ai.usage.context_tokens', GenAiUsageInputTokens: 'gen_ai.usage.input_tokens', GenAiUsageOutputTokens: 'gen_ai.usage.output_tokens', GenAiUsageThinkingTokens: 'gen_ai.usage.thinking_tokens', @@ -446,9 +453,11 @@ export const TraceAttr = { KnowledgeBaseId: 'knowledge_base.id', KnowledgeBaseName: 'knowledge_base.name', LlmBackend: 'llm.backend', + LlmCompactionPause: 'llm.compaction.pause', LlmErrorStage: 'llm.error_stage', LlmProtocol: 'llm.protocol', LlmRequestBodyBytes: 'llm.request.body_bytes', + LlmRequestCompactionTrigger: 'llm.request.compaction_trigger', LlmStreamBytes: 'llm.stream.bytes', LlmStreamChunks: 'llm.stream.chunks', LlmStreamFirstChunkBytes: 'llm.stream.first_chunk_bytes', @@ -716,6 +725,12 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'chat.artifact_keys', 'chat.artifacts_bytes', 'chat.auth_type', + 'chat.blob.op', + 'chat.blob.site', + 'chat.blob_bytes_offloaded', + 'chat.blob_bytes_resolved', + 'chat.blobs_offloaded', + 'chat.blobs_resolved', 'chat.context_count', 'chat.context_usage', 'chat.continuation.messages_before', @@ -1036,6 +1051,7 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'gen_ai.usage.cache_creation_tokens', 'gen_ai.usage.cache_read.input_tokens', 'gen_ai.usage.cache_read_tokens', + 'gen_ai.usage.context_tokens', 'gen_ai.usage.input_tokens', 'gen_ai.usage.output_tokens', 'gen_ai.usage.thinking_tokens', @@ -1062,9 +1078,11 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'knowledge_base.id', 'knowledge_base.name', 'llm.backend', + 'llm.compaction.pause', 'llm.error_stage', 'llm.protocol', 'llm.request.body_bytes', + 'llm.request.compaction_trigger', 'llm.stream.bytes', 'llm.stream.chunks', 'llm.stream.first_chunk_bytes', diff --git a/apps/sim/lib/copilot/request/tool-call-state.test.ts b/apps/sim/lib/copilot/request/tool-call-state.test.ts new file mode 100644 index 00000000000..7f5c3dd008a --- /dev/null +++ b/apps/sim/lib/copilot/request/tool-call-state.test.ts @@ -0,0 +1,56 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from 'vitest' +import { MothershipStreamV1ToolOutcome } from '@/lib/copilot/generated/mothership-stream-v1' +import type { ToolCallState } from '@/lib/copilot/request/types' +import { getToolCallTerminalData } from './tool-call-state' + +describe('getToolCallTerminalData', () => { + it('reduces a successful generate_api_key result to only its status message', () => { + const tool: ToolCallState = { + id: 't1', + name: 'generate_api_key', + status: MothershipStreamV1ToolOutcome.success, + result: { + success: true, + output: { + id: 'k1', + name: 'prod', + key: 'sk-sim-secret-value', + message: 'API key "prod" created.', + }, + }, + } + + const data = getToolCallTerminalData(tool) + + // The model gets only the status message — no key, no id/name/workspaceId. + expect(data).toBe('API key "prod" created.') + expect(JSON.stringify(data)).not.toContain('sk-sim-secret-value') + expect(JSON.stringify(data)).not.toContain('k1') + }) + + it('passes through other tools output unchanged', () => { + const tool: ToolCallState = { + id: 't2', + name: 'read', + status: MothershipStreamV1ToolOutcome.success, + result: { success: true, output: { content: 'file contents' } }, + } + + expect(getToolCallTerminalData(tool)).toEqual({ content: 'file contents' }) + }) + + it('surfaces the error for a failed generate_api_key without inventing a key', () => { + const tool: ToolCallState = { + id: 't3', + name: 'generate_api_key', + status: MothershipStreamV1ToolOutcome.error, + error: 'name is required', + } + + expect(getToolCallTerminalData(tool)).toEqual({ error: 'name is required' }) + }) +}) diff --git a/apps/sim/lib/copilot/request/tool-call-state.ts b/apps/sim/lib/copilot/request/tool-call-state.ts index 70433429043..e5f1b833bff 100644 --- a/apps/sim/lib/copilot/request/tool-call-state.ts +++ b/apps/sim/lib/copilot/request/tool-call-state.ts @@ -1,3 +1,4 @@ +import { toolResultForModel } from '@/lib/copilot/chat/sim-key-redaction' import { MothershipStreamV1ToolOutcome, type MothershipStreamV1ToolOutcome as TerminalToolCallStatus, @@ -57,17 +58,43 @@ export function requireToolCallError( } export function getToolCallTerminalData( + toolCall: Pick +): unknown { + // getToolCallTerminalData is the single producer of "what the model reads on + // resume", so it is where a tool's result is reduced to its model-facing form — + // e.g. generate_api_key yields only its status message; the generated key never + // reaches the model (it goes to the browser via the SSE tool result instead). + return toolResultForModel(toolCall.name, getToolCallTerminalDataRaw(toolCall)) +} + +function getToolCallTerminalDataRaw( toolCall: Pick ): unknown { const output = getToolCallStateOutput(toolCall) + const failed = !isSuccessfulToolCallStatus(toolCall.status as TerminalToolCallStatus) + if (output !== undefined) { - return output + if (!failed) { + return output + } + /** + * A failed call must always surface its error in the terminal data — this + * is what the model reads on resume. Handlers can fail with an + * empty-but-defined output (the app-tool executor's "Tool not found" ships + * `output: {}`), and preferring that output rendered failures as bare `{}`, + * so the model retried blind instead of reacting to the error. + */ + const error = + typeof toolCall.error === 'string' && toolCall.error.length > 0 + ? toolCall.error + : 'Tool failed without an error message' + if (output && typeof output === 'object' && !Array.isArray(output)) { + return 'error' in output ? output : { ...output, error } + } + return { output, error } } - if ( - toolCall.status === MothershipStreamV1ToolOutcome.success || - toolCall.status === MothershipStreamV1ToolOutcome.skipped - ) { + if (!failed) { return undefined } diff --git a/apps/sim/lib/copilot/request/tools/executor.ts b/apps/sim/lib/copilot/request/tools/executor.ts index f479d5e99b7..f7219ed6627 100644 --- a/apps/sim/lib/copilot/request/tools/executor.ts +++ b/apps/sim/lib/copilot/request/tools/executor.ts @@ -33,12 +33,13 @@ import { KnowledgeBase, MaterializeFile, Media, - Research, Run, RunBlock, + RunCode, RunFromBlock, RunWorkflow, RunWorkflowUntilBlock, + Search, WorkspaceFile, } from '@/lib/copilot/generated/tool-catalog-v1' import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' @@ -47,7 +48,6 @@ import { recordSimToolMetric } from '@/lib/copilot/request/metrics' import { withCopilotToolSpan } from '@/lib/copilot/request/otel' import { markToolResultSeen } from '@/lib/copilot/request/sse-utils' import { - getToolCallStateOutput, getToolCallTerminalData, requireToolCallError, setTerminalToolCallState, @@ -225,12 +225,13 @@ const LONG_RUNNING_TOOL_IDS: ReadonlySet = new Set([ RunWorkflow.id, RunWorkflowUntilBlock.id, FunctionExecute.id, + RunCode.id, GenerateImage.id, GenerateAudio.id, GenerateVideo.id, Ffmpeg.id, Media.id, - Research.id, + Search.id, CrawlWebsite.id, KnowledgeBase.id, DownloadToWorkspaceFile.id, @@ -347,7 +348,10 @@ function terminalCompletionFromToolCall(toolCall: ToolCallState): AsyncToolCompl } if (toolCall.status === MothershipStreamV1ToolOutcome.success) { - const data = getToolCallStateOutput(toolCall) + // getToolCallTerminalData (not raw output) so the completion signal carries + // the model-facing/redacted result — keeps the sim_key out of every path + // that consumes a completion, matching the error branch below. + const data = getToolCallTerminalData(toolCall) return buildCompletionSignal({ status: MothershipStreamV1ToolOutcome.success, message: 'Tool completed', @@ -356,7 +360,7 @@ function terminalCompletionFromToolCall(toolCall: ToolCallState): AsyncToolCompl } if (toolCall.status === MothershipStreamV1ToolOutcome.skipped) { - const data = getToolCallStateOutput(toolCall) + const data = getToolCallTerminalData(toolCall) return buildCompletionSignal({ status: MothershipStreamV1ToolOutcome.success, message: 'Tool skipped', @@ -667,7 +671,10 @@ async function executeToolAndReportInner( }) if (result.success) { - const raw = result.output + // Log the model-facing (redacted) view, not result.output — for + // generate_api_key the raw output carries the plaintext key, which must + // never reach application logs. + const raw = getToolCallTerminalData(toolCall) const preview = typeof raw === 'string' ? raw.slice(0, 200) diff --git a/apps/sim/lib/copilot/tool-executor/register-handlers.ts b/apps/sim/lib/copilot/tool-executor/register-handlers.ts index b39027e3526..4f6b21f5f1f 100644 --- a/apps/sim/lib/copilot/tool-executor/register-handlers.ts +++ b/apps/sim/lib/copilot/tool-executor/register-handlers.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { CheckDeploymentStatus, CompleteScheduledTask, + Cp as CpTool, CreateWorkflow, CreateWorkspaceMcpServer, DeleteWorkflow, @@ -32,16 +33,17 @@ import { ManageScheduledTask, ManageSkill, MaterializeFile, - MoveWorkflow, + Mkdir as MkdirTool, + Mv as MvTool, OauthGetAuthLink, OauthRequestAccess, OpenResource, PromoteToLive, Read as ReadTool, Redeploy, - RenameWorkflow, RestoreResource, RunBlock, + RunCode, RunFromBlock, RunWorkflow, RunWorkflowUntilBlock, @@ -87,7 +89,9 @@ import { executeOAuthGetAuthLink, executeOAuthRequestAccess } from '../tools/han import { executeGetPlatformActions } from '../tools/handlers/platform' import { executeOpenResource } from '../tools/handlers/resources' import { executeRestoreResource } from '../tools/handlers/restore-resource' +import { executeRunCode } from '../tools/handlers/run-code' import { executeVfsGlob, executeVfsGrep, executeVfsRead } from '../tools/handlers/vfs' +import { executeVfsCp, executeVfsMkdir, executeVfsMv } from '../tools/handlers/vfs-mutate' import { executeCreateWorkflow, executeDeleteWorkflow, @@ -142,9 +146,12 @@ function buildHandlerMap(): Record { [CreateWorkflow.id]: h(executeCreateWorkflow), [DeleteWorkflow.id]: h(executeDeleteWorkflow), - [RenameWorkflow.id]: h(executeRenameWorkflow), - [MoveWorkflow.id]: h(executeMoveWorkflow), [ManageFolder.id]: h(executeManageFolder), + // rename_workflow / move_workflow were removed from the mothership catalog + // in favor of mv; the executors stay registered under literal names so + // in-flight checkpoints still resume. Delete after the mv release soaks. + rename_workflow: h(executeRenameWorkflow), + move_workflow: h(executeMoveWorkflow), [RunWorkflow.id]: h(executeRunWorkflow), [RunWorkflowUntilBlock.id]: h(executeRunWorkflowUntilBlock), [RunFromBlock.id]: h(executeRunFromBlock), @@ -175,6 +182,9 @@ function buildHandlerMap(): Record { [GrepTool.id]: h(executeVfsGrep), [GlobTool.id]: h(executeVfsGlob), [ReadTool.id]: h(executeVfsRead), + [MvTool.id]: h(executeVfsMv), + [CpTool.id]: h(executeVfsCp), + [MkdirTool.id]: h(executeVfsMkdir), [ManageCustomTool.id]: h(executeManageCustomTool), [ManageMcpTool.id]: h(executeManageMcpTool), @@ -188,6 +198,7 @@ function buildHandlerMap(): Record { [ListIntegrationTools.id]: h(executeListIntegrationTools), [MaterializeFile.id]: h(executeMaterializeFile), [FunctionExecute.id]: h(executeFunctionExecute), + [RunCode.id]: h(executeRunCode), ...buildServerToolHandlers(), } diff --git a/apps/sim/lib/copilot/tools/handlers/access.ts b/apps/sim/lib/copilot/tools/handlers/access.ts index 7391a829ead..2f5d592269e 100644 --- a/apps/sim/lib/copilot/tools/handlers/access.ts +++ b/apps/sim/lib/copilot/tools/handlers/access.ts @@ -1,6 +1,6 @@ import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import type { getWorkflowById } from '@/lib/workflows/utils' -import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' +import { checkWorkspaceAccess, type WorkspaceAccess } from '@/lib/workspaces/permissions/utils' import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' type WorkflowRecord = NonNullable>> @@ -47,22 +47,23 @@ export async function ensureWorkspaceAccess( workspaceId: string, userId: string, level: 'read' | 'write' | 'admin' = 'read' -): Promise { +): Promise { const access = await checkWorkspaceAccess(workspaceId, userId) if (!access.exists || !access.hasAccess) { throw new Error(`Workspace ${workspaceId} not found`) } - if (level === 'read') return + if (level === 'read') return access if (level === 'admin') { if (!access.canAdmin) { throw new Error('Admin access required for this workspace') } - return + return access } if (!access.canWrite) { throw new Error('Write or admin access required for this workspace') } + return access } diff --git a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts index a514f6f735c..7f2a8d64930 100644 --- a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts +++ b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts @@ -46,7 +46,14 @@ async function executeSave(fileName: string, chatId: string): Promise ({ + mockEnsureWorkspaceAccess: vi.fn(), + mockGetCredentialActorContext: vi.fn(), +})) + +vi.mock('@/lib/copilot/tools/handlers/access', () => ({ + ensureWorkspaceAccess: mockEnsureWorkspaceAccess, +})) + +vi.mock('@/lib/credentials/access', () => ({ + getCredentialActorContext: mockGetCredentialActorContext, +})) + +vi.mock('@/lib/oauth/utils', () => ({ + getAllOAuthServices: vi.fn(() => [ + { providerId: 'google-email', name: 'Gmail' }, + { providerId: 'slack', name: 'Slack' }, + { providerId: 'trello', name: 'Trello' }, + { providerId: 'shopify', name: 'Shopify' }, + ]), +})) + +import type { ExecutionContext } from '@/lib/copilot/request/types' +import { executeOAuthGetAuthLink } from '@/lib/copilot/tools/handlers/oauth' + +const BASE_URL = 'https://sim.test' +const WORKSPACE_ID = 'ws-1' +const USER_ID = 'user-1' +const CREDENTIAL_ID = 'cred-1' + +const context = { + workspaceId: WORKSPACE_ID, + userId: USER_ID, + chatId: 'chat-1', +} as unknown as ExecutionContext + +const WORKSPACE_ACCESS = { + exists: true, + hasAccess: true, + canWrite: true, + canAdmin: false, + workspace: { id: WORKSPACE_ID }, +} + +function oauthCredentialActor(overrides: Record = {}) { + return { + credential: { + id: CREDENTIAL_ID, + workspaceId: WORKSPACE_ID, + type: 'oauth', + providerId: 'google-email', + ...((overrides.credential as Record) ?? {}), + }, + member: null, + hasWorkspaceAccess: true, + canWriteWorkspace: true, + isAdmin: true, + ...Object.fromEntries(Object.entries(overrides).filter(([key]) => key !== 'credential')), + } +} + +describe('executeOAuthGetAuthLink', () => { + beforeEach(() => { + vi.clearAllMocks() + process.env.NEXT_PUBLIC_APP_URL = BASE_URL + mockEnsureWorkspaceAccess.mockResolvedValue(WORKSPACE_ACCESS) + }) + + describe('connect (no credentialId)', () => { + it('returns an authorize URL without a credentialId param', async () => { + const result = await executeOAuthGetAuthLink({ providerName: 'google-email' }, context) + + expect(result.success).toBe(true) + const url = new URL((result.output as { oauth_url: string }).oauth_url) + expect(url.pathname).toBe('/api/auth/oauth2/authorize') + expect(url.searchParams.get('providerId')).toBe('google-email') + expect(url.searchParams.get('credentialId')).toBeNull() + expect(mockGetCredentialActorContext).not.toHaveBeenCalled() + }) + }) + + describe('reconnect (credentialId passed)', () => { + it('returns an authorize URL carrying the credentialId and a reconnect message', async () => { + mockGetCredentialActorContext.mockResolvedValue(oauthCredentialActor()) + + const result = await executeOAuthGetAuthLink( + { providerName: 'google-email', credentialId: CREDENTIAL_ID }, + context + ) + + expect(result.success).toBe(true) + const output = result.output as { oauth_url: string; message: string } + const url = new URL(output.oauth_url) + expect(url.searchParams.get('credentialId')).toBe(CREDENTIAL_ID) + expect(output.message).toContain('Reconnect') + expect(output.message).toContain(CREDENTIAL_ID) + }) + + it('reuses the already-resolved workspace access for the credential lookup', async () => { + mockGetCredentialActorContext.mockResolvedValue(oauthCredentialActor()) + + await executeOAuthGetAuthLink( + { providerName: 'google-email', credentialId: CREDENTIAL_ID }, + context + ) + + expect(mockGetCredentialActorContext).toHaveBeenCalledWith(CREDENTIAL_ID, USER_ID, { + workspaceAccess: WORKSPACE_ACCESS, + }) + }) + + it('fails with an agent-visible error for a nonexistent credential', async () => { + mockGetCredentialActorContext.mockResolvedValue({ + credential: null, + member: null, + hasWorkspaceAccess: false, + canWriteWorkspace: false, + isAdmin: false, + }) + + const result = await executeOAuthGetAuthLink( + { providerName: 'google-email', credentialId: 'cred-hallucinated' }, + context + ) + + expect(result.success).toBe(false) + expect(result.error).toContain('not found in this workspace') + }) + + it('fails when the credential belongs to another workspace', async () => { + mockGetCredentialActorContext.mockResolvedValue( + oauthCredentialActor({ credential: { workspaceId: 'ws-other' } }) + ) + + const result = await executeOAuthGetAuthLink( + { providerName: 'google-email', credentialId: CREDENTIAL_ID }, + context + ) + + expect(result.success).toBe(false) + expect(result.error).toContain('not found in this workspace') + }) + + it('fails when the credential is not an OAuth credential', async () => { + mockGetCredentialActorContext.mockResolvedValue( + oauthCredentialActor({ credential: { type: 'env_workspace' } }) + ) + + const result = await executeOAuthGetAuthLink( + { providerName: 'google-email', credentialId: CREDENTIAL_ID }, + context + ) + + expect(result.success).toBe(false) + expect(result.error).toContain('not an OAuth credential') + }) + + it('fails naming the actual provider when providerName does not match the credential', async () => { + mockGetCredentialActorContext.mockResolvedValue(oauthCredentialActor()) + + const result = await executeOAuthGetAuthLink( + { providerName: 'slack', credentialId: CREDENTIAL_ID }, + context + ) + + expect(result.success).toBe(false) + expect(result.error).toContain('google-email') + }) + + it('fails when the caller is not a credential admin', async () => { + mockGetCredentialActorContext.mockResolvedValue(oauthCredentialActor({ isAdmin: false })) + + const result = await executeOAuthGetAuthLink( + { providerName: 'google-email', credentialId: CREDENTIAL_ID }, + context + ) + + expect(result.success).toBe(false) + expect(result.error).toContain('Admin access') + }) + + it('rejects reconnect for Trello and directs the user to the integrations page', async () => { + const result = await executeOAuthGetAuthLink( + { providerName: 'trello', credentialId: CREDENTIAL_ID }, + context + ) + + expect(result.success).toBe(false) + expect(result.error).toContain('integrations page') + expect(mockGetCredentialActorContext).not.toHaveBeenCalled() + }) + + it('rejects reconnect for Shopify and directs the user to the integrations page', async () => { + const result = await executeOAuthGetAuthLink( + { providerName: 'shopify', credentialId: CREDENTIAL_ID }, + context + ) + + expect(result.success).toBe(false) + expect(result.error).toContain('integrations page') + expect(mockGetCredentialActorContext).not.toHaveBeenCalled() + }) + }) +}) diff --git a/apps/sim/lib/copilot/tools/handlers/oauth.ts b/apps/sim/lib/copilot/tools/handlers/oauth.ts index 3919b0ba557..f0a5f9dbbb0 100644 --- a/apps/sim/lib/copilot/tools/handlers/oauth.ts +++ b/apps/sim/lib/copilot/tools/handlers/oauth.ts @@ -2,32 +2,44 @@ import { toError } from '@sim/utils/errors' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { ensureWorkspaceAccess } from '@/lib/copilot/tools/handlers/access' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getCredentialActorContext } from '@/lib/credentials/access' import { getAllOAuthServices } from '@/lib/oauth/utils' +import type { WorkspaceAccess } from '@/lib/workspaces/permissions/utils' export async function executeOAuthGetAuthLink( rawParams: Record, context: ExecutionContext ): Promise { const providerName = String(rawParams.providerName || rawParams.provider_name || '') + const rawCredentialId = rawParams.credentialId || rawParams.credential_id + const credentialId = rawCredentialId ? String(rawCredentialId) : undefined const baseUrl = getBaseUrl() try { if (!context.workspaceId || !context.userId) { throw new Error('workspaceId and userId are required to generate an OAuth link') } - await ensureWorkspaceAccess(context.workspaceId, context.userId, 'write') + const workspaceAccess = await ensureWorkspaceAccess( + context.workspaceId, + context.userId, + 'write' + ) const result = await generateOAuthLink( context.workspaceId, context.workflowId, context.chatId, providerName, - baseUrl + baseUrl, + credentialId ? { credentialId, userId: context.userId, workspaceAccess } : undefined ) + const action = credentialId ? 'reconnect' : 'connect' return { success: true, output: { - message: `Authorization URL generated for ${result.serviceName}.`, + message: credentialId + ? `Reconnect authorization URL generated for ${result.serviceName}. Completing it re-authorizes credential ${credentialId} in place — its id stays the same.` + : `Authorization URL generated for ${result.serviceName}.`, oauth_url: result.url, - instructions: `Open this URL in your browser to connect ${result.serviceName}: ${result.url}`, + instructions: `Open this URL in your browser to ${action} ${result.serviceName}: ${result.url}`, provider: result.serviceName, providerId: result.providerId, }, @@ -72,13 +84,20 @@ export async function executeOAuthRequestAccess( * calls Better Auth, so the draft's TTL starts at click and the signed `state` * cookie is planted in the user's browser and the OAuth callback's state check * passes. + * + * When `reconnect` is set, the URL carries the existing credential id so the + * authorize endpoint creates a reconnect draft and the OAuth callback rebinds + * the credential in place instead of creating a new one. Validation happens + * here too (not just at click time) so a bad id fails in the tool result where + * the agent can see it, rather than as a silent browser redirect. */ async function generateOAuthLink( workspaceId: string | undefined, workflowId: string | undefined, chatId: string | undefined, providerName: string, - baseUrl: string + baseUrl: string, + reconnect?: { credentialId: string; userId: string; workspaceAccess: WorkspaceAccess } ): Promise<{ url: string; providerId: string; serviceName: string }> { if (!workspaceId) { throw new Error('workspaceId is required to generate an OAuth link') @@ -105,6 +124,39 @@ async function generateOAuthLink( } const { providerId, name: serviceName } = matched + + if (reconnect) { + if (providerId === 'trello' || providerId === 'shopify') { + throw new Error( + `Reconnect is not supported for ${serviceName} from chat. Ask the user to open the ` + + `integrations page and press Reconnect on the credential there.` + ) + } + const actor = await getCredentialActorContext(reconnect.credentialId, reconnect.userId, { + workspaceAccess: reconnect.workspaceAccess, + }) + if (!actor.credential || actor.credential.workspaceId !== workspaceId) { + throw new Error( + `Credential "${reconnect.credentialId}" was not found in this workspace. Read ` + + `environment/credentials.json for valid credential ids.` + ) + } + if (actor.credential.type !== 'oauth') { + throw new Error( + `Credential "${reconnect.credentialId}" is not an OAuth credential and cannot be reconnected.` + ) + } + if (actor.credential.providerId !== providerId) { + throw new Error( + `Credential "${reconnect.credentialId}" belongs to provider "${actor.credential.providerId}", ` + + `not "${providerId}". Pass the matching providerName.` + ) + } + if (!actor.isAdmin) { + throw new Error('Admin access on the credential is required to reconnect it.') + } + } + const callbackURL = workflowId && workspaceId ? `${baseUrl}/workspace/${workspaceId}/w/${workflowId}` @@ -138,6 +190,9 @@ async function generateOAuthLink( authorizeUrl.searchParams.set('providerId', providerId) authorizeUrl.searchParams.set('workspaceId', workspaceId) authorizeUrl.searchParams.set('callbackURL', callbackURL) + if (reconnect) { + authorizeUrl.searchParams.set('credentialId', reconnect.credentialId) + } return { url: authorizeUrl.toString(), providerId, serviceName } } diff --git a/apps/sim/lib/copilot/tools/handlers/run-code.ts b/apps/sim/lib/copilot/tools/handlers/run-code.ts new file mode 100644 index 00000000000..e27babc4512 --- /dev/null +++ b/apps/sim/lib/copilot/tools/handlers/run-code.ts @@ -0,0 +1,31 @@ +import type { ToolExecutionContext, ToolExecutionResult } from '@/lib/copilot/tool-executor/types' +import { executeFunctionExecute } from '@/lib/copilot/tools/handlers/function-execute' + +/** + * Compute-only variant of function_execute for info-gathering agents: same + * sandbox and inputs, but it must never create or overwrite workspace + * resources. The write vectors (outputs.files, outputTable) are rejected here + * on top of the Go executor's fail-fast guard; run_code is also absent from + * the name-gated output post-processors (OUTPUT_PATH_TOOLS etc.), so even a + * leaked arg could not write anything. + */ +export async function executeRunCode( + params: Record, + context: ToolExecutionContext +): Promise { + if ('outputs' in params) { + return { + success: false, + error: + 'run_code is compute-only: outputs (workspace file writes) is not available; return the data and report it instead', + } + } + if ('outputTable' in params) { + return { + success: false, + error: + 'run_code is compute-only: outputTable (workspace table overwrite) is not available; return the data and report it instead', + } + } + return executeFunctionExecute(params, context) +} diff --git a/apps/sim/lib/copilot/tools/handlers/vfs-mutate.test.ts b/apps/sim/lib/copilot/tools/handlers/vfs-mutate.test.ts new file mode 100644 index 00000000000..30847fdcc07 --- /dev/null +++ b/apps/sim/lib/copilot/tools/handlers/vfs-mutate.test.ts @@ -0,0 +1,509 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + ensureWorkspaceAccess: vi.fn(), + ensureWorkflowAccess: vi.fn(), + getDefaultWorkspaceId: vi.fn(), + assertFolderMutable: vi.fn(), + assertWorkflowMutable: vi.fn(), + getWorkspaceFileByName: vi.fn(), + findWorkspaceFileFolderIdByPath: vi.fn(), + ensureWorkspaceFileFolderPath: vi.fn(), + performMoveRenameWorkspaceFile: vi.fn(), + performUpdateWorkspaceFileFolder: vi.fn(), + performCreateFolder: vi.fn(), + performUpdateFolder: vi.fn(), + performUpdateWorkflow: vi.fn(), + duplicateWorkflow: vi.fn(), + listFolders: vi.fn(), + verifyFolderWorkspace: vi.fn(), + listTables: vi.fn(), + renameTable: vi.fn(), + getKnowledgeBases: vi.fn(), + updateKnowledgeBase: vi.fn(), + checkKnowledgeBaseWriteAccess: vi.fn(), + workflowRows: vi.fn(), +})) + +vi.mock('@sim/db', () => ({ + db: { + select: () => ({ + from: () => ({ + where: () => Promise.resolve(mocks.workflowRows()), + }), + }), + }, + workflow: { id: 'id', name: 'name', folderId: 'folderId', workspaceId: 'workspaceId' }, +})) + +vi.mock('@sim/platform-authz/workflow', () => ({ + assertFolderMutable: mocks.assertFolderMutable, + assertWorkflowMutable: mocks.assertWorkflowMutable, +})) + +vi.mock('@/lib/copilot/tools/handlers/access', () => ({ + ensureWorkspaceAccess: mocks.ensureWorkspaceAccess, + ensureWorkflowAccess: mocks.ensureWorkflowAccess, + getDefaultWorkspaceId: mocks.getDefaultWorkspaceId, +})) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ + getWorkspaceFileByName: mocks.getWorkspaceFileByName, +})) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-folder-manager', () => ({ + findWorkspaceFileFolderIdByPath: mocks.findWorkspaceFileFolderIdByPath, + ensureWorkspaceFileFolderPath: mocks.ensureWorkspaceFileFolderPath, + normalizeWorkspaceFileItemName: vi.fn((name: string) => name.trim()), +})) + +vi.mock('@/lib/workspace-files/orchestration', () => ({ + performMoveRenameWorkspaceFile: mocks.performMoveRenameWorkspaceFile, + performUpdateWorkspaceFileFolder: mocks.performUpdateWorkspaceFileFolder, +})) + +vi.mock('@/lib/workflows/orchestration', () => ({ + performCreateFolder: mocks.performCreateFolder, + performUpdateFolder: mocks.performUpdateFolder, + performUpdateWorkflow: mocks.performUpdateWorkflow, +})) + +vi.mock('@/lib/workflows/persistence/duplicate', () => ({ + duplicateWorkflow: mocks.duplicateWorkflow, +})) + +vi.mock('@/lib/workflows/utils', () => ({ + listFolders: mocks.listFolders, + verifyFolderWorkspace: mocks.verifyFolderWorkspace, +})) + +vi.mock('@/lib/table/service', () => ({ + listTables: mocks.listTables, + renameTable: mocks.renameTable, +})) + +vi.mock('@/lib/knowledge/service', () => ({ + getKnowledgeBases: mocks.getKnowledgeBases, + updateKnowledgeBase: mocks.updateKnowledgeBase, +})) + +vi.mock('@/app/api/knowledge/utils', () => ({ + checkKnowledgeBaseWriteAccess: mocks.checkKnowledgeBaseWriteAccess, +})) + +import type { ExecutionContext } from '@/lib/copilot/request/types' +import { executeVfsCp, executeVfsMkdir, executeVfsMv } from './vfs-mutate' + +const context = { userId: 'user-1', workspaceId: 'ws-1' } as ExecutionContext + +describe('vfs mv/cp', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.ensureWorkspaceAccess.mockResolvedValue(undefined) + mocks.ensureWorkflowAccess.mockResolvedValue({ workspaceId: 'ws-1', workflow: {} }) + mocks.assertFolderMutable.mockResolvedValue(undefined) + mocks.assertWorkflowMutable.mockResolvedValue(undefined) + mocks.verifyFolderWorkspace.mockResolvedValue(true) + mocks.listFolders.mockResolvedValue([]) + mocks.workflowRows.mockReturnValue([]) + mocks.getWorkspaceFileByName.mockResolvedValue(null) + mocks.findWorkspaceFileFolderIdByPath.mockResolvedValue(null) + mocks.ensureWorkspaceFileFolderPath.mockResolvedValue('ensured-folder') + }) + + describe('category rules', () => { + it('rejects cross-category moves', async () => { + const result = await executeVfsMv( + { sources: ['files/report.pdf'], destination: 'workflows/report' }, + context + ) + expect(result.success).toBe(false) + expect(result.error).toContain('across categories') + }) + + it('rejects uploads with a materialize_file pointer', async () => { + const result = await executeVfsMv( + { sources: ['uploads/data.csv'], destination: 'files/data.csv' }, + context + ) + expect(result.success).toBe(false) + expect(result.error).toContain('materialize_file') + }) + + it('rejects read-only categories', async () => { + const result = await executeVfsMv( + { sources: ['components/blocks/gmail.json'], destination: 'components/blocks/g.json' }, + context + ) + expect(result.success).toBe(false) + expect(result.error).toContain('not a movable resource') + }) + + it('aborts before mutating when the request was cancelled', async () => { + const abortedContext = { + userId: 'user-1', + workspaceId: 'ws-1', + abortSignal: { aborted: true }, + } as unknown as ExecutionContext + const result = await executeVfsMv( + { sources: ['files/a.md'], destination: 'files/b.md' }, + abortedContext + ) + expect(result.success).toBe(false) + expect(result.error).toContain('aborted') + expect(mocks.performMoveRenameWorkspaceFile).not.toHaveBeenCalled() + }) + }) + + describe('files', () => { + it('moves and renames a file in one call, auto-creating destination folders', async () => { + mocks.getWorkspaceFileByName.mockResolvedValue({ id: 'file-1', name: 'draft.md' }) + mocks.performMoveRenameWorkspaceFile.mockResolvedValue({ + success: true, + file: { id: 'file-1', name: 'final.md' }, + }) + + const result = await executeVfsMv( + { sources: ['files/draft.md'], destination: 'files/Reports/2026/final.md' }, + context + ) + + expect(mocks.getWorkspaceFileByName).toHaveBeenCalledWith('ws-1', 'draft.md', { + folderId: null, + }) + expect(mocks.ensureWorkspaceFileFolderPath).toHaveBeenCalledWith({ + workspaceId: 'ws-1', + userId: 'user-1', + pathSegments: ['Reports', '2026'], + }) + expect(mocks.performMoveRenameWorkspaceFile).toHaveBeenCalledWith({ + workspaceId: 'ws-1', + userId: 'user-1', + fileId: 'file-1', + targetFolderId: 'ensured-folder', + newName: 'final.md', + }) + expect(result.success).toBe(true) + expect(result.output).toMatchObject({ + results: [{ from: 'files/draft.md', to: 'files/Reports/2026/final.md', kind: 'file' }], + }) + }) + + it('moves into an existing folder keeping the name without creating anything', async () => { + mocks.findWorkspaceFileFolderIdByPath.mockResolvedValue('folder-images') + mocks.getWorkspaceFileByName.mockResolvedValue({ id: 'file-1', name: 'a.png' }) + mocks.performMoveRenameWorkspaceFile.mockResolvedValue({ + success: true, + file: { id: 'file-1', name: 'a.png' }, + }) + + const result = await executeVfsMv( + { sources: ['files/a.png'], destination: 'files/Images' }, + context + ) + + expect(mocks.performMoveRenameWorkspaceFile).toHaveBeenCalledWith( + expect.objectContaining({ targetFolderId: 'folder-images', newName: 'a.png' }) + ) + expect(mocks.ensureWorkspaceFileFolderPath).not.toHaveBeenCalled() + expect(result.success).toBe(true) + expect(result.output).toMatchObject({ results: [{ to: 'files/Images/a.png' }] }) + }) + + it('requires a folder destination for multiple sources', async () => { + const result = await executeVfsMv( + { sources: ['files/a.png', 'files/b.png'], destination: 'files/Images/c.png' }, + context + ) + expect(result.success).toBe(false) + expect(result.error).toContain('must be a folder') + }) + + it('resolves sources at their exact path only — no cross-folder name fallback', async () => { + mocks.getWorkspaceFileByName.mockResolvedValue(null) + mocks.findWorkspaceFileFolderIdByPath.mockResolvedValue(null) + + const result = await executeVfsMv( + { sources: ['files/report.pdf'], destination: 'files/Archive/' }, + context + ) + + expect(result.success).toBe(false) + expect(result.error).toContain('Not found') + expect(mocks.performMoveRenameWorkspaceFile).not.toHaveBeenCalled() + expect(mocks.ensureWorkspaceFileFolderPath).not.toHaveBeenCalled() + }) + + it('rejects copying workspace files — cp is workflows-only', async () => { + mocks.getWorkspaceFileByName.mockResolvedValue({ id: 'file-1', name: 'template.md' }) + + const result = await executeVfsCp( + { sources: ['files/template.md'], destination: 'files/Reports/january.md' }, + context + ) + + expect(result.success).toBe(false) + expect(result.error).toContain('cp only duplicates workflows') + expect(mocks.ensureWorkspaceFileFolderPath).not.toHaveBeenCalled() + }) + + it('moves and renames a file folder via performUpdateWorkspaceFileFolder', async () => { + mocks.findWorkspaceFileFolderIdByPath + .mockResolvedValueOnce(null) // destination is not an existing folder + .mockResolvedValueOnce('folder-src') // source resolves as folder + mocks.performUpdateWorkspaceFileFolder.mockResolvedValue({ + success: true, + folder: { name: 'Reports 2025' }, + }) + + const result = await executeVfsMv( + { sources: ['files/Reports'], destination: 'files/Archive/Reports 2025' }, + context + ) + + expect(mocks.performUpdateWorkspaceFileFolder).toHaveBeenCalledWith({ + workspaceId: 'ws-1', + folderId: 'folder-src', + userId: 'user-1', + name: 'Reports 2025', + parentId: 'ensured-folder', + }) + expect(result.success).toBe(true) + }) + + it('rejects reserved alias backing paths', async () => { + const result = await executeVfsMv( + { sources: ['files/.plans/wf_1/launch.md'], destination: 'files/launch.md' }, + context + ) + expect(result.success).toBe(false) + expect(result.error).toContain('Reserved system paths') + }) + }) + + describe('workflows', () => { + it('renames a workflow at root', async () => { + mocks.workflowRows.mockReturnValue([{ id: 'wf-1', name: 'Old Name', folderId: null }]) + mocks.performUpdateWorkflow.mockResolvedValue({ success: true }) + + const result = await executeVfsMv( + { sources: ['workflows/Old%20Name'], destination: 'workflows/New Name' }, + context + ) + + expect(mocks.assertWorkflowMutable).toHaveBeenCalledWith('wf-1') + expect(mocks.performUpdateWorkflow).toHaveBeenCalledWith( + expect.objectContaining({ workflowId: 'wf-1', name: 'New Name', folderId: null }) + ) + expect(result.success).toBe(true) + expect(result.output).toMatchObject({ results: [{ to: 'workflows/New%20Name' }] }) + }) + + it('moves a workflow into an existing folder keeping its name', async () => { + mocks.listFolders.mockResolvedValue([ + { folderId: 'fold-1', folderName: 'Archive', parentId: null }, + ]) + mocks.workflowRows.mockReturnValue([{ id: 'wf-1', name: 'My Workflow', folderId: null }]) + mocks.performUpdateWorkflow.mockResolvedValue({ success: true }) + + const result = await executeVfsMv( + { sources: ['workflows/My%20Workflow'], destination: 'workflows/Archive' }, + context + ) + + expect(mocks.assertFolderMutable).toHaveBeenCalledWith('fold-1') + expect(mocks.performUpdateWorkflow).toHaveBeenCalledWith( + expect.objectContaining({ workflowId: 'wf-1', name: undefined, folderId: 'fold-1' }) + ) + expect(result.success).toBe(true) + }) + + it('surfaces locked-workflow rejections per item', async () => { + mocks.workflowRows.mockReturnValue([{ id: 'wf-1', name: 'Locked One', folderId: null }]) + mocks.assertWorkflowMutable.mockRejectedValue(new Error('Workflow is locked')) + + const result = await executeVfsMv( + { sources: ['workflows/Locked%20One'], destination: 'workflows/Renamed' }, + context + ) + + expect(result.success).toBe(false) + expect(result.error).toContain('locked') + }) + + it('duplicates a workflow with cp (locked source allowed)', async () => { + mocks.workflowRows.mockReturnValue([{ id: 'wf-1', name: 'Template', folderId: null }]) + mocks.duplicateWorkflow.mockResolvedValue({ id: 'wf-2', name: 'My Copy' }) + + const result = await executeVfsCp( + { sources: ['workflows/Template'], destination: 'workflows/My Copy' }, + context + ) + + expect(mocks.assertWorkflowMutable).not.toHaveBeenCalled() + expect(mocks.duplicateWorkflow).toHaveBeenCalledWith( + expect.objectContaining({ + sourceWorkflowId: 'wf-1', + workspaceId: 'ws-1', + folderId: null, + name: 'My Copy', + }) + ) + expect(result.success).toBe(true) + expect(result.output).toMatchObject({ results: [{ to: 'workflows/My%20Copy', id: 'wf-2' }] }) + }) + + it('rejects copying workflow folders', async () => { + mocks.listFolders.mockResolvedValue([ + { folderId: 'fold-1', folderName: 'Projects', parentId: null }, + ]) + const result = await executeVfsCp( + { sources: ['workflows/Projects'], destination: 'workflows/Projects Copy' }, + context + ) + expect(result.success).toBe(false) + expect(result.error).toContain('cannot be copied') + }) + + it('moves and renames a workflow folder', async () => { + mocks.listFolders.mockResolvedValue([ + { folderId: 'fold-1', folderName: 'Q1', parentId: null }, + { folderId: 'fold-2', folderName: 'Archive', parentId: null }, + ]) + mocks.performUpdateFolder.mockResolvedValue({ success: true }) + + const result = await executeVfsMv( + { sources: ['workflows/Q1'], destination: 'workflows/Archive/Q1 2026' }, + context + ) + + expect(mocks.performUpdateFolder).toHaveBeenCalledWith( + expect.objectContaining({ folderId: 'fold-1', name: 'Q1 2026', parentId: 'fold-2' }) + ) + expect(result.success).toBe(true) + }) + }) + + describe('mkdir', () => { + it('creates a nested file folder chain', async () => { + const result = await executeVfsMkdir({ paths: ['files/Reports/2026'] }, context) + + expect(mocks.ensureWorkspaceFileFolderPath).toHaveBeenCalledWith({ + workspaceId: 'ws-1', + userId: 'user-1', + pathSegments: ['Reports', '2026'], + }) + expect(result.success).toBe(true) + expect(result.output).toMatchObject({ + results: [{ from: 'files/Reports/2026', to: 'files/Reports/2026', kind: 'file_folder' }], + }) + }) + + it('creates a workflow folder via performCreateFolder', async () => { + mocks.listFolders.mockResolvedValue([]) + mocks.performCreateFolder.mockResolvedValue({ success: true, folder: { id: 'fold-new' } }) + + const result = await executeVfsMkdir({ paths: ['workflows/Archive'] }, context) + + expect(mocks.performCreateFolder).toHaveBeenCalledWith({ + workspaceId: 'ws-1', + userId: 'user-1', + name: 'Archive', + parentId: undefined, + }) + expect(result.success).toBe(true) + expect(result.output).toMatchObject({ + results: [{ to: 'workflows/Archive', kind: 'workflow_folder', id: 'fold-new' }], + }) + }) + + it('rejects flat namespaces and reserved paths', async () => { + const result = await executeVfsMkdir({ paths: ['tables/CRM', 'files/.plans/wf_1'] }, context) + expect(result.success).toBe(false) + expect(result.output).toMatchObject({ + results: [ + { from: 'tables/CRM', error: expect.stringContaining('flat namespace') }, + { from: 'files/.plans/wf_1', error: expect.stringContaining('Reserved') }, + ], + }) + expect(mocks.ensureWorkspaceFileFolderPath).not.toHaveBeenCalled() + }) + + it('rejects creation inside a locked workflow folder', async () => { + mocks.listFolders.mockResolvedValue([]) + mocks.assertFolderMutable.mockRejectedValue(new Error('Folder is locked')) + + const result = await executeVfsMkdir({ paths: ['workflows/Locked/Sub'] }, context) + + expect(result.success).toBe(false) + expect(result.error).toContain('locked') + expect(mocks.performCreateFolder).not.toHaveBeenCalled() + }) + }) + + describe('tables and knowledge bases (flat namespaces)', () => { + it('renames a table', async () => { + mocks.listTables.mockResolvedValue([{ id: 'tbl-1', name: 'Leads' }]) + mocks.renameTable.mockResolvedValue({ id: 'tbl-1', name: 'Customers' }) + + const result = await executeVfsMv( + { sources: ['tables/Leads'], destination: 'tables/Customers' }, + context + ) + + expect(mocks.renameTable).toHaveBeenCalledWith('tbl-1', 'Customers', expect.any(String)) + expect(result.success).toBe(true) + expect(result.output).toMatchObject({ results: [{ to: 'tables/Customers', kind: 'table' }] }) + }) + + it('rejects nested table destinations as flat-namespace violations', async () => { + const result = await executeVfsMv( + { sources: ['tables/Leads'], destination: 'tables/CRM/Leads' }, + context + ) + expect(result.success).toBe(false) + expect(result.error).toContain('flat namespace') + expect(mocks.renameTable).not.toHaveBeenCalled() + }) + + it('rejects copying tables', async () => { + const result = await executeVfsCp( + { sources: ['tables/Leads'], destination: 'tables/Leads Copy' }, + context + ) + expect(result.success).toBe(false) + expect(result.error).toContain('cannot be copied') + }) + + it('renames a knowledge base after a write-access check', async () => { + mocks.getKnowledgeBases.mockResolvedValue([{ id: 'kb-1', name: 'Docs' }]) + mocks.checkKnowledgeBaseWriteAccess.mockResolvedValue({ hasAccess: true }) + mocks.updateKnowledgeBase.mockResolvedValue({ id: 'kb-1', name: 'Product Docs' }) + + const result = await executeVfsMv( + { sources: ['knowledgebases/Docs'], destination: 'knowledgebases/Product Docs' }, + context + ) + + expect(mocks.checkKnowledgeBaseWriteAccess).toHaveBeenCalledWith('kb-1', 'user-1') + expect(mocks.updateKnowledgeBase).toHaveBeenCalledWith( + 'kb-1', + { name: 'Product Docs' }, + expect.any(String) + ) + expect(result.success).toBe(true) + }) + + it('rejects the reserved knowledgebases/connectors name', async () => { + const result = await executeVfsMv( + { sources: ['knowledgebases/Docs'], destination: 'knowledgebases/connectors' }, + context + ) + expect(result.success).toBe(false) + expect(result.error).toContain('reserved') + }) + }) +}) diff --git a/apps/sim/lib/copilot/tools/handlers/vfs-mutate.ts b/apps/sim/lib/copilot/tools/handlers/vfs-mutate.ts new file mode 100644 index 00000000000..44682d3e615 --- /dev/null +++ b/apps/sim/lib/copilot/tools/handlers/vfs-mutate.ts @@ -0,0 +1,768 @@ +import { db, workflow as workflowTable } from '@sim/db' +import { createLogger } from '@sim/logger' +import { assertFolderMutable, assertWorkflowMutable } from '@sim/platform-authz/workflow' +import { toError } from '@sim/utils/errors' +import { eq } from 'drizzle-orm' +import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { + ensureWorkflowAccess, + ensureWorkspaceAccess, + getDefaultWorkspaceId, +} from '@/lib/copilot/tools/handlers/access' +import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment' +import { + buildVfsFolderPathMap, + canonicalWorkflowVfsDir, + decodeVfsPathSegments, + encodeVfsPathSegments, +} from '@/lib/copilot/vfs/path-utils' +import { isWorkflowAliasBackingPath } from '@/lib/copilot/vfs/workflow-aliases' +import { generateRequestId } from '@/lib/core/utils/request' +import { getKnowledgeBases, updateKnowledgeBase } from '@/lib/knowledge/service' +import { listTables, renameTable } from '@/lib/table/service' +import { + ensureWorkspaceFileFolderPath, + findWorkspaceFileFolderIdByPath, + normalizeWorkspaceFileItemName, +} from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' +import { + getWorkspaceFileByName, + type WorkspaceFileRecord, +} from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { + performCreateFolder, + performUpdateFolder, + performUpdateWorkflow, +} from '@/lib/workflows/orchestration' +import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate' +import { listFolders, verifyFolderWorkspace } from '@/lib/workflows/utils' +import { + performMoveRenameWorkspaceFile, + performUpdateWorkspaceFileFolder, +} from '@/lib/workspace-files/orchestration' +import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' + +const logger = createLogger('VfsMutateTools') + +type MutateVerb = 'mv' | 'cp' + +type MutateCategory = 'files' | 'workflows' | 'tables' | 'knowledgebases' + +const MUTATE_CATEGORIES = new Set(['files', 'workflows', 'tables', 'knowledgebases']) + +const CATEGORY_REJECTIONS: Record = { + uploads: + 'uploads/ files are chat-scoped and immutable. Use materialize_file to promote one into files/ first.', + 'recently-deleted': + 'recently-deleted/ items cannot be moved or copied. Restore them with restore_resource first.', +} + +interface VfsMutateOutcome { + from: string + to?: string + kind: 'file' | 'file_folder' | 'workflow' | 'workflow_folder' | 'table' | 'knowledge_base' + id?: string + error?: string +} + +/** Top-level VFS segment of a raw (possibly encoded) path. */ +function topLevelSegment(path: string): string { + return path.trim().replace(/^\/+/, '').split('/')[0] ?? '' +} + +function classifyCategory(path: string): { category: MutateCategory } | { error: string } { + const top = topLevelSegment(path) + if (MUTATE_CATEGORIES.has(top)) return { category: top as MutateCategory } + const rejection = CATEGORY_REJECTIONS[top] + if (rejection) return { error: rejection } + return { + error: `"${path}" is not a movable resource. Only files/, workflows/, tables/, and knowledgebases/ paths are supported.`, + } +} + +function normalizeSources(raw: unknown): string[] { + if (typeof raw === 'string') return raw.trim() ? [raw.trim()] : [] + if (!Array.isArray(raw)) return [] + return raw.filter((s): s is string => typeof s === 'string' && s.trim().length > 0) +} + +function hasTrailingSlash(path: string): boolean { + return /\/\s*$/.test(path) +} + +function assertMutationNotAborted(context: ExecutionContext): void { + if (context.abortSignal?.aborted) { + throw new Error('Request aborted before the mutation could be applied.') + } +} + +function buildResult(verb: MutateVerb | 'mkdir', outcomes: VfsMutateOutcome[]): ToolCallResult { + const failed = outcomes.filter((o) => o.error) + if (failed.length === outcomes.length) { + return { + success: false, + error: failed[0]?.error || `${verb} failed`, + output: { results: outcomes }, + } + } + return { success: true, output: { results: outcomes } } +} + +export async function executeVfsMv( + params: Record, + context: ExecutionContext +): Promise { + return executeVfsMutate('mv', params, context) +} + +export async function executeVfsCp( + params: Record, + context: ExecutionContext +): Promise { + return executeVfsMutate('cp', params, context) +} + +/** + * mkdir -p over the VFS: creates each folder path (missing parents included) + * under files/ or workflows/. Existing folders are not an error. + */ +export async function executeVfsMkdir( + params: Record, + context: ExecutionContext +): Promise { + try { + const paths = normalizeSources(params.paths) + if (paths.length === 0) { + return { success: false, error: 'paths is required (an array of folder VFS paths)' } + } + + const workspaceId = context.workspaceId || (await getDefaultWorkspaceId(context.userId)) + await ensureWorkspaceAccess(workspaceId, context.userId, 'write') + assertMutationNotAborted(context) + + let ensureWorkflowFolder: ((segments: string[]) => Promise) | undefined + + const outcomes: VfsMutateOutcome[] = [] + for (const path of paths) { + const top = topLevelSegment(path) + const segments = decodeVfsPathSegments(path).slice(1) + const kind = top === 'workflows' ? 'workflow_folder' : 'file_folder' + + if (top !== 'files' && top !== 'workflows') { + const rejection = + top === 'tables' || top === 'knowledgebases' + ? `${top}/ is a flat namespace with no folders.` + : (CATEGORY_REJECTIONS[top] ?? + `"${path}" is not a folder target. mkdir supports files/ and workflows/ paths.`) + outcomes.push({ from: path, kind, error: rejection }) + continue + } + if (segments.length === 0) { + outcomes.push({ from: path, kind, error: 'Path must include at least one folder segment' }) + continue + } + if (top === 'files' && isWorkflowAliasBackingPath(path)) { + outcomes.push({ from: path, kind, error: `Reserved system path: ${path}` }) + continue + } + + try { + assertMutationNotAborted(context) + let folderId: string | null + if (top === 'files') { + folderId = await ensureWorkspaceFileFolderPath({ + workspaceId, + userId: context.userId, + pathSegments: segments, + }) + } else { + ensureWorkflowFolder ??= makeWorkflowFolderEnsurer( + workspaceId, + context.userId, + await loadWorkflowFolderIndex(workspaceId) + ) + folderId = await ensureWorkflowFolder(segments) + } + outcomes.push({ + from: path, + to: `${top}/${encodeVfsPathSegments(segments)}`, + kind, + id: folderId ?? undefined, + }) + } catch (error) { + outcomes.push({ from: path, kind, error: toError(error).message }) + } + } + + return buildResult('mkdir', outcomes) + } catch (error) { + return { success: false, error: toError(error).message } + } +} + +async function executeVfsMutate( + verb: MutateVerb, + params: Record, + context: ExecutionContext +): Promise { + try { + const sources = normalizeSources(params.sources) + const destination = typeof params.destination === 'string' ? params.destination.trim() : '' + if (sources.length === 0) { + return { success: false, error: 'sources is required (an array of canonical VFS paths)' } + } + if (!destination) { + return { success: false, error: 'destination is required' } + } + + const workspaceId = context.workspaceId || (await getDefaultWorkspaceId(context.userId)) + await ensureWorkspaceAccess(workspaceId, context.userId, 'write') + assertMutationNotAborted(context) + + const classified = classifyCategory(sources[0]) + if ('error' in classified) return { success: false, error: classified.error } + const { category } = classified + + for (const source of sources.slice(1)) { + const other = classifyCategory(source) + if ('error' in other) return { success: false, error: other.error } + if (other.category !== category) { + return { + success: false, + error: `All sources must share one category; got ${category}/ and ${other.category}/.`, + } + } + } + + const destTop = topLevelSegment(destination) + if (destTop !== category) { + return { + success: false, + error: `Cannot ${verb} across categories: ${category}/ sources cannot target "${destination}". Resources stay within their category.`, + } + } + + switch (category) { + case 'files': + return await mutateWorkspaceFiles(verb, sources, destination, context, workspaceId) + case 'workflows': + return await mutateWorkflows(verb, sources, destination, context, workspaceId) + default: + return await renameFlatResource(verb, category, sources, destination, context, workspaceId) + } + } catch (error) { + return { success: false, error: toError(error).message } + } +} + +interface DestinationPlan { + /** True when sources move INTO the destination folder keeping their names. */ + dirMode: boolean + /** Decoded display-name segments of the destination folder. */ + folderSegments: string[] + /** New leaf name; set only when `dirMode` is false. */ + leafName?: string + /** + * Resolve the destination folder id, creating missing folders on first call. + * Deferred and memoized so nothing is created until a source is confirmed + * valid — a fully-failed mv/cp must not leave folders behind. + */ + ensureFolderId: () => Promise +} + +/** + * Shared destination interpretation for every category with folders: an + * existing folder (or a trailing "/") means move/copy INTO it keeping names; + * otherwise the last segment is the new name and the preceding segments are + * the target folder. Folder creation is deferred to `ensureFolderId`. + */ +async function planDestination(args: { + destination: string + sourceCount: number + lookupFolder: (segments: string[]) => Promise + ensureFolderPath: (segments: string[]) => Promise +}): Promise { + const rest = decodeVfsPathSegments(args.destination).slice(1) + const plan = ( + dirMode: boolean, + folderSegments: string[], + leafName?: string, + knownFolderId?: string | null + ): DestinationPlan => { + let memo: Promise | undefined + return { + dirMode, + folderSegments, + leafName, + ensureFolderId: () => + (memo ??= + knownFolderId !== undefined + ? Promise.resolve(knownFolderId) + : folderSegments.length > 0 + ? args.ensureFolderPath(folderSegments) + : Promise.resolve(null)), + } + } + + if (rest.length === 0) return plan(true, [], undefined, null) + if (hasTrailingSlash(args.destination)) return plan(true, rest) + const existing = await args.lookupFolder(rest) + if (existing) return plan(true, rest, undefined, existing) + if (args.sourceCount > 1) { + return { + error: `With multiple sources the destination must be a folder. "${args.destination}" does not exist — end it with "/" to create it.`, + } + } + return plan(false, rest.slice(0, -1), rest.at(-1) as string) +} + +/** + * Resolve a `files/...` source to the file at EXACTLY that path (folder- + * anchored). Deliberately not the lenient read-side resolver — on a + * destructive path a bare-name fallback could match a file in a different + * folder than the one named. + */ +async function resolveFileAtExactPath( + workspaceId: string, + segments: string[] +): Promise { + const fileName = normalizeWorkspaceFileItemName(segments.at(-1) ?? '', 'File') + if (segments.length === 1) { + return getWorkspaceFileByName(workspaceId, fileName, { folderId: null }) + } + const folderId = await findWorkspaceFileFolderIdByPath(workspaceId, segments.slice(0, -1)) + if (!folderId) return null + return getWorkspaceFileByName(workspaceId, fileName, { folderId }) +} + +async function mutateWorkspaceFiles( + verb: MutateVerb, + sources: string[], + destination: string, + context: ExecutionContext, + workspaceId: string +): Promise { + if (verb === 'cp') { + return { + success: false, + error: 'Workspace files cannot be copied — cp only duplicates workflows.', + } + } + for (const path of [...sources, destination]) { + if (isWorkflowAliasBackingPath(path)) { + return { + success: false, + error: `Reserved system paths cannot be moved or renamed: ${path}`, + } + } + } + + const dest = await planDestination({ + destination, + sourceCount: sources.length, + lookupFolder: (segments) => findWorkspaceFileFolderIdByPath(workspaceId, segments), + ensureFolderPath: (segments) => + ensureWorkspaceFileFolderPath({ + workspaceId, + userId: context.userId, + pathSegments: segments, + }), + }) + if ('error' in dest) return { success: false, error: dest.error } + + // Resolve every source read-only before mutating anything, so a fully + // invalid call cannot create destination folders as a side effect. + type SourceRef = + | { source: string; file: WorkspaceFileRecord } + | { source: string; folderId: string } + | { source: string; error: string } + const refs: SourceRef[] = [] + for (const source of sources) { + const segments = decodeVfsPathSegments(source).slice(1) + if (segments.length === 0) { + refs.push({ source, error: 'Source must name a file or folder under files/' }) + continue + } + const file = await resolveFileAtExactPath(workspaceId, segments) + if (file) { + refs.push({ source, file }) + continue + } + const folderId = await findWorkspaceFileFolderIdByPath(workspaceId, segments) + if (folderId) refs.push({ source, folderId }) + else refs.push({ source, error: `Not found: ${source}` }) + } + + const outcomes: VfsMutateOutcome[] = [] + for (const ref of refs) { + if ('error' in ref) { + outcomes.push({ from: ref.source, kind: 'file', error: ref.error }) + continue + } + + if ('file' in ref) { + assertMutationNotAborted(context) + const targetName = dest.dirMode ? ref.file.name : (dest.leafName as string) + const targetFolderId = await dest.ensureFolderId() + const result = await performMoveRenameWorkspaceFile({ + workspaceId, + userId: context.userId, + fileId: ref.file.id, + targetFolderId, + newName: targetName, + }) + outcomes.push( + result.success && result.file + ? { + from: ref.source, + to: `files/${encodeVfsPathSegments([...dest.folderSegments, result.file.name])}`, + kind: 'file', + id: ref.file.id, + } + : { from: ref.source, kind: 'file', error: result.error || 'Failed to move file' } + ) + continue + } + + assertMutationNotAborted(context) + const targetFolderId = await dest.ensureFolderId() + if (targetFolderId === ref.folderId) { + outcomes.push({ + from: ref.source, + kind: 'file_folder', + error: 'Cannot move a folder into itself', + }) + continue + } + const result = await performUpdateWorkspaceFileFolder({ + workspaceId, + folderId: ref.folderId, + userId: context.userId, + name: dest.dirMode ? undefined : dest.leafName, + parentId: targetFolderId, + }) + outcomes.push( + result.success && result.folder + ? { + from: ref.source, + to: `files/${encodeVfsPathSegments([...dest.folderSegments, result.folder.name])}`, + kind: 'file_folder', + id: ref.folderId, + } + : { from: ref.source, kind: 'file_folder', error: result.error || 'Failed to move folder' } + ) + } + + return buildResult(verb, outcomes) +} + +interface WorkflowFolderIndex { + folderPathById: Map + folderIdByPath: Map +} + +async function loadWorkflowFolderIndex(workspaceId: string): Promise { + const folderPathById = buildVfsFolderPathMap(await listFolders(workspaceId)) + const folderIdByPath = new Map() + for (const [id, path] of folderPathById.entries()) folderIdByPath.set(path, id) + return { folderPathById, folderIdByPath } +} + +/** + * mkdir -p for workflow folders: resolves each segment against the index, + * creating missing ones (locked parents rejected) and keeping the index maps + * current so later paths in the same call see the new folders. + */ +function makeWorkflowFolderEnsurer( + workspaceId: string, + userId: string, + index: WorkflowFolderIndex +): (segments: string[]) => Promise { + return async (segments) => { + let parentId: string | null = null + let pathSoFar = '' + for (const segment of segments) { + pathSoFar = pathSoFar + ? `${pathSoFar}/${encodeVfsPathSegments([segment])}` + : encodeVfsPathSegments([segment]) + const existing = index.folderIdByPath.get(pathSoFar) + if (existing) { + parentId = existing + continue + } + await assertFolderMutable(parentId) + const created = await performCreateFolder({ + workspaceId, + userId, + name: segment, + parentId: parentId ?? undefined, + }) + if (!created.success || !created.folder) { + throw new Error(created.error || `Failed to create workflow folder "${segment}"`) + } + index.folderIdByPath.set(pathSoFar, created.folder.id) + index.folderPathById.set(created.folder.id, pathSoFar) + parentId = created.folder.id + } + return parentId + } +} + +async function mutateWorkflows( + verb: MutateVerb, + sources: string[], + destination: string, + context: ExecutionContext, + workspaceId: string +): Promise { + const index = await loadWorkflowFolderIndex(workspaceId) + const { folderPathById, folderIdByPath } = index + + const workflowRows = await db + .select({ + id: workflowTable.id, + name: workflowTable.name, + folderId: workflowTable.folderId, + }) + .from(workflowTable) + .where(eq(workflowTable.workspaceId, workspaceId)) + const workflowByPath = new Map() + for (const row of workflowRows) { + const dir = canonicalWorkflowVfsDir({ + name: row.name, + folderPath: row.folderId ? folderPathById.get(row.folderId) : null, + }) + if (!workflowByPath.has(dir)) workflowByPath.set(dir, row) + } + + const ensureWorkflowFolderPath = makeWorkflowFolderEnsurer(workspaceId, context.userId, index) + + const dest = await planDestination({ + destination, + sourceCount: sources.length, + lookupFolder: async (segments) => folderIdByPath.get(encodeVfsPathSegments(segments)) ?? null, + ensureFolderPath: ensureWorkflowFolderPath, + }) + if ('error' in dest) return { success: false, error: dest.error } + if (!dest.dirMode && (dest.leafName as string).length > 200) { + return { success: false, error: 'Workflow name must be 200 characters or less' } + } + + // Resolve every source against the in-memory maps before mutating anything. + type SourceRef = + | { source: string; workflow: (typeof workflowRows)[number] } + | { source: string; folderId: string } + | { source: string; error: string } + const refs: SourceRef[] = [] + for (const source of sources) { + const segments = decodeVfsPathSegments(source).slice(1) + if (segments.length === 0) { + refs.push({ source, error: 'Source must name a workflow or folder under workflows/' }) + continue + } + const encoded = encodeVfsPathSegments(segments) + const wf = workflowByPath.get(`workflows/${encoded}`) + if (wf) { + refs.push({ source, workflow: wf }) + continue + } + const folderId = folderIdByPath.get(encoded) + if (folderId) refs.push({ source, folderId }) + else refs.push({ source, error: `Not found: ${source}` }) + } + + const outcomes: VfsMutateOutcome[] = [] + for (const ref of refs) { + if ('error' in ref) { + outcomes.push({ from: ref.source, kind: 'workflow', error: ref.error }) + continue + } + + if ('workflow' in ref) { + const wf = ref.workflow + const targetName = dest.dirMode ? wf.name : (dest.leafName as string) + try { + assertMutationNotAborted(context) + if (verb === 'cp') { + const targetFolderId = await dest.ensureFolderId() + const duplicated = await duplicateWorkflow({ + sourceWorkflowId: wf.id, + userId: context.userId, + workspaceId, + folderId: targetFolderId, + name: targetName, + requestId: generateRequestId(), + }) + outcomes.push({ + from: ref.source, + to: `workflows/${encodeVfsPathSegments([...dest.folderSegments, duplicated.name])}`, + kind: 'workflow', + id: duplicated.id, + }) + } else { + await ensureWorkflowAccess(wf.id, context.userId, 'write') + await assertWorkflowMutable(wf.id) + const targetFolderId = await dest.ensureFolderId() + await assertFolderMutable(targetFolderId) + if (targetFolderId && !(await verifyFolderWorkspace(targetFolderId, workspaceId))) { + outcomes.push({ + from: ref.source, + kind: 'workflow', + error: 'Destination folder not found', + }) + continue + } + const result = await performUpdateWorkflow({ + workflowId: wf.id, + userId: context.userId, + workspaceId, + currentName: wf.name, + currentFolderId: wf.folderId, + name: dest.dirMode ? undefined : targetName, + folderId: targetFolderId, + }) + outcomes.push( + result.success + ? { + from: ref.source, + to: `workflows/${encodeVfsPathSegments([...dest.folderSegments, targetName])}`, + kind: 'workflow', + id: wf.id, + } + : { + from: ref.source, + kind: 'workflow', + error: result.error || 'Failed to move workflow', + } + ) + } + } catch (error) { + outcomes.push({ from: ref.source, kind: 'workflow', error: toError(error).message }) + } + continue + } + + if (verb === 'cp') { + outcomes.push({ + from: ref.source, + kind: 'workflow_folder', + error: 'Workflow folders cannot be copied.', + }) + continue + } + try { + assertMutationNotAborted(context) + await assertFolderMutable(ref.folderId) + const targetFolderId = await dest.ensureFolderId() + if (targetFolderId === ref.folderId) { + outcomes.push({ + from: ref.source, + kind: 'workflow_folder', + error: 'Cannot move a folder into itself', + }) + continue + } + await assertFolderMutable(targetFolderId) + const result = await performUpdateFolder({ + folderId: ref.folderId, + workspaceId, + userId: context.userId, + name: dest.dirMode ? undefined : dest.leafName, + parentId: targetFolderId, + }) + const finalLeaf = dest.dirMode + ? (decodeVfsPathSegments(ref.source).slice(1).at(-1) ?? '') + : (dest.leafName as string) + outcomes.push( + result.success + ? { + from: ref.source, + to: `workflows/${encodeVfsPathSegments([...dest.folderSegments, finalLeaf])}`, + kind: 'workflow_folder', + id: ref.folderId, + } + : { + from: ref.source, + kind: 'workflow_folder', + error: result.error || 'Failed to move folder', + } + ) + } catch (error) { + outcomes.push({ from: ref.source, kind: 'workflow_folder', error: toError(error).message }) + } + } + + return buildResult(verb, outcomes) +} + +async function renameFlatResource( + verb: MutateVerb, + category: 'tables' | 'knowledgebases', + sources: string[], + destination: string, + context: ExecutionContext, + workspaceId: string +): Promise { + const label = category === 'tables' ? 'Tables' : 'Knowledge bases' + const kind = category === 'tables' ? 'table' : 'knowledge_base' + + if (verb === 'cp') { + return { success: false, error: `${label} cannot be copied — duplication is not supported.` } + } + if (sources.length > 1) { + return { success: false, error: `${label} are renamed one at a time.` } + } + + const sourceSegments = decodeVfsPathSegments(sources[0]).slice(1) + const destSegments = decodeVfsPathSegments(destination).slice(1) + if (sourceSegments.length !== 1 || destSegments.length !== 1 || hasTrailingSlash(destination)) { + return { + success: false, + error: `${label} have a flat namespace with no folders — mv only renames them, e.g. mv({sources: ["${category}/Old Name"], destination: "${category}/New Name"}).`, + } + } + + const sourceName = sourceSegments[0] + const newName = destSegments[0] + const canonicalSource = normalizeVfsSegment(sourceName) + + if (category === 'tables') { + const tables = await listTables(workspaceId) + const match = tables.find((t) => normalizeVfsSegment(t.name) === canonicalSource) + if (!match) { + return { success: false, error: `Table not found at ${sources[0]}` } + } + assertMutationNotAborted(context) + const renamed = await renameTable(match.id, newName, generateRequestId()) + return buildResult(verb, [ + { + from: sources[0], + to: `tables/${normalizeVfsSegment(renamed.name)}`, + kind, + id: match.id, + }, + ]) + } + + if (newName.toLowerCase() === 'connectors') { + return { success: false, error: '"knowledgebases/connectors" is a reserved path.' } + } + const kbs = await getKnowledgeBases(context.userId, workspaceId) + const match = kbs.find((kb) => normalizeVfsSegment(kb.name) === canonicalSource) + if (!match) { + return { success: false, error: `Knowledge base not found at ${sources[0]}` } + } + const access = await checkKnowledgeBaseWriteAccess(match.id, context.userId) + if (!access.hasAccess) { + return { + success: false, + error: `Write access required to rename knowledge base "${match.name}"`, + } + } + assertMutationNotAborted(context) + await updateKnowledgeBase(match.id, { name: newName }, generateRequestId()) + logger.info('Renamed knowledge base via mv', { knowledgeBaseId: match.id, workspaceId }) + return buildResult(verb, [ + { from: sources[0], to: `knowledgebases/${normalizeVfsSegment(newName)}`, kind, id: match.id }, + ]) +} diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index 2e0f7fc5300..98bf953d7fd 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts @@ -853,8 +853,7 @@ export async function executeGenerateApiKey( name: result.key.name, key: result.key.key, workspaceId, - message: - 'API key created successfully. Copy this key now — it will not be shown again. Use this key in the x-api-key header when calling workflow API endpoints.', + message: `API key "${result.key.name}" created. You did NOT receive the key value — Sim reveals it to the user ONLY through the secure, copyable chip it renders where you place a {"type":"sim_key"} tag, so you MUST emit that tag now or the user can never see the key (it cannot be shown again). Never print, guess, or fabricate a value. The key authenticates calls to deployed workflow endpoints via the x-api-key header.`, }, } } catch (error) { diff --git a/apps/sim/lib/copilot/tools/server/files/file-folders.ts b/apps/sim/lib/copilot/tools/server/files/file-folders.ts index 109256e23e4..8cd3ff8c2ce 100644 --- a/apps/sim/lib/copilot/tools/server/files/file-folders.ts +++ b/apps/sim/lib/copilot/tools/server/files/file-folders.ts @@ -1,13 +1,6 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { - CreateFileFolder, - DeleteFileFolder, - ListFileFolders, - MoveFile, - MoveFileFolder, - RenameFileFolder, -} from '@/lib/copilot/generated/tool-catalog-v1' +import { DeleteFileFolder } from '@/lib/copilot/generated/tool-catalog-v1' import { ensureWorkspaceAccess } from '@/lib/copilot/tools/handlers/access' import { assertServerToolNotAborted, @@ -15,7 +8,9 @@ import { type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' import { decodeVfsPathSegments } from '@/lib/copilot/vfs/path-utils' +import { isWorkflowAliasBackingPath } from '@/lib/copilot/vfs/workflow-aliases' import { + ensureWorkspaceFileFolderPath, findWorkspaceFileFolderIdByPath, getWorkspaceFileFolder, listWorkspaceFileFolders, @@ -192,7 +187,7 @@ function folderLabel(folder: WorkspaceFileFolderRecord): string { } export const listFileFoldersServerTool: BaseServerTool = { - name: ListFileFolders.id, + name: 'list_file_folders', async execute( params: ListFileFoldersArgs, context?: ServerToolContext @@ -215,7 +210,7 @@ export const listFileFoldersServerTool: BaseServerTool = { - name: CreateFileFolder.id, + name: 'create_file_folder', async execute( params: CreateFileFolderArgs, context?: ServerToolContext @@ -236,22 +231,23 @@ export const createFileFolderServerTool: BaseServerTool 1) { - const resolvedParentId = await findWorkspaceFileFolderIdByPath( + parentId = await ensureWorkspaceFileFolderPath({ workspaceId, - pathSegments.slice(0, -1) - ) - if (!resolvedParentId) { - return { - success: false, - message: `Parent folder not found at files/${pathSegments.slice(0, -1).join('/')}`, - } - } - parentId = resolvedParentId + userId: context.userId, + pathSegments: pathSegments.slice(0, -1), + }) } assertServerToolNotAborted(context) @@ -285,7 +281,7 @@ export const createFileFolderServerTool: BaseServerTool = { - name: RenameFileFolder.id, + name: 'rename_file_folder', async execute( params: RenameFileFolderArgs, context?: ServerToolContext @@ -341,7 +337,7 @@ export const renameFileFolderServerTool: BaseServerTool = { - name: MoveFileFolder.id, + name: 'move_file_folder', async execute( params: MoveFileFolderArgs, context?: ServerToolContext @@ -450,7 +446,7 @@ export const deleteFileFolderServerTool: BaseServerTool = { - name: MoveFile.id, + name: 'move_file', async execute(params: MoveFileArgs, context?: ServerToolContext): Promise { try { const workspaceId = await resolveWorkspaceId(params, context, 'write') diff --git a/apps/sim/lib/copilot/tools/server/files/rename-file.ts b/apps/sim/lib/copilot/tools/server/files/rename-file.ts index 10bac242eca..a6f9e9f63c8 100644 --- a/apps/sim/lib/copilot/tools/server/files/rename-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/rename-file.ts @@ -1,5 +1,4 @@ import { createLogger } from '@sim/logger' -import { RenameFile } from '@/lib/copilot/generated/tool-catalog-v1' import { ensureWorkspaceAccess } from '@/lib/copilot/tools/handlers/access' import { assertServerToolNotAborted, @@ -31,8 +30,13 @@ interface RenameFileResult { } } +/** + * Removed from the mothership catalog in favor of mv; the executor stays + * registered under its literal name so in-flight checkpoints paused on + * rename_file still resume. Delete after the mv release soaks. + */ export const renameFileServerTool: BaseServerTool = { - name: RenameFile.id, + name: 'rename_file', async execute(params: RenameFileArgs, context?: ServerToolContext): Promise { if (!context?.userId) { throw new Error('Authentication required') diff --git a/apps/sim/lib/copilot/tools/server/knowledge/search-knowledge-base.ts b/apps/sim/lib/copilot/tools/server/knowledge/search-knowledge-base.ts new file mode 100644 index 00000000000..8b22ee7d7a4 --- /dev/null +++ b/apps/sim/lib/copilot/tools/server/knowledge/search-knowledge-base.ts @@ -0,0 +1,39 @@ +import { SearchKnowledgeBase } from '@/lib/copilot/generated/tool-catalog-v1' +import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' +import { knowledgeBaseServerTool } from '@/lib/copilot/tools/server/knowledge/knowledge-base' + +type SearchKnowledgeBaseArgs = { + operation: string + args?: Record +} + +type SearchKnowledgeBaseResult = { + success: boolean + message: string + data?: any +} + +const READ_OPERATIONS = new Set(['get', 'query', 'list_tags']) + +/** + * Read-only variant of knowledge_base for info-gathering agents. Copilot + * access control is a per-agent tool allowlist, so read-only access gets its + * own tool name with its own operation contract — enforced here (where + * execution happens) on top of the fail-fast guard in the Go executor. + */ +export const searchKnowledgeBaseServerTool: BaseServerTool< + SearchKnowledgeBaseArgs, + SearchKnowledgeBaseResult +> = { + name: SearchKnowledgeBase.id, + async execute(params: SearchKnowledgeBaseArgs, context?: ServerToolContext) { + const operation = params?.operation + if (!READ_OPERATIONS.has(operation)) { + return { + success: false, + message: `search_knowledge_base is read-only: operation '${operation}' is not available (allowed: get, list_tags, query); mutations go through the knowledge agent's knowledge_base tool`, + } + } + return knowledgeBaseServerTool.execute(params, context) + }, +} diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index 6754920bca4..b5cf76e0e4a 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -2,7 +2,6 @@ import { createLogger } from '@sim/logger' import { z } from 'zod' import { CreateFile, - CreateFileFolder, DeleteFile, DeleteFileFolder, DownloadToWorkspaceFile, @@ -15,10 +14,6 @@ import { ManageCustomTool, ManageMcpTool, ManageSkill, - MoveFile, - MoveFileFolder, - RenameFile, - RenameFileFolder, UserTable, WorkspaceFile, } from '@/lib/copilot/generated/tool-catalog-v1' @@ -49,10 +44,12 @@ import { validateGeneratedToolPayload } from '@/lib/copilot/tools/server/generat import { generateImageServerTool } from '@/lib/copilot/tools/server/image/generate-image' import { getJobLogsServerTool } from '@/lib/copilot/tools/server/jobs/get-job-logs' import { knowledgeBaseServerTool } from '@/lib/copilot/tools/server/knowledge/knowledge-base' +import { searchKnowledgeBaseServerTool } from '@/lib/copilot/tools/server/knowledge/search-knowledge-base' import { ffmpegServerTool } from '@/lib/copilot/tools/server/media/ffmpeg' import { generateAudioServerTool } from '@/lib/copilot/tools/server/media/generate-audio' import { generateVideoServerTool } from '@/lib/copilot/tools/server/media/generate-video' import { searchOnlineServerTool } from '@/lib/copilot/tools/server/other/search-online' +import { queryUserTableServerTool } from '@/lib/copilot/tools/server/table/query-user-table' import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table' import { getCredentialsServerTool } from '@/lib/copilot/tools/server/user/get-credentials' import { setEnvironmentVariablesServerTool } from '@/lib/copilot/tools/server/user/set-environment-variables' @@ -118,12 +115,12 @@ const WRITE_ACTIONS: Record = { [WorkspaceFile.id]: ['create', 'append', 'update', 'delete', 'rename', 'patch'], [editContentServerTool.name]: ['*'], [CreateFile.id]: ['*'], - [RenameFile.id]: ['*'], + rename_file: ['*'], [DeleteFile.id]: ['*'], - [MoveFile.id]: ['*'], - [CreateFileFolder.id]: ['*'], - [RenameFileFolder.id]: ['*'], - [MoveFileFolder.id]: ['*'], + move_file: ['*'], + create_file_folder: ['*'], + rename_file_folder: ['*'], + move_file_folder: ['*'], [DeleteFileFolder.id]: ['*'], [DownloadToWorkspaceFile.id]: ['*'], [GenerateImage.id]: ['generate'], @@ -158,8 +155,10 @@ const baseServerToolRegistry: Record = { [setEnvironmentVariablesServerTool.name]: setEnvironmentVariablesServerTool, [getCredentialsServerTool.name]: getCredentialsServerTool, [knowledgeBaseServerTool.name]: knowledgeBaseServerTool, + [searchKnowledgeBaseServerTool.name]: searchKnowledgeBaseServerTool, [enrichmentRunServerTool.name]: enrichmentRunServerTool, [userTableServerTool.name]: userTableServerTool, + [queryUserTableServerTool.name]: queryUserTableServerTool, [workspaceFileServerTool.name]: workspaceFileServerTool, [editContentServerTool.name]: editContentServerTool, [createFileServerTool.name]: createFileServerTool, diff --git a/apps/sim/lib/copilot/tools/server/table/query-user-table.ts b/apps/sim/lib/copilot/tools/server/table/query-user-table.ts new file mode 100644 index 00000000000..e2b07176da5 --- /dev/null +++ b/apps/sim/lib/copilot/tools/server/table/query-user-table.ts @@ -0,0 +1,51 @@ +import { QueryUserTable } from '@/lib/copilot/generated/tool-catalog-v1' +import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' +import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table' + +type QueryUserTableArgs = { + operation: string + args?: Record +} + +type QueryUserTableResult = { + success: boolean + message: string + data?: any +} + +const READ_OPERATIONS = new Set(['get', 'get_schema', 'get_row', 'query_rows']) + +/** + * Read-only variant of user_table for info-gathering agents. Copilot access + * control is a per-agent tool allowlist, so read-only access gets its own tool + * name with its own operation contract — enforced here (where execution + * happens) on top of the fail-fast guard in the Go executor. outputPath is + * rejected because query_rows exports rows to a workspace file through it. + */ +export const queryUserTableServerTool: BaseServerTool = { + name: QueryUserTable.id, + async execute(params: QueryUserTableArgs, context?: ServerToolContext) { + const operation = params?.operation + if (!READ_OPERATIONS.has(operation)) { + return { + success: false, + message: `query_user_table is read-only: operation '${operation}' is not available (allowed: get, get_row, get_schema, query_rows); mutations go through the table agent's user_table tool`, + } + } + if (params?.args && 'outputPath' in params.args) { + return { + success: false, + message: + 'query_user_table is read-only: outputPath (file export) is not available; digest the rows directly or route exports through the table agent', + } + } + if (params && 'outputPath' in (params as Record)) { + return { + success: false, + message: + 'query_user_table is read-only: outputPath (file export) is not available; digest the rows directly or route exports through the table agent', + } + } + return userTableServerTool.execute(params, context) + }, +} diff --git a/apps/sim/lib/copilot/tools/tool-display.test.ts b/apps/sim/lib/copilot/tools/tool-display.test.ts new file mode 100644 index 00000000000..b7504a92a26 --- /dev/null +++ b/apps/sim/lib/copilot/tools/tool-display.test.ts @@ -0,0 +1,58 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { getToolDisplayTitle, mvDisplayVerb } from './tool-display' + +describe('mvDisplayVerb', () => { + it('reads a leaf-only change in the same folder as a rename', () => { + expect(mvDisplayVerb('workflows/falling-vacuum', 'workflows/failing-vacuum')).toBe('Renaming') + expect(mvDisplayVerb('files/Reports/a.md', 'files/Reports/b.md')).toBe('Renaming') + expect(mvDisplayVerb('tables/Leads', 'tables/Customers')).toBe('Renaming') + }) + + it('decodes segments so encoded sources compare against plain destinations', () => { + expect(mvDisplayVerb('workflows/My%20Flow', 'workflows/New Flow')).toBe('Renaming') + expect(mvDisplayVerb('files/My%20Docs/a.md', 'files/My Docs/b.md')).toBe('Renaming') + }) + + it('reads parent changes and folder destinations as moves', () => { + expect(mvDisplayVerb('files/a.png', 'files/Images/')).toBe('Moving') + expect(mvDisplayVerb('files/Reports/a.md', 'files/Archive/a.md')).toBe('Moving') + expect(mvDisplayVerb('files/Reports/a.md', 'files/Archive/b.md')).toBe('Moving') + expect(mvDisplayVerb('workflows/My Flow', 'workflows/Archive/')).toBe('Moving') + }) + + it('falls back to Moving when arguments are incomplete', () => { + expect(mvDisplayVerb(undefined, 'files/x.md')).toBe('Moving') + expect(mvDisplayVerb('files/x.md', undefined)).toBe('Moving') + }) +}) + +describe('getToolDisplayTitle for the vfs verbs', () => { + it('uses the derived verb for mv titles', () => { + expect( + getToolDisplayTitle('mv', { + sources: ['workflows/falling-vacuum'], + destination: 'workflows/failing-vacuum', + toolTitle: 'falling-vacuum to failing-vacuum', + }) + ).toBe('Renaming falling-vacuum to failing-vacuum') + expect( + getToolDisplayTitle('mv', { + sources: ['files/a.png', 'files/b.png'], + destination: 'files/Images/', + toolTitle: '2 files to Images', + }) + ).toBe('Moving 2 files to Images') + }) + + it('titles cp and mkdir by intent', () => { + expect(getToolDisplayTitle('cp', { toolTitle: 'My Workflow' })).toBe('Duplicating My Workflow') + expect(getToolDisplayTitle('mkdir', { toolTitle: 'Reports/2026' })).toBe( + 'Creating Reports/2026' + ) + expect(getToolDisplayTitle('cp', {})).toBe('Duplicating workflow') + expect(getToolDisplayTitle('mkdir', {})).toBe('Creating folder') + }) +}) diff --git a/apps/sim/lib/copilot/tools/tool-display.ts b/apps/sim/lib/copilot/tools/tool-display.ts index 5ee25a69959..8f6a7f5c282 100644 --- a/apps/sim/lib/copilot/tools/tool-display.ts +++ b/apps/sim/lib/copilot/tools/tool-display.ts @@ -55,6 +55,41 @@ function isWorkflowArtifactPath(path: string, filename: string): boolean { return trimmed.startsWith('workflows/') && trimmed.endsWith(`/${filename}`) } +function decodePathSegment(segment: string): string { + try { + return decodeURIComponent(segment) + } catch { + return segment + } +} + +/** + * Verb for an mv call, derived from its arguments so the row reads as what + * the call actually does: a single source whose parent path matches the + * destination's (only the leaf changes) is a rename; multiple sources, a + * trailing-slash folder destination, or a parent change is a move. Segments + * are decoded so an encoded source compares correctly against a plain-text + * destination leaf. + */ +export function mvDisplayVerb( + source: string | undefined, + destination: string | undefined +): 'Renaming' | 'Moving' { + if (!source || !destination || /\/\s*$/.test(destination)) return 'Moving' + const segments = (path: string) => + path + .trim() + .replace(/^\/+|\/+$/g, '') + .split('/') + .map(decodePathSegment) + const src = segments(source) + const dst = segments(destination) + if (src.length < 2 || dst.length < 2) return 'Moving' + const sameParent = src.slice(0, -1).join('/') === dst.slice(0, -1).join('/') + const leafChanged = src.at(-1) !== dst.at(-1) + return sameParent && leafChanged ? 'Renaming' : 'Moving' +} + function workspaceFileTitle(args: ToolArgs): string { const title = stringArg(args, 'title') if (!title) return '' @@ -74,13 +109,15 @@ function workspaceFileTitle(args: ToolArgs): string { const TOOL_TITLES: Record = { read: 'Reading file', search_library_docs: 'Searching library docs', - user_memory: 'Accessing memory', user_table: 'Managing table', + run_code: 'Running code', + query_user_table: 'Querying table', workspace_file: 'Editing file', edit_content: 'Applying file content', create_workflow: 'Creating workflow', edit_workflow: 'Editing workflow', knowledge_base: 'Managing knowledge base', + search_knowledge_base: 'Searching knowledge base', open_resource: 'Opening resource', generate_image: 'Generating image', generate_video: 'Generating video', @@ -97,6 +134,8 @@ const TOOL_TITLES: Record = { scheduled_task: 'Scheduled Task Agent', agent: 'Tools Agent', research: 'Research Agent', + scout: 'Scout Agent', + search: 'Search Agent', media: 'Media Agent', superagent: 'Executing action', } @@ -130,6 +169,21 @@ export function getToolDisplayTitle(name: string, args?: Record const target = firstStringArg(args, 'toolTitle', 'title') return target ? `Finding ${target}` : 'Finding files' } + case 'mv': { + const sources = stringArrayArg(args, 'sources') + const verb = + sources.length === 1 ? mvDisplayVerb(sources[0], stringArg(args, 'destination')) : 'Moving' + const target = firstStringArg(args, 'toolTitle', 'title') + return target ? `${verb} ${target}` : verb + } + case 'cp': { + const target = firstStringArg(args, 'toolTitle', 'title') + return target ? `Duplicating ${target}` : 'Duplicating workflow' + } + case 'mkdir': { + const target = firstStringArg(args, 'toolTitle', 'title') + return target ? `Creating ${target}` : 'Creating folder' + } case 'enrichment_run': { const subject = nestedStringArg( args, diff --git a/apps/sim/lib/copilot/vfs/resource-writer.test.ts b/apps/sim/lib/copilot/vfs/resource-writer.test.ts index f0181d0a396..0acdbbda432 100644 --- a/apps/sim/lib/copilot/vfs/resource-writer.test.ts +++ b/apps/sim/lib/copilot/vfs/resource-writer.test.ts @@ -281,6 +281,96 @@ describe('resource writer workflow aliases', () => { ) }) + it('auto-creates missing parent folders for plain workspace file creates', async () => { + mocks.resolveWorkflowAliasForWorkspace.mockResolvedValue(null) + mocks.ensureWorkspaceFileFolderPath.mockResolvedValue('folder-nested') + mocks.getWorkspaceFileByName.mockResolvedValue(null) + mocks.uploadWorkspaceFile.mockResolvedValue({ + id: 'file-report', + name: 'summary.csv', + size: 7, + type: 'text/csv', + url: '/download', + }) + + const result = await writeWorkspaceFileByPath({ + workspaceId: 'workspace-1', + userId: 'user-1', + target: { + path: 'files/Reports/2026/summary.csv', + mode: 'create', + }, + buffer: Buffer.from('content'), + inferredMimeType: 'text/csv', + }) + + expect(mocks.ensureWorkspaceFileFolderPath).toHaveBeenCalledWith({ + workspaceId: 'workspace-1', + userId: 'user-1', + pathSegments: ['Reports', '2026'], + }) + expect(mocks.findWorkspaceFileFolderIdByPath).not.toHaveBeenCalled() + expect(mocks.uploadWorkspaceFile).toHaveBeenCalledWith( + 'workspace-1', + 'user-1', + Buffer.from('content'), + 'summary.csv', + 'text/csv', + { folderId: 'folder-nested' } + ) + expect(result).toMatchObject({ + id: 'file-report', + vfsPath: 'files/Reports/2026/summary.csv', + mode: 'create', + }) + }) + + it('validates create targets read-only, resolving existing parent folders without creating', async () => { + mocks.resolveWorkflowAliasForWorkspace.mockResolvedValue(null) + mocks.findWorkspaceFileFolderIdByPath.mockResolvedValue('folder-nested') + mocks.getWorkspaceFileByName.mockResolvedValue(null) + + const validation = await validateWorkspaceFileWriteTarget({ + workspaceId: 'workspace-1', + userId: 'user-1', + target: { + path: 'files/Reports/2026/summary.csv', + mode: 'create', + }, + }) + + expect(mocks.ensureWorkspaceFileFolderPath).not.toHaveBeenCalled() + expect(validation).toMatchObject({ + mode: 'create', + vfsPath: 'files/Reports/2026/summary.csv', + fileName: 'summary.csv', + folderId: 'folder-nested', + }) + }) + + it('accepts create targets with missing parent folders during validation without creating them', async () => { + mocks.resolveWorkflowAliasForWorkspace.mockResolvedValue(null) + mocks.findWorkspaceFileFolderIdByPath.mockResolvedValue(null) + + const validation = await validateWorkspaceFileWriteTarget({ + workspaceId: 'workspace-1', + userId: 'user-1', + target: { + path: 'files/Reports/2026/summary.csv', + mode: 'create', + }, + }) + + expect(mocks.ensureWorkspaceFileFolderPath).not.toHaveBeenCalled() + expect(mocks.getWorkspaceFileByName).not.toHaveBeenCalled() + expect(validation).toMatchObject({ + mode: 'create', + vfsPath: 'files/Reports/2026/summary.csv', + fileName: 'summary.csv', + folderId: null, + }) + }) + it('reports alias path when exact-name alias backing creation conflicts', async () => { mocks.resolveWorkflowAliasForWorkspace.mockResolvedValue({ kind: 'plan_file', diff --git a/apps/sim/lib/copilot/vfs/resource-writer.ts b/apps/sim/lib/copilot/vfs/resource-writer.ts index 460f6ba36dd..148c0ee0333 100644 --- a/apps/sim/lib/copilot/vfs/resource-writer.ts +++ b/apps/sim/lib/copilot/vfs/resource-writer.ts @@ -57,6 +57,7 @@ export type WorkspaceFileWriteValidation = vfsPath: string backingVfsPath?: string fileName: string + /** Null for root targets AND for parent chains that don't exist yet — validation is read-only; missing folders are created at write time. */ folderId: string | null } | { @@ -97,22 +98,41 @@ export function parseWorkspaceFileCreatePath(path: string): { } } +/** + * Resolve a create-mode write target. Pass `createFolders` (the write path) to + * create missing parent folders; without it (the validation path) resolution + * is read-only — a missing parent chain yields `folderId: null`, since the + * folders are created at write time and nothing can conflict there yet. + */ async function resolveCreateTarget( workspaceId: string, - path: string + path: string, + createFolders?: { userId: string } ): Promise { const parsed = parseWorkspaceFileCreatePath(path) - const folderId = - parsed.folderSegments.length > 0 - ? await findWorkspaceFileFolderIdByPath(workspaceId, parsed.folderSegments, { - includeReservedSystemFolders: true, - }) - : null - - if (parsed.folderSegments.length > 0 && !folderId) { - throw new Error( - `Directory not yet created: ${displayFolderPath(parsed.folderSegments)}. Create the directory first, then retry the file write.` - ) + let folderId: string | null = null + if (parsed.folderSegments.length > 0) { + if (createFolders) { + folderId = await ensureWorkspaceFileFolderPath({ + workspaceId, + userId: createFolders.userId, + pathSegments: parsed.folderSegments, + }) + if (!folderId) { + throw new Error(`Failed to create directory: ${displayFolderPath(parsed.folderSegments)}`) + } + } else { + folderId = await findWorkspaceFileFolderIdByPath(workspaceId, parsed.folderSegments, { + includeReservedSystemFolders: true, + }) + if (!folderId) { + return { + fileName: parsed.fileName, + folderId: null, + vfsPath: parsed.vfsPath, + } + } + } } const existing = await getWorkspaceFileByName(workspaceId, parsed.fileName, { folderId }) @@ -388,7 +408,9 @@ export async function writeWorkspaceFileByPath(args: { } } - const createTarget = await resolveCreateTarget(args.workspaceId, args.target.path) + const createTarget = await resolveCreateTarget(args.workspaceId, args.target.path, { + userId: args.userId, + }) const uploaded = await uploadWorkspaceFile( args.workspaceId, args.userId, diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index 571768bad6a..417124f5b49 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -1,4 +1,3 @@ -import { truncate } from '@sim/utils/string' import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' import { isHosted } from '@/lib/core/config/env-flags' import { isSubBlockHidden } from '@/lib/workflows/subblocks/visibility' @@ -669,7 +668,7 @@ export function serializeCustomTool(tool: { id: tool.id, title: tool.title, schema: tool.schema, - codePreview: truncate(tool.code, 500), + code: tool.code, }, null, 2 @@ -716,7 +715,7 @@ export function serializeSkill(s: { id: s.id, name: s.name, description: s.description, - contentPreview: truncate(s.content, 500), + content: s.content, createdAt: s.createdAt.toISOString(), }, null, diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index abfc3ad9508..fe9734df9e6 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -3,10 +3,12 @@ import { db } from '@sim/db' import { chat as chatTable, copilotChats, + customTools as customToolsTable, document, jobExecutionLogs, knowledgeConnector, mcpServers as mcpServersTable, + skill as skillTable, workflowDeploymentVersion, workflowExecutionLogs, workflowFolder, @@ -16,7 +18,7 @@ import { } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { and, desc, eq, isNotNull, isNull, ne, sql } from 'drizzle-orm' +import { and, desc, eq, isNotNull, isNull, ne, or, sql } from 'drizzle-orm' import { listApiKeys } from '@/lib/api-key/service' import { buildWorkspaceContextMd, @@ -108,13 +110,13 @@ import { type WorkspaceFileRecord, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { listCustomBlocksWithInputsForWorkspace } from '@/lib/workflows/custom-blocks/operations' -import { listCustomTools } from '@/lib/workflows/custom-tools/operations' +import { getCustomToolById } from '@/lib/workflows/custom-tools/operations' import { loadWorkflowDeploymentSnapshot, loadWorkflowFromNormalizedTables, } from '@/lib/workflows/persistence/utils' import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer' -import { listSkills } from '@/lib/workflows/skills/operations' +import { getSkillById } from '@/lib/workflows/skills/operations' import { listFolders, listWorkflows } from '@/lib/workflows/utils' import { assertActiveWorkspaceAccess, @@ -1827,25 +1829,47 @@ export class WorkspaceVFS { } /** - * Materialize custom tools using the shared listCustomTools function. + * Advertise custom tools in the VFS without eagerly loading their code. + * Paths are registered as lazy so glob/WORKSPACE.md see them, but full + * schema+code is fetched only when read (or a grep whose scope touches them). */ private async materializeCustomTools( workspaceId: string, userId: string ): Promise> { try { - const toolRows = await listCustomTools({ userId, workspaceId }) + // Metadata only — tool code can be large; keep it out of the eager map. + // Visibility matches listCustomTools: workspace tools + legacy user-owned. + const toolRows = await db + .select({ + id: customToolsTable.id, + title: customToolsTable.title, + }) + .from(customToolsTable) + .where( + or( + eq(customToolsTable.workspaceId, workspaceId), + and(isNull(customToolsTable.workspaceId), eq(customToolsTable.userId, userId)) + ) + ) + .orderBy(desc(customToolsTable.createdAt)) for (const tool of toolRows) { const safeName = sanitizeName(tool.title) - const serialized = serializeCustomTool({ - id: tool.id, - title: tool.title, - schema: tool.schema, - code: tool.code, - }) - this.files.set(`custom-tools/${safeName}.json`, serialized) - this.files.set(`agent/custom-tools/${safeName}.json`, serialized) + const toolId = tool.id + const load = async () => { + const full = await getCustomToolById({ toolId, userId, workspaceId }) + if (!full) return null + return serializeCustomTool({ + id: full.id, + title: full.title, + schema: full.schema, + code: full.code, + }) + } + // Legacy alias + canonical agent/ path — each resolves independently on read. + this.registerLazy(`custom-tools/${safeName}.json`, load) + this.registerLazy(`agent/custom-tools/${safeName}.json`, load) } return toolRows.map((t) => ({ id: t.id, name: t.title })) @@ -1944,26 +1968,39 @@ export class WorkspaceVFS { } /** - * Materialize workspace skills using the shared listSkills function. + * Advertise workspace skills in the VFS without eagerly loading their bodies. + * Paths are registered as lazy so glob/WORKSPACE.md see them, but full content + * is fetched only when read (or a grep whose scope touches the path) resolves them. */ private async materializeSkills( workspaceId: string ): Promise> { try { - const skillRows = await listSkills({ workspaceId, includeBuiltins: false }) + // Metadata only — skill bodies can be large; keep them out of the eager map. + const skillRows = await db + .select({ + id: skillTable.id, + name: skillTable.name, + description: skillTable.description, + }) + .from(skillTable) + .where(eq(skillTable.workspaceId, workspaceId)) + .orderBy(desc(skillTable.createdAt)) for (const s of skillRows) { const safeName = sanitizeName(s.name) - this.files.set( - `agent/skills/${safeName}.json`, - serializeSkill({ - id: s.id, - name: s.name, - description: s.description, - content: s.content, - createdAt: s.createdAt, + const skillId = s.id + this.registerLazy(`agent/skills/${safeName}.json`, async () => { + const full = await getSkillById({ skillId, workspaceId }) + if (!full) return null + return serializeSkill({ + id: full.id, + name: full.name, + description: full.description, + content: full.content, + createdAt: full.createdAt, }) - ) + }) } return skillRows.map((s) => ({ id: s.id, name: s.name, description: s.description })) diff --git a/apps/sim/lib/credentials/display-name.test.ts b/apps/sim/lib/credentials/display-name.test.ts new file mode 100644 index 00000000000..36d23dfc8b9 --- /dev/null +++ b/apps/sim/lib/credentials/display-name.test.ts @@ -0,0 +1,57 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + DISPLAY_NAME_MAX_LENGTH, + defaultCredentialDisplayName, +} from '@/lib/credentials/display-name' + +const NONE: ReadonlySet = new Set() + +describe('defaultCredentialDisplayName', () => { + it("produces {Name}'s {Service} when the user name is known", () => { + expect(defaultCredentialDisplayName('Justin', 'Gmail', NONE)).toBe("Justin's Gmail") + }) + + it('trims surrounding whitespace from the user name', () => { + expect(defaultCredentialDisplayName(' Justin ', 'Gmail', NONE)).toBe("Justin's Gmail") + }) + + it.each([null, undefined, '', ' '])('falls back to My {Service} for user name %j', (name) => { + expect(defaultCredentialDisplayName(name, 'Gmail', NONE)).toBe('My Gmail') + }) + + it('appends " 2" when the base name is taken', () => { + const taken = new Set(["justin's gmail"]) + expect(defaultCredentialDisplayName('Justin', 'Gmail', taken)).toBe("Justin's Gmail 2") + }) + + it('skips taken numbered names and picks the next free slot', () => { + const taken = new Set(["justin's gmail", "justin's gmail 2", "justin's gmail 3"]) + expect(defaultCredentialDisplayName('Justin', 'Gmail', taken)).toBe("Justin's Gmail 4") + }) + + it('compares collisions case-insensitively', () => { + const taken = new Set(["justin's gmail"]) + expect(defaultCredentialDisplayName('JUSTIN', 'Gmail', taken)).toBe("JUSTIN's Gmail 2") + }) + + it('numbers the My {Service} fallback on collision too', () => { + const taken = new Set(['my gmail']) + expect(defaultCredentialDisplayName(null, 'Gmail', taken)).toBe('My Gmail 2') + }) + + it('truncates a long user name so name + suffix + disambiguator fit the max length', () => { + const longName = 'x'.repeat(400) + const result = defaultCredentialDisplayName(longName, 'Gmail', NONE) + + expect(result.endsWith("'s Gmail")).toBe(true) + expect(result.length).toBeLessThanOrEqual(DISPLAY_NAME_MAX_LENGTH) + + const taken = new Set([result.toLowerCase()]) + const numbered = defaultCredentialDisplayName(longName, 'Gmail', taken) + expect(numbered).toBe(`${result} 2`) + expect(numbered.length).toBeLessThanOrEqual(DISPLAY_NAME_MAX_LENGTH) + }) +}) diff --git a/apps/sim/lib/credentials/display-name.ts b/apps/sim/lib/credentials/display-name.ts new file mode 100644 index 00000000000..9b946f31e56 --- /dev/null +++ b/apps/sim/lib/credentials/display-name.ts @@ -0,0 +1,49 @@ +/** Server-enforced max for `WorkspaceCredential.displayName` — see `lib/api/contracts/credentials.ts`. */ +export const DISPLAY_NAME_MAX_LENGTH = 255 + +/** + * Reserved tail budget when truncating the username so the auto-numbering + * disambiguator (e.g. `" 9999"`) always fits within {@link DISPLAY_NAME_MAX_LENGTH}. + */ +const COLLISION_SUFFIX_RESERVATION = 5 + +/** Upper bound for the auto-numbering search — pathological if ever reached. */ +const MAX_COLLISION_INDEX = 10000 + +/** + * Default credential display name. Produces `"{Name}'s {Service}"` when the + * user's name is known, falling back to `"My {Service}"` otherwise. The + * username is truncated so the full string (including any auto-numbering + * disambiguator) stays within {@link DISPLAY_NAME_MAX_LENGTH}. + * + * When the base name collides with an existing credential in `takenNames`, + * `" 2"`, `" 3"`, ... are appended until an unused name is found. `takenNames` + * must contain lowercased names; comparison is case-insensitive to match the + * duplicate-detection in the connect modal. + */ +export function defaultCredentialDisplayName( + userName: string | null | undefined, + serviceName: string, + takenNames: ReadonlySet +): string { + const trimmed = userName?.trim() + let base: string + if (trimmed) { + const suffix = `'s ${serviceName}` + const nameBudget = Math.max( + 0, + DISPLAY_NAME_MAX_LENGTH - suffix.length - COLLISION_SUFFIX_RESERVATION + ) + const safeName = trimmed.length > nameBudget ? trimmed.slice(0, nameBudget) : trimmed + base = `${safeName}${suffix}` + } else { + base = `My ${serviceName}` + } + + if (!takenNames.has(base.toLowerCase())) return base + for (let n = 2; n < MAX_COLLISION_INDEX; n++) { + const candidate = `${base} ${n}` + if (!takenNames.has(candidate.toLowerCase())) return candidate + } + return base +} diff --git a/apps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts b/apps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts index 7c6d066e0dc..a143c5e6dd2 100644 --- a/apps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts @@ -96,6 +96,51 @@ describe('trackChatUpload', () => { ) }) + it('stamps message_id on the UPDATE arm when the birth message is known', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'wf_existing' }]) + + await trackChatUpload( + WORKSPACE_ID, + USER_ID, + CHAT_ID, + S3_KEY, + 'image.png', + 'image/png', + 1024, + 'msg_abc' + ) + + expect(dbChainMockFns.set).toHaveBeenCalledWith( + expect.objectContaining({ chatId: CHAT_ID, messageId: 'msg_abc' }) + ) + }) + + it('stamps message_id on the fallback INSERT arm and nulls it when omitted', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([]) + + await trackChatUpload( + WORKSPACE_ID, + USER_ID, + CHAT_ID, + S3_KEY, + 'image.png', + 'image/png', + 1024, + 'msg_abc' + ) + + expect(dbChainMockFns.values).toHaveBeenCalledWith( + expect.objectContaining({ chatId: CHAT_ID, messageId: 'msg_abc' }) + ) + + // Legacy callers without a message id write an explicit NULL ("birth unknown"). + dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'wf_existing' }]) + await trackChatUpload(WORKSPACE_ID, USER_ID, CHAT_ID, S3_KEY, 'image.png', 'image/png', 1024) + expect(dbChainMockFns.set).toHaveBeenLastCalledWith( + expect.objectContaining({ messageId: null }) + ) + }) + it('retries with a suffixed displayName on collision against the chat displayName index', async () => { // 23505 from the partial unique index on (chat_id, display_name) — the case we retry. const displayNameCollision = Object.assign(new Error('duplicate key'), { diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts index 5a8e47f72b9..6d31fc10487 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts @@ -452,6 +452,14 @@ export async function ensureWorkspaceFileFolderPath(params: { }): Promise { if (params.pathSegments.length === 0) return null + // Fast path: the whole chain already exists (the common case for repeated + // writes into known folders) — per-segment indexed lookups instead of + // loading the workspace's entire folder table. + const existing = await findWorkspaceFileFolderIdByPath(params.workspaceId, params.pathSegments, { + includeReservedSystemFolders: true, + }) + if (existing) return existing + // Load all active folders once and build a lookup keyed by "name|parentId" // so we can resolve existing segments without a per-segment SELECT. const existingFolders = await db diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 8e091d8d8f2..610c445db21 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -487,14 +487,20 @@ export async function trackChatUpload( s3Key: string, fileName: string, contentType: string, - size: number + size: number, + messageId?: string ): Promise<{ displayName: string }> { for (let n = 1; n <= MAX_CHAT_DISPLAY_NAME_RETRIES; n++) { const candidate = suffixedName(fileName, n) try { const updated = await db .update(workspaceFiles) - .set({ chatId, context: 'mothership', displayName: candidate }) + .set({ + chatId, + messageId: messageId ?? null, + context: 'mothership', + displayName: candidate, + }) .where( and( eq(workspaceFiles.key, s3Key), @@ -520,6 +526,7 @@ export async function trackChatUpload( workspaceId, context: 'mothership', chatId, + messageId: messageId ?? null, originalName: fileName, displayName: candidate, contentType, @@ -1016,6 +1023,77 @@ export async function renameWorkspaceFile( } } +/** + * Move and/or rename a workspace file in one atomic row update. Either side + * may be a no-op (same folder = pure rename, same name = pure move); when + * both are unchanged the record is returned untouched. Conflicts at the + * destination throw {@link FileConflictError}. The `renamed`/`moved` flags + * report what actually changed, computed from the same read the update uses. + */ +export async function moveRenameWorkspaceFile(params: { + workspaceId: string + fileId: string + targetFolderId: string | null + newName: string +}): Promise<{ file: WorkspaceFileRecord; renamed: boolean; moved: boolean }> { + const normalizedName = normalizeWorkspaceFileItemName(params.newName.trim(), 'File') + + const fileRecord = await getWorkspaceFile(params.workspaceId, params.fileId) + if (!fileRecord) { + throw new Error('File not found') + } + + const targetFolderId = await assertWorkspaceFileFolderTarget( + params.workspaceId, + params.targetFolderId + ) + const currentFolderId = fileRecord.folderId ?? null + const renamed = fileRecord.name !== normalizedName + const moved = currentFolderId !== targetFolderId + if (!renamed && !moved) { + return { file: fileRecord, renamed, moved } + } + + const exists = await fileExistsInWorkspace(params.workspaceId, normalizedName, targetFolderId) + if (exists) { + throw new FileConflictError(normalizedName) + } + + let updated: { id: string }[] + try { + updated = await db + .update(workspaceFiles) + .set({ originalName: normalizedName, folderId: targetFolderId, updatedAt: new Date() }) + .where( + and( + eq(workspaceFiles.id, params.fileId), + eq(workspaceFiles.workspaceId, params.workspaceId), + eq(workspaceFiles.context, 'workspace') + ) + ) + .returning({ id: workspaceFiles.id }) + } catch (error: unknown) { + if (getPostgresErrorCode(error) === '23505') { + throw new FileConflictError(normalizedName) + } + throw error + } + + if (updated.length === 0) { + throw new Error('File not found or could not be moved') + } + + return { + file: { + ...fileRecord, + name: normalizedName, + folderId: targetFolderId, + }, + renamed, + moved, + } +} + /** * Soft delete a workspace file. */ diff --git a/apps/sim/lib/workspace-files/orchestration/file-folder-lifecycle.ts b/apps/sim/lib/workspace-files/orchestration/file-folder-lifecycle.ts index 30379de8031..ce11157ccb7 100644 --- a/apps/sim/lib/workspace-files/orchestration/file-folder-lifecycle.ts +++ b/apps/sim/lib/workspace-files/orchestration/file-folder-lifecycle.ts @@ -5,6 +5,7 @@ import { bulkArchiveWorkspaceFileItems, createWorkspaceFileFolder, FileConflictError, + moveRenameWorkspaceFile, moveWorkspaceFileItems, renameWorkspaceFile, restoreWorkspaceFile, @@ -307,6 +308,72 @@ export async function performRenameWorkspaceFile( } } +export interface PerformMoveRenameWorkspaceFileParams { + workspaceId: string + userId: string + fileId: string + targetFolderId: string | null + newName: string +} + +export interface PerformMoveRenameWorkspaceFileResult { + success: boolean + error?: string + errorCode?: WorkspaceFilesOrchestrationErrorCode + file?: WorkspaceFileRecord +} + +export async function performMoveRenameWorkspaceFile( + params: PerformMoveRenameWorkspaceFileParams +): Promise { + const { workspaceId, userId, fileId, targetFolderId, newName } = params + + try { + const { file, renamed, moved } = await moveRenameWorkspaceFile({ + workspaceId, + fileId, + targetFolderId, + newName, + }) + logger.info('Moved/renamed workspace file', { workspaceId, fileId, renamed, moved }) + + if (moved) { + recordAudit({ + workspaceId, + actorId: userId, + action: AuditAction.FILE_MOVED, + resourceType: AuditResourceType.FILE, + resourceId: fileId, + resourceName: file.name, + description: `Moved file "${file.name}"${targetFolderId ? ' to folder' : ' to root'}`, + metadata: { targetFolderId }, + }) + } + if (renamed) { + recordAudit({ + workspaceId, + actorId: userId, + action: AuditAction.FILE_UPDATED, + resourceType: AuditResourceType.FILE, + resourceId: fileId, + resourceName: file.name, + description: `Renamed file to "${file.name}"`, + }) + } + + return { success: true, file } + } catch (error) { + logger.error('Failed to move/rename workspace file', { error }) + if (error instanceof FileConflictError || getPostgresErrorCode(error) === '23505') { + return { success: false, error: toError(error).message, errorCode: 'conflict' } + } + if (toError(error).message.includes('not found')) { + return { success: false, error: toError(error).message, errorCode: 'not_found' } + } + return { success: false, error: toError(error).message, errorCode: 'internal' } + } +} + export async function performRestoreWorkspaceFile( params: PerformRestoreWorkspaceFileParams ): Promise { diff --git a/apps/sim/lib/workspace-files/orchestration/index.ts b/apps/sim/lib/workspace-files/orchestration/index.ts index 5e5268bda8c..81c7af23352 100644 --- a/apps/sim/lib/workspace-files/orchestration/index.ts +++ b/apps/sim/lib/workspace-files/orchestration/index.ts @@ -3,6 +3,8 @@ export { type PerformCreateWorkspaceFileFolderResult, type PerformDeleteWorkspaceFileItemsParams, type PerformDeleteWorkspaceFileItemsResult, + type PerformMoveRenameWorkspaceFileParams, + type PerformMoveRenameWorkspaceFileResult, type PerformMoveWorkspaceFileItemsParams, type PerformMoveWorkspaceFileItemsResult, type PerformRenameWorkspaceFileParams, @@ -15,6 +17,7 @@ export { type PerformUpdateWorkspaceFileFolderResult, performCreateWorkspaceFileFolder, performDeleteWorkspaceFileItems, + performMoveRenameWorkspaceFile, performMoveWorkspaceFileItems, performRenameWorkspaceFile, performRestoreWorkspaceFile, diff --git a/apps/sim/tools/registry.minimal.ts b/apps/sim/tools/registry.minimal.ts index d1b34868ec8..9b9721c94a2 100644 --- a/apps/sim/tools/registry.minimal.ts +++ b/apps/sim/tools/registry.minimal.ts @@ -1,5 +1,75 @@ import { functionExecuteTool } from '@/tools/function' +import { + gmailAddLabelV2Tool, + gmailArchiveV2Tool, + gmailCreateLabelV2Tool, + gmailDeleteDraftV2Tool, + gmailDeleteLabelV2Tool, + gmailDeleteV2Tool, + gmailDraftV2Tool, + gmailEditDraftV2Tool, + gmailGetDraftV2Tool, + gmailGetThreadV2Tool, + gmailListDraftsV2Tool, + gmailListLabelsV2Tool, + gmailListThreadsV2Tool, + gmailMarkReadV2Tool, + gmailMarkUnreadV2Tool, + gmailMoveV2Tool, + gmailReadV2Tool, + gmailRemoveLabelV2Tool, + gmailSearchV2Tool, + gmailSendV2Tool, + gmailTrashThreadV2Tool, + gmailUnarchiveV2Tool, + gmailUntrashThreadV2Tool, + gmailUpdateLabelV2Tool, +} from '@/tools/gmail' import { httpRequestTool } from '@/tools/http' +import { + slackAddReactionTool, + slackArchiveConversationTool, + slackCanvasTool, + slackCreateChannelCanvasTool, + slackCreateConversationTool, + slackDeleteCanvasTool, + slackDeleteMessageTool, + slackDeleteScheduledMessageTool, + slackDownloadTool, + slackEditCanvasTool, + slackEphemeralMessageTool, + slackGetCanvasTool, + slackGetChannelHistoryTool, + slackGetChannelInfoTool, + slackGetMessageTool, + slackGetPermalinkTool, + slackGetThreadRepliesTool, + slackGetThreadTool, + slackGetUserPresenceTool, + slackGetUserTool, + slackInviteToConversationTool, + slackListCanvasesTool, + slackListChannelsTool, + slackListMembersTool, + slackListScheduledMessagesTool, + slackListUsersTool, + slackLookupCanvasSectionsTool, + slackMessageReaderTool, + slackMessageTool, + slackOpenViewTool, + slackPublishViewTool, + slackPushViewTool, + slackRemoveReactionTool, + slackRenameConversationTool, + slackScheduleMessageTool, + slackSetConversationPurposeTool, + slackSetConversationTopicTool, + slackSetStatusTool, + slackSetSuggestedPromptsTool, + slackSetTitleTool, + slackUpdateMessageTool, + slackUpdateViewTool, +} from '@/tools/slack' import type { ToolConfig } from '@/tools/types' /** @@ -8,9 +78,76 @@ import type { ToolConfig } from '@/tools/types' * next.config.ts) so the local dev server never compiles the full ~247-tool * graph (~2,074 modules) that the shared workspace layout otherwise drags into * every route. Only these tools execute in minimal mode; unset the flag for the - * full set. NEVER aliased in production. + * full set. The slack and gmail v2 sets back the `slack` and `gmail_v2` blocks + * kept in `blocks/registry-maps.minimal.ts`. NEVER aliased in production. */ export const tools: Record = { http_request: httpRequestTool, function_execute: functionExecuteTool, + gmail_send_v2: gmailSendV2Tool, + gmail_read_v2: gmailReadV2Tool, + gmail_search_v2: gmailSearchV2Tool, + gmail_draft_v2: gmailDraftV2Tool, + gmail_move_v2: gmailMoveV2Tool, + gmail_mark_read_v2: gmailMarkReadV2Tool, + gmail_mark_unread_v2: gmailMarkUnreadV2Tool, + gmail_archive_v2: gmailArchiveV2Tool, + gmail_unarchive_v2: gmailUnarchiveV2Tool, + gmail_delete_v2: gmailDeleteV2Tool, + gmail_add_label_v2: gmailAddLabelV2Tool, + gmail_remove_label_v2: gmailRemoveLabelV2Tool, + gmail_create_label_v2: gmailCreateLabelV2Tool, + gmail_delete_draft_v2: gmailDeleteDraftV2Tool, + gmail_delete_label_v2: gmailDeleteLabelV2Tool, + gmail_edit_draft_v2: gmailEditDraftV2Tool, + gmail_get_draft_v2: gmailGetDraftV2Tool, + gmail_get_thread_v2: gmailGetThreadV2Tool, + gmail_list_drafts_v2: gmailListDraftsV2Tool, + gmail_list_labels_v2: gmailListLabelsV2Tool, + gmail_list_threads_v2: gmailListThreadsV2Tool, + gmail_trash_thread_v2: gmailTrashThreadV2Tool, + gmail_untrash_thread_v2: gmailUntrashThreadV2Tool, + gmail_update_label_v2: gmailUpdateLabelV2Tool, + slack_message: slackMessageTool, + slack_message_reader: slackMessageReaderTool, + slack_list_channels: slackListChannelsTool, + slack_list_members: slackListMembersTool, + slack_list_users: slackListUsersTool, + slack_get_user: slackGetUserTool, + slack_get_message: slackGetMessageTool, + slack_get_thread: slackGetThreadTool, + slack_get_thread_replies: slackGetThreadRepliesTool, + slack_get_channel_history: slackGetChannelHistoryTool, + slack_get_permalink: slackGetPermalinkTool, + slack_set_status: slackSetStatusTool, + slack_set_title: slackSetTitleTool, + slack_set_suggested_prompts: slackSetSuggestedPromptsTool, + slack_canvas: slackCanvasTool, + slack_download: slackDownloadTool, + slack_ephemeral_message: slackEphemeralMessageTool, + slack_update_message: slackUpdateMessageTool, + slack_delete_message: slackDeleteMessageTool, + slack_add_reaction: slackAddReactionTool, + slack_remove_reaction: slackRemoveReactionTool, + slack_get_channel_info: slackGetChannelInfoTool, + slack_get_user_presence: slackGetUserPresenceTool, + slack_open_view: slackOpenViewTool, + slack_update_view: slackUpdateViewTool, + slack_push_view: slackPushViewTool, + slack_publish_view: slackPublishViewTool, + slack_edit_canvas: slackEditCanvasTool, + slack_create_channel_canvas: slackCreateChannelCanvasTool, + slack_get_canvas: slackGetCanvasTool, + slack_list_canvases: slackListCanvasesTool, + slack_lookup_canvas_sections: slackLookupCanvasSectionsTool, + slack_delete_canvas: slackDeleteCanvasTool, + slack_create_conversation: slackCreateConversationTool, + slack_invite_to_conversation: slackInviteToConversationTool, + slack_schedule_message: slackScheduleMessageTool, + slack_list_scheduled_messages: slackListScheduledMessagesTool, + slack_delete_scheduled_message: slackDeleteScheduledMessageTool, + slack_archive_conversation: slackArchiveConversationTool, + slack_rename_conversation: slackRenameConversationTool, + slack_set_conversation_topic: slackSetConversationTopicTool, + slack_set_conversation_purpose: slackSetConversationPurposeTool, } diff --git a/bun.lock b/bun.lock index 0f9ba9efcf1..5d4c96f206d 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", diff --git a/packages/db/migrations/0258_swift_red_wolf.sql b/packages/db/migrations/0258_swift_red_wolf.sql new file mode 100644 index 00000000000..180d5266afa --- /dev/null +++ b/packages/db/migrations/0258_swift_red_wolf.sql @@ -0,0 +1 @@ +ALTER TABLE "workspace_files" ADD COLUMN "message_id" text; \ No newline at end of file diff --git a/packages/db/migrations/0258_tranquil_madame_web.sql b/packages/db/migrations/0258_tranquil_madame_web.sql new file mode 100644 index 00000000000..180d5266afa --- /dev/null +++ b/packages/db/migrations/0258_tranquil_madame_web.sql @@ -0,0 +1 @@ +ALTER TABLE "workspace_files" ADD COLUMN "message_id" text; \ No newline at end of file diff --git a/packages/db/migrations/meta/0258_snapshot.json b/packages/db/migrations/meta/0258_snapshot.json new file mode 100644 index 00000000000..f86e187c733 --- /dev/null +++ b/packages/db/migrations/meta/0258_snapshot.json @@ -0,0 +1,16724 @@ +{ + "id": "80b3d0e9-156b-4db8-aa07-fd1e8548aec1", + "prevId": "c405fc48-c6bb-4786-863f-3d3e686095b5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "workflow_mcp_server", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index b457b88600c..81b802dc07b 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1800,6 +1800,13 @@ "when": 1783531236677, "tag": "0257_majestic_chat", "breakpoints": true + }, + { + "idx": 258, + "version": "7", + "when": 1783551403285, + "tag": "0258_tranquil_madame_web", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 62bf0b5d806..77392d61ac5 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1666,6 +1666,16 @@ export const workspaceFiles = pgTable( }), context: text('context').notNull(), // 'workspace', 'mothership', 'copilot', 'chat', 'knowledge-base', 'profile-pictures', 'general', 'execution' chatId: uuid('chat_id').references(() => copilotChats.id, { onDelete: 'cascade' }), + /** + * Logical id of the copilot message this file was born in (the user message the + * upload was attached to). Plain text with no FK: message ids are only unique per + * chat — the same id legitimately exists in the source chat and every fork of it, + * which is what lets a fork's "copy files at-or-before this message" cut match rows + * in both. NULL means "birth unknown / not tracked": rows predating this column and + * contexts that don't stamp it. Nulled together with chatId when a file is + * materialized to the workspace. + */ + messageId: text('message_id'), originalName: text('original_name').notNull(), /** * Collision-disambiguated name exposed to the copilot VFS as `uploads/`. diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts index fedc7d65fec..b9b3d37165c 100644 --- a/packages/emcn/src/components/index.ts +++ b/packages/emcn/src/components/index.ts @@ -7,7 +7,7 @@ export { CalendarDayCell, type CalendarDayCellProps, } from './calendar/calendar-day-cell' -export { Checkbox } from './checkbox/checkbox' +export { Checkbox, checkboxIconVariants, checkboxVariants } from './checkbox/checkbox' export { Chip, ChipLink, diff --git a/packages/emcn/src/icons/chevron-left.tsx b/packages/emcn/src/icons/chevron-left.tsx new file mode 100644 index 00000000000..f79b8280e7f --- /dev/null +++ b/packages/emcn/src/icons/chevron-left.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * ChevronLeft icon component + * @param props - SVG properties including className, fill, etc. + */ +export function ChevronLeft(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/chevron-right.tsx b/packages/emcn/src/icons/chevron-right.tsx new file mode 100644 index 00000000000..2f2a591ae87 --- /dev/null +++ b/packages/emcn/src/icons/chevron-right.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * ChevronRight icon component + * @param props - SVG properties including className, fill, etc. + */ +export function ChevronRight(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/index.ts b/packages/emcn/src/icons/index.ts index 1af0459200b..81fba9fa370 100644 --- a/packages/emcn/src/icons/index.ts +++ b/packages/emcn/src/icons/index.ts @@ -15,6 +15,8 @@ export { Calendar } from './calendar' export { Card } from './card' export { Check } from './check' export { ChevronDown } from './chevron-down' +export { ChevronLeft } from './chevron-left' +export { ChevronRight } from './chevron-right' export { CircleAlert } from './circle-alert' export { CircleCheck } from './circle-check' export { CircleInfo } from './circle-info' @@ -87,6 +89,7 @@ export { ShieldCheck } from './shield-check' export { Shuffle } from './shuffle' export { Sim } from './sim' export { Slash } from './slash' +export { Split } from './split' export { Square } from './square' export { SquareArrowUpRight } from './square-arrow-up-right' export { Table } from './table' diff --git a/packages/emcn/src/icons/split.tsx b/packages/emcn/src/icons/split.tsx new file mode 100644 index 00000000000..aac39588a5c --- /dev/null +++ b/packages/emcn/src/icons/split.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * Split icon component - one path branching into two + * @param props - SVG properties including className, fill, etc. + */ +export function Split(props: SVGProps) { + return ( + + ) +}