Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
25 changes: 17 additions & 8 deletions .claude/rules/global.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,31 @@ const tiny = generateShortId(8)
## Common Utilities
Use shared helpers from `@sim/utils` instead of writing inline implementations:

- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`
- `sleep(ms)` from `@sim/utils/helpers` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
- `toError(value)` from `@sim/utils/errors` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
- `getErrorMessage(value, fallback?)` from `@sim/utils/errors` — extract error message string. Never write `e instanceof Error ? e.message : 'fallback'`
- `structuredClone(value)` — built-in deep clone, no import needed. Never write `JSON.parse(JSON.stringify(obj))`
- `omit(obj, keys)` from `@sim/utils/object` — remove keys from object
- `filterUndefined(obj)` from `@sim/utils/object` — strip undefined-valued keys. Never write `Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))`
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — safe string truncation with ellipsis
- `backoffWithJitter(attempt, retryAfterMs, options?)` from `@sim/utils/retry` — exponential backoff with jitter
- `parseRetryAfter(header)` from `@sim/utils/retry` — parse HTTP `Retry-After` header to milliseconds

```typescript
// ✗ Bad
await new Promise(resolve => setTimeout(resolve, 1000))
const msg = error instanceof Error ? error.message : String(error)
const err = error instanceof Error ? error : new Error(String(error))
const msg = error instanceof Error ? error.message : 'Unknown error'
const clone = JSON.parse(JSON.stringify(obj))
const filtered = Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))

// ✓ Good
import { sleep } from '@sim/utils/helpers'
import { toError } from '@sim/utils/errors'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { filterUndefined } from '@sim/utils/object'
await sleep(1000)
const msg = toError(error).message
const err = toError(error)
const msg = getErrorMessage(error, 'Unknown error')
const clone = structuredClone(obj)
const filtered = filterUndefined(obj)
```

## Package Manager
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,4 @@ i18n.cache
.claude/launch.json
.claude/worktrees/
.claude/scheduled_tasks.lock
.deepsec/
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ You are a professional software engineer. All code must follow best practices: a
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
- **Styling**: Never update global styles. Keep all styling local to components
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id`
- **Common Utilities**: Use shared helpers from `@sim/utils` instead of inline implementations. `sleep(ms)` from `@sim/utils/helpers` for delays, `toError(e)` from `@sim/utils/errors` to normalize caught values.
- **Common Utilities**: Use shared helpers from `@sim/utils` instead of inline implementations:
- `sleep(ms)` from `@sim/utils/helpers` — never `new Promise(resolve => setTimeout(resolve, ms))`
- `toError(e)` from `@sim/utils/errors` — normalize caught values to `Error`
- `getErrorMessage(e, fallback?)` from `@sim/utils/errors` — extract message string from unknown caught value; never write `e instanceof Error ? e.message : 'fallback'`
- `structuredClone(value)` — built-in deep clone; never `JSON.parse(JSON.stringify(...))`
- `omit(obj, keys)` / `filterUndefined(obj)` from `@sim/utils/object` — object trimming; never `Object.fromEntries(Object.entries(...).filter(...))`
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — never inline slice + ellipsis
- `backoffWithJitter(attempt, retryAfterMs, options?)` / `parseRetryAfter(header)` from `@sim/utils/retry` — shared retry pacing; never reimplement exponential backoff inline
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`

## Architecture
Expand Down
3 changes: 2 additions & 1 deletion apps/realtime/src/database/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
VARIABLE_OPERATIONS,
WORKFLOW_OPERATIONS,
} from '@sim/realtime-protocol/constants'
import { randomFloat } from '@sim/utils/random'
import { getActiveWorkflowContext } from '@sim/workflow-authz'
import { loadWorkflowFromNormalizedTablesRaw } from '@sim/workflow-persistence/load'
import { mergeSubBlockValues } from '@sim/workflow-persistence/subblocks'
Expand Down Expand Up @@ -204,7 +205,7 @@ export async function persistWorkflowOperation(workflowId: string, operation: an
throw new Error(`Workflow ${workflowId} is archived or unavailable`)
}

if (op === BLOCK_OPERATIONS.UPDATE_POSITION && Math.random() < 0.01) {
if (op === BLOCK_OPERATIONS.UPDATE_POSITION && randomFloat() < 0.01) {
logger.debug('Socket DB operation sample:', {
operation: op,
target,
Expand Down
7 changes: 4 additions & 3 deletions apps/realtime/src/handlers/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
WORKFLOW_OPERATIONS,
} from '@sim/realtime-protocol/constants'
import { WorkflowOperationSchema } from '@sim/realtime-protocol/schemas'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz'
import { ZodError } from 'zod'
Expand Down Expand Up @@ -205,7 +206,7 @@ export function setupOperationsHandlers(socket: AuthenticatedSocket, roomManager
if (operationId) {
socket.emit('operation-failed', {
operationId,
error: error instanceof Error ? error.message : 'Database persistence failed',
error: getErrorMessage(error, 'Database persistence failed'),
retryable: true,
})
}
Expand Down Expand Up @@ -247,7 +248,7 @@ export function setupOperationsHandlers(socket: AuthenticatedSocket, roomManager
if (operationId) {
socket.emit('operation-failed', {
operationId,
error: error instanceof Error ? error.message : 'Database persistence failed',
error: getErrorMessage(error, 'Database persistence failed'),
retryable: true,
})
}
Expand Down Expand Up @@ -587,7 +588,7 @@ export function setupOperationsHandlers(socket: AuthenticatedSocket, roomManager
})
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
const errorMessage = getErrorMessage(error, 'Unknown error occurred')

if (operationId) {
socket.emit('operation-failed', {
Expand Down
5 changes: 3 additions & 2 deletions apps/realtime/src/handlers/subblocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { db } from '@sim/db'
import { workflow, workflowBlocks } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { SUBBLOCK_OPERATIONS } from '@sim/realtime-protocol/constants'
import { getErrorMessage } from '@sim/utils/errors'
import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz'
import { isWorkflowBlockProtected } from '@sim/workflow-types/workflow'
import { and, eq } from 'drizzle-orm'
Expand Down Expand Up @@ -208,7 +209,7 @@ export function setupSubblocksHandlers(socket: AuthenticatedSocket, roomManager:
} catch (error) {
logger.error('Error handling subblock update:', error)

const errorMessage = error instanceof Error ? error.message : 'Unknown error'
const errorMessage = getErrorMessage(error, 'Unknown error')

if (operationId) {
socket.emit('operation-failed', {
Expand Down Expand Up @@ -360,7 +361,7 @@ async function flushSubblockUpdate(
pending.opToSocket.forEach((socketId, opId) => {
io.to(socketId).emit('operation-failed', {
operationId: opId,
error: error instanceof Error ? error.message : 'Unknown error',
error: getErrorMessage(error, 'Unknown error'),
retryable: true,
})
})
Expand Down
5 changes: 3 additions & 2 deletions apps/realtime/src/handlers/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { db } from '@sim/db'
import { workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { VARIABLE_OPERATIONS } from '@sim/realtime-protocol/constants'
import { getErrorMessage } from '@sim/utils/errors'
import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz'
import { eq } from 'drizzle-orm'
import type { AuthenticatedSocket } from '@/middleware/auth'
Expand Down Expand Up @@ -195,7 +196,7 @@ export function setupVariablesHandlers(socket: AuthenticatedSocket, roomManager:
} catch (error) {
logger.error('Error handling variable update:', error)

const errorMessage = error instanceof Error ? error.message : 'Unknown error'
const errorMessage = getErrorMessage(error, 'Unknown error')

if (operationId) {
socket.emit('operation-failed', {
Expand Down Expand Up @@ -326,7 +327,7 @@ async function flushVariableUpdate(
pending.opToSocket.forEach((socketId, opId) => {
io.to(socketId).emit('operation-failed', {
operationId: opId,
error: error instanceof Error ? error.message : 'Unknown error',
error: getErrorMessage(error, 'Unknown error'),
retryable: true,
})
})
Expand Down
5 changes: 3 additions & 2 deletions apps/realtime/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
import { createServer, request as httpRequest } from 'http'
import { createMockLogger } from '@sim/testing'
import { randomInt } from '@sim/utils/random'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { createSocketIOServer } from '@/config/socket'
import { MemoryRoomManager } from '@/rooms'
Expand Down Expand Up @@ -95,7 +96,7 @@ describe('Socket Server Index Integration', () => {
})

beforeEach(async () => {
PORT = 3333 + Math.floor(Math.random() * 1000)
PORT = 3333 + randomInt(0, 1000)

httpServer = createServer()

Expand All @@ -120,7 +121,7 @@ describe('Socket Server Index Integration', () => {
httpServer.on('error', (err: any) => {
clearTimeout(timeout)
if (err.code === 'EADDRINUSE') {
PORT = 3333 + Math.floor(Math.random() * 1000)
PORT = 3333 + randomInt(0, 1000)
httpServer.close(() => {
httpServer.listen(PORT, '0.0.0.0', () => {
resolve()
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
Expand Down Expand Up @@ -292,8 +293,7 @@ export default function LoginPage({
},
})
} catch (requestError) {
let errorMessage =
requestError instanceof Error ? requestError.message : 'Failed to request password reset'
let errorMessage = getErrorMessage(requestError, 'Failed to request password reset')

if (
errorMessage.includes('Invalid body parameters') ||
Expand Down Expand Up @@ -325,7 +325,7 @@ export default function LoginPage({
logger.error('Error requesting password reset:', { error })
setResetStatus({
type: 'error',
message: error instanceof Error ? error.message : 'Failed to request password reset',
message: getErrorMessage(error, 'Failed to request password reset'),
})
} finally {
setIsSubmittingReset(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { Suspense, useState } from 'react'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { requestJson } from '@/lib/api/client/request'
Expand Down Expand Up @@ -53,7 +54,7 @@ function ResetPasswordContent() {
logger.error('Error resetting password:', { error })
setStatusMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Failed to reset password',
text: getErrorMessage(error, 'Failed to reset password'),
})
} finally {
setIsSubmitting(false)
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Modal,
ModalClose,
ModalContent,
ModalDescription,
ModalTitle,
ModalTrigger,
} from '@/components/emcn'
Expand Down Expand Up @@ -134,6 +135,9 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
<ModalTitle className='sr-only'>
{view === 'login' ? 'Log in' : 'Create account'}
</ModalTitle>
<ModalDescription className='sr-only'>
{view === 'login' ? 'Sign in to your account' : 'Create a new account'}
</ModalDescription>

<div className='relative px-6 pt-6 pb-6'>
<ModalClose className='absolute top-6 right-6 rounded-sm opacity-70 transition-opacity hover:opacity-100'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Modal,
ModalBody,
ModalContent,
ModalDescription,
ModalFooter,
ModalHeader,
ModalTrigger,
Expand Down Expand Up @@ -152,6 +153,9 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
}
>
<ModalBody>
<ModalDescription className='sr-only'>
Fill out this form to request a demo and talk to the sales team
</ModalDescription>
<div className='space-y-3'>
<div className='grid gap-3 sm:grid-cols-2'>
<LandingField htmlFor='firstName' label='First name' error={errors.firstName}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Modal,
ModalBody,
ModalContent,
ModalDescription,
ModalFooter,
ModalHeader,
Textarea,
Expand Down Expand Up @@ -83,6 +84,9 @@ export function RequestIntegrationModal() {

{status === 'success' ? (
<ModalBody>
<ModalDescription className='sr-only'>
Integration request submitted successfully
</ModalDescription>
<div className='flex flex-col items-center gap-3 py-6 text-center'>
<div className='flex size-10 items-center justify-center rounded-full bg-[#33C482]/10'>
<svg
Expand All @@ -106,6 +110,10 @@ export function RequestIntegrationModal() {
) : (
<form onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>
<ModalBody>
<ModalDescription className='sr-only'>
Submit a request for a new integration by entering the integration name and your
email
</ModalDescription>
<div className='space-y-3'>
<div className='flex flex-col gap-1'>
<Label htmlFor='integration-name'>Integration name</Label>
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/(landing)/integrations/data/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
extend_v2: ExtendIcon,
fathom: FathomIcon,
file_v3: DocumentIcon,
file_v4: DocumentIcon,
firecrawl: FirecrawlIcon,
fireflies_v2: FirefliesIcon,
gamma: GammaIcon,
Expand Down
14 changes: 9 additions & 5 deletions apps/sim/app/(landing)/integrations/data/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -4032,18 +4032,22 @@
"tags": ["meeting", "note-taking"]
},
{
"type": "file_v3",
"type": "file_v4",
"slug": "file",
"name": "File",
"description": "Read and write workspace files",
"longDescription": "Read and parse files from uploads or URLs, write new workspace files, or append content to existing files.",
"description": "Read, fetch, write, and append files",
"longDescription": "Read workspace files by picker or canonical ID, fetch and parse files from URLs with optional headers, write new workspace files, or append content to existing files.",
"bgColor": "#40916C",
"iconName": "DocumentIcon",
"docsUrl": "https://docs.sim.ai/tools/file",
"operations": [
{
"name": "Read",
"description": "Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc.)"
"description": "Get a workspace file object from a selected file or canonical workspace file ID."
},
{
"name": "Fetch",
"description": "Parse a file from a URL with optional custom headers for authenticated downloads."
},
{
"name": "Write",
Expand All @@ -4054,7 +4058,7 @@
"description": "Append content to an existing workspace file. The file must already exist. Content is added to the end of the file."
}
],
"operationCount": 3,
"operationCount": 4,
"triggers": [],
"triggerCount": 0,
"authType": "none",
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/a2a/serve/[agentId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Artifact, Message, PushNotificationConfig, TaskState } from '@a2a-
import { db } from '@sim/db'
import { a2aAgent, a2aPushNotificationConfig, a2aTask, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
Expand Down Expand Up @@ -1394,7 +1395,7 @@ async function handleTaskResubscribe(
logger.error('Error during SSE poll:', error)
sendEvent('error', {
code: A2A_ERROR_CODES.INTERNAL_ERROR,
message: error instanceof Error ? error.message : 'Polling failed',
message: getErrorMessage(error, 'Polling failed'),
})
cleanup()
try {
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/admin/mothership/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { db } from '@sim/db'
import { settings, user } from '@sim/db/schema'
import { getErrorMessage } from '@sim/utils/errors'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { adminMothershipQuerySchema } from '@/lib/api/contracts/mothership-tasks'
Expand Down Expand Up @@ -109,7 +110,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
} catch (error) {
return NextResponse.json(
{
error: `Failed to reach mothership (${environment}): ${error instanceof Error ? error.message : 'Unknown error'}`,
error: `Failed to reach mothership (${environment}): ${getErrorMessage(error, 'Unknown error')}`,
},
{ status: 502 }
)
Expand Down Expand Up @@ -165,7 +166,7 @@ export const GET = withRouteHandler(async (req: NextRequest) => {
} catch (error) {
return NextResponse.json(
{
error: `Failed to reach mothership (${environment}): ${error instanceof Error ? error.message : 'Unknown error'}`,
error: `Failed to reach mothership (${environment}): ${getErrorMessage(error, 'Unknown error')}`,
},
{ status: 502 }
)
Expand Down
Loading
Loading