Skip to content
Merged
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
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
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
3 changes: 2 additions & 1 deletion apps/sim/app/api/audit-logs/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { listAuditLogsContract } from '@/lib/api/contracts/audit-logs'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
Expand Down Expand Up @@ -78,7 +79,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
nextCursor,
})
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error'
const message = getErrorMessage(error, 'Unknown error')
logger.error('Audit logs fetch error', { error: message })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/auth/oauth/token/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import {
oauthTokenGetContract,
Expand Down Expand Up @@ -97,7 +98,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

return NextResponse.json({ accessToken }, { status: 200 })
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to get OAuth token'
const message = getErrorMessage(error, 'Failed to get OAuth token')
logger.warn(`[${requestId}] OAuth token error: ${message}`)
return NextResponse.json({ error: message }, { status: 403 })
}
Expand Down
7 changes: 4 additions & 3 deletions apps/sim/app/api/auth/sso/register/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { db, member, ssoProvider } from '@sim/db'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { ssoRegistrationContract } from '@/lib/api/contracts/auth'
Expand Down Expand Up @@ -249,7 +250,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})
} catch (error) {
logger.error('Error fetching OIDC discovery document', {
error: error instanceof Error ? error.message : 'Unknown error',
error: getErrorMessage(error, 'Unknown error'),
discoveryUrl,
})
return NextResponse.json(
Expand Down Expand Up @@ -427,15 +428,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
} catch (error) {
logger.error('Failed to register SSO provider', {
error,
errorMessage: error instanceof Error ? error.message : 'Unknown error',
errorMessage: getErrorMessage(error, 'Unknown error'),
errorStack: error instanceof Error ? error.stack : undefined,
errorDetails: JSON.stringify(error),
})

return NextResponse.json(
{
error: 'Failed to register SSO provider',
details: error instanceof Error ? error.message : 'Unknown error',
details: getErrorMessage(error, 'Unknown error'),
},
{ status: 500 }
)
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/billing/switch-plan/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { subscription as subscriptionTable } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { billingSwitchPlanContract } from '@/lib/api/contracts/subscription'
Expand Down Expand Up @@ -181,7 +181,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
error: toError(error).message,
})
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to switch plan' },
{ error: getErrorMessage(error, 'Failed to switch plan') },
{ status: 500 }
)
}
Expand Down
5 changes: 1 addition & 4 deletions apps/sim/app/api/chat/manage/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { chat } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { chatIdParamsSchema, updateChatContract } from '@/lib/api/contracts/chats'
Expand All @@ -20,10 +21,6 @@ export const maxDuration = 120

const logger = createLogger('ChatDetailAPI')

function getErrorMessage(error: unknown, fallback: string): string {
return error instanceof Error ? error.message : fallback
}

/**
* GET endpoint to fetch a specific chat deployment by ID
*/
Expand Down
5 changes: 1 addition & 4 deletions apps/sim/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { chat } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { createChatContract } from '@/lib/api/contracts/chats'
Expand All @@ -13,10 +14,6 @@ import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/

const logger = createLogger('ChatAPI')

function getErrorMessage(error: unknown, fallback: string): string {
return error instanceof Error ? error.message : fallback
}

export const GET = withRouteHandler(async (_request: NextRequest) => {
try {
const session = await getSession()
Expand Down
Loading
Loading