mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
11 Commits
feat/crede
...
feat/fix-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
498504c35b | ||
|
|
c367da81fb | ||
|
|
7971a64e63 | ||
|
|
f39b4c74dc | ||
|
|
0ba8ab1ec7 | ||
|
|
039e57541e | ||
|
|
75f8c6ad7e | ||
|
|
c2b12cf21f | ||
|
|
4a9439e952 | ||
|
|
893e322a49 | ||
|
|
b0cb95be2f |
@@ -90,6 +90,7 @@ Sim also supports local models via [Ollama](https://ollama.ai) and [vLLM](https:
|
||||
git clone https://github.com/simstudioai/sim.git
|
||||
cd sim
|
||||
bun install
|
||||
bun run prepare # Set up pre-commit hooks
|
||||
```
|
||||
|
||||
2. Set up PostgreSQL with pgvector:
|
||||
@@ -104,6 +105,11 @@ Or install manually via the [pgvector guide](https://github.com/pgvector/pgvecto
|
||||
|
||||
```bash
|
||||
cp apps/sim/.env.example apps/sim/.env
|
||||
# Create your secrets
|
||||
perl -i -pe "s/your_encryption_key/$(openssl rand -hex 32)/" apps/sim/.env
|
||||
perl -i -pe "s/your_internal_api_secret/$(openssl rand -hex 32)/" apps/sim/.env
|
||||
perl -i -pe "s/your_api_encryption_key/$(openssl rand -hex 32)/" apps/sim/.env
|
||||
# DB configs for migration
|
||||
cp packages/db/.env.example packages/db/.env
|
||||
# Edit both .env files to set DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
|
||||
```
|
||||
@@ -111,7 +117,7 @@ cp packages/db/.env.example packages/db/.env
|
||||
4. Run migrations:
|
||||
|
||||
```bash
|
||||
cd packages/db && bunx drizzle-kit migrate --config=./drizzle.config.ts
|
||||
cd packages/db && bun run db:migrate
|
||||
```
|
||||
|
||||
5. Start development servers:
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Database (Required)
|
||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres"
|
||||
|
||||
# PostgreSQL Port (Optional) - defaults to 5432 if not specified
|
||||
# POSTGRES_PORT=5432
|
||||
DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
|
||||
|
||||
# Authentication (Required unless DISABLE_AUTH=true)
|
||||
BETTER_AUTH_SECRET=your_secret_key # Use `openssl rand -hex 32` to generate, or visit https://www.better-auth.com/docs/installation
|
||||
|
||||
@@ -99,8 +99,6 @@ function SignupFormContent({
|
||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
const turnstileRef = useRef<TurnstileInstance>(null)
|
||||
const captchaResolveRef = useRef<((token: string) => void) | null>(null)
|
||||
const captchaRejectRef = useRef<((reason: Error) => void) | null>(null)
|
||||
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
|
||||
const redirectUrl = useMemo(
|
||||
() => searchParams.get('redirect') || searchParams.get('callbackUrl') || '',
|
||||
@@ -258,27 +256,14 @@ function SignupFormContent({
|
||||
let token: string | undefined
|
||||
const widget = turnstileRef.current
|
||||
if (turnstileSiteKey && widget) {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
widget.reset()
|
||||
token = await Promise.race([
|
||||
new Promise<string>((resolve, reject) => {
|
||||
captchaResolveRef.current = resolve
|
||||
captchaRejectRef.current = reject
|
||||
widget.execute()
|
||||
}),
|
||||
new Promise<string>((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error('Captcha timed out')), 15_000)
|
||||
}),
|
||||
])
|
||||
widget.execute()
|
||||
token = await widget.getResponsePromise()
|
||||
} catch {
|
||||
setFormError('Captcha verification failed. Please try again.')
|
||||
setIsLoading(false)
|
||||
return
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
captchaResolveRef.current = null
|
||||
captchaRejectRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,10 +520,7 @@ function SignupFormContent({
|
||||
<Turnstile
|
||||
ref={turnstileRef}
|
||||
siteKey={turnstileSiteKey}
|
||||
onSuccess={(token) => captchaResolveRef.current?.(token)}
|
||||
onError={() => captchaRejectRef.current?.(new Error('Captcha verification failed'))}
|
||||
onExpire={() => captchaRejectRef.current?.(new Error('Captcha token expired'))}
|
||||
options={{ execution: 'execute' }}
|
||||
options={{ execution: 'execute', appearance: 'execute' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
formatPrice,
|
||||
formatTokenCount,
|
||||
formatUpdatedAt,
|
||||
getEffectiveMaxOutputTokens,
|
||||
getModelBySlug,
|
||||
getPricingBounds,
|
||||
getProviderBySlug,
|
||||
@@ -198,7 +199,8 @@ export default async function ModelPage({
|
||||
</div>
|
||||
|
||||
<p className='max-w-[820px] text-[17px] text-[var(--landing-text-muted)] leading-relaxed'>
|
||||
{model.summary} {model.bestFor}
|
||||
{model.summary}
|
||||
{model.bestFor ? ` ${model.bestFor}` : ''}
|
||||
</p>
|
||||
|
||||
<div className='mt-8 flex flex-wrap gap-3'>
|
||||
@@ -229,13 +231,11 @@ export default async function ModelPage({
|
||||
? `${formatPrice(model.pricing.cachedInput)}/1M`
|
||||
: 'N/A'
|
||||
}
|
||||
compact
|
||||
/>
|
||||
<StatCard label='Output price' value={`${formatPrice(model.pricing.output)}/1M`} />
|
||||
<StatCard
|
||||
label='Context window'
|
||||
value={model.contextWindow ? formatTokenCount(model.contextWindow) : 'Unknown'}
|
||||
compact
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -280,12 +280,12 @@ export default async function ModelPage({
|
||||
label='Max output'
|
||||
value={
|
||||
model.capabilities.maxOutputTokens
|
||||
? `${formatTokenCount(model.capabilities.maxOutputTokens)} tokens`
|
||||
: 'Standard defaults'
|
||||
? `${formatTokenCount(getEffectiveMaxOutputTokens(model.capabilities))} tokens`
|
||||
: 'Not published'
|
||||
}
|
||||
/>
|
||||
<DetailItem label='Provider' value={provider.name} />
|
||||
<DetailItem label='Best for' value={model.bestFor} />
|
||||
{model.bestFor ? <DetailItem label='Best for' value={model.bestFor} /> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
49
apps/sim/app/(landing)/models/utils.test.ts
Normal file
49
apps/sim/app/(landing)/models/utils.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildModelCapabilityFacts, getEffectiveMaxOutputTokens, getModelBySlug } from './utils'
|
||||
|
||||
describe('model catalog capability facts', () => {
|
||||
it.concurrent(
|
||||
'shows structured outputs support and published max output tokens for gpt-4o',
|
||||
() => {
|
||||
const model = getModelBySlug('openai', 'gpt-4o')
|
||||
|
||||
expect(model).not.toBeNull()
|
||||
expect(model).toBeDefined()
|
||||
|
||||
const capabilityFacts = buildModelCapabilityFacts(model!)
|
||||
const structuredOutputs = capabilityFacts.find((fact) => fact.label === 'Structured outputs')
|
||||
const maxOutputTokens = capabilityFacts.find((fact) => fact.label === 'Max output tokens')
|
||||
|
||||
expect(getEffectiveMaxOutputTokens(model!.capabilities)).toBe(16384)
|
||||
expect(structuredOutputs?.value).toBe('Supported')
|
||||
expect(maxOutputTokens?.value).toBe('16k')
|
||||
}
|
||||
)
|
||||
|
||||
it.concurrent('preserves native structured outputs labeling for claude models', () => {
|
||||
const model = getModelBySlug('anthropic', 'claude-sonnet-4-6')
|
||||
|
||||
expect(model).not.toBeNull()
|
||||
expect(model).toBeDefined()
|
||||
|
||||
const capabilityFacts = buildModelCapabilityFacts(model!)
|
||||
const structuredOutputs = capabilityFacts.find((fact) => fact.label === 'Structured outputs')
|
||||
|
||||
expect(structuredOutputs?.value).toBe('Supported (native)')
|
||||
})
|
||||
|
||||
it.concurrent('does not invent a max output token limit when one is not published', () => {
|
||||
expect(getEffectiveMaxOutputTokens({})).toBeNull()
|
||||
})
|
||||
|
||||
it.concurrent('keeps best-for copy for clearly differentiated models only', () => {
|
||||
const researchModel = getModelBySlug('google', 'deep-research-pro-preview-12-2025')
|
||||
const generalModel = getModelBySlug('xai', 'grok-4-latest')
|
||||
|
||||
expect(researchModel).not.toBeNull()
|
||||
expect(generalModel).not.toBeNull()
|
||||
|
||||
expect(researchModel?.bestFor).toContain('research workflows')
|
||||
expect(generalModel?.bestFor).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -112,7 +112,7 @@ export interface CatalogModel {
|
||||
capabilities: ModelCapabilities
|
||||
capabilityTags: string[]
|
||||
summary: string
|
||||
bestFor: string
|
||||
bestFor?: string
|
||||
searchText: string
|
||||
}
|
||||
|
||||
@@ -190,6 +190,14 @@ export function formatCapabilityBoolean(
|
||||
return value ? positive : negative
|
||||
}
|
||||
|
||||
function supportsCatalogStructuredOutputs(capabilities: ModelCapabilities): boolean {
|
||||
return !capabilities.deepResearch
|
||||
}
|
||||
|
||||
export function getEffectiveMaxOutputTokens(capabilities: ModelCapabilities): number | null {
|
||||
return capabilities.maxOutputTokens ?? null
|
||||
}
|
||||
|
||||
function trimTrailingZeros(value: string): string {
|
||||
return value.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
|
||||
}
|
||||
@@ -326,7 +334,7 @@ function buildCapabilityTags(capabilities: ModelCapabilities): string[] {
|
||||
tags.push('Tool choice')
|
||||
}
|
||||
|
||||
if (capabilities.nativeStructuredOutputs) {
|
||||
if (supportsCatalogStructuredOutputs(capabilities)) {
|
||||
tags.push('Structured outputs')
|
||||
}
|
||||
|
||||
@@ -365,7 +373,7 @@ function buildBestForLine(model: {
|
||||
pricing: PricingInfo
|
||||
capabilities: ModelCapabilities
|
||||
contextWindow: number | null
|
||||
}): string {
|
||||
}): string | null {
|
||||
const { pricing, capabilities, contextWindow } = model
|
||||
|
||||
if (capabilities.deepResearch) {
|
||||
@@ -376,10 +384,6 @@ function buildBestForLine(model: {
|
||||
return 'Best for reasoning-heavy tasks that need more deliberate model control.'
|
||||
}
|
||||
|
||||
if (pricing.input <= 0.2 && pricing.output <= 1.25) {
|
||||
return 'Best for cost-sensitive automations, background tasks, and high-volume workloads.'
|
||||
}
|
||||
|
||||
if (contextWindow && contextWindow >= 1000000) {
|
||||
return 'Best for long-context retrieval, large documents, and high-memory workflows.'
|
||||
}
|
||||
@@ -388,7 +392,11 @@ function buildBestForLine(model: {
|
||||
return 'Best for production workflows that need reliable typed outputs.'
|
||||
}
|
||||
|
||||
return 'Best for general-purpose AI workflows inside Sim.'
|
||||
if (pricing.input <= 0.2 && pricing.output <= 1.25) {
|
||||
return 'Best for cost-sensitive automations, background tasks, and high-volume workloads.'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function buildModelSummary(
|
||||
@@ -437,6 +445,11 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
|
||||
const shortId = stripProviderPrefix(provider.id, model.id)
|
||||
const mergedCapabilities = { ...provider.capabilities, ...model.capabilities }
|
||||
const capabilityTags = buildCapabilityTags(mergedCapabilities)
|
||||
const bestFor = buildBestForLine({
|
||||
pricing: model.pricing,
|
||||
capabilities: mergedCapabilities,
|
||||
contextWindow: model.contextWindow ?? null,
|
||||
})
|
||||
const displayName = formatModelDisplayName(provider.id, model.id)
|
||||
const modelSlug = slugify(shortId)
|
||||
const href = `/models/${providerSlug}/${modelSlug}`
|
||||
@@ -461,11 +474,7 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
|
||||
model.contextWindow ?? null,
|
||||
capabilityTags
|
||||
),
|
||||
bestFor: buildBestForLine({
|
||||
pricing: model.pricing,
|
||||
capabilities: mergedCapabilities,
|
||||
contextWindow: model.contextWindow ?? null,
|
||||
}),
|
||||
...(bestFor ? { bestFor } : {}),
|
||||
searchText: [
|
||||
provider.name,
|
||||
providerDisplayName,
|
||||
@@ -683,6 +692,7 @@ export function buildModelFaqs(provider: CatalogProvider, model: CatalogModel):
|
||||
|
||||
export function buildModelCapabilityFacts(model: CatalogModel): CapabilityFact[] {
|
||||
const { capabilities } = model
|
||||
const supportsStructuredOutputs = supportsCatalogStructuredOutputs(capabilities)
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -711,7 +721,11 @@ export function buildModelCapabilityFacts(model: CatalogModel): CapabilityFact[]
|
||||
},
|
||||
{
|
||||
label: 'Structured outputs',
|
||||
value: formatCapabilityBoolean(capabilities.nativeStructuredOutputs),
|
||||
value: supportsStructuredOutputs
|
||||
? capabilities.nativeStructuredOutputs
|
||||
? 'Supported (native)'
|
||||
: 'Supported'
|
||||
: 'Not supported',
|
||||
},
|
||||
{
|
||||
label: 'Tool choice',
|
||||
@@ -732,8 +746,8 @@ export function buildModelCapabilityFacts(model: CatalogModel): CapabilityFact[]
|
||||
{
|
||||
label: 'Max output tokens',
|
||||
value: capabilities.maxOutputTokens
|
||||
? formatTokenCount(capabilities.maxOutputTokens)
|
||||
: 'Standard defaults',
|
||||
? formatTokenCount(getEffectiveMaxOutputTokens(capabilities))
|
||||
: 'Not published',
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -752,8 +766,8 @@ export function getProviderCapabilitySummary(provider: CatalogProvider): Capabil
|
||||
const reasoningCount = provider.models.filter(
|
||||
(model) => model.capabilities.reasoningEffort || model.capabilities.thinking
|
||||
).length
|
||||
const structuredCount = provider.models.filter(
|
||||
(model) => model.capabilities.nativeStructuredOutputs
|
||||
const structuredCount = provider.models.filter((model) =>
|
||||
supportsCatalogStructuredOutputs(model.capabilities)
|
||||
).length
|
||||
const deepResearchCount = provider.models.filter(
|
||||
(model) => model.capabilities.deepResearch
|
||||
|
||||
96
apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts
Normal file
96
apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
type AlarmType,
|
||||
CloudWatchClient,
|
||||
DescribeAlarmsCommand,
|
||||
type StateValue,
|
||||
} from '@aws-sdk/client-cloudwatch'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
const logger = createLogger('CloudWatchDescribeAlarms')
|
||||
|
||||
const DescribeAlarmsSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
alarmNamePrefix: z.string().optional(),
|
||||
stateValue: z.preprocess(
|
||||
(v) => (v === '' ? undefined : v),
|
||||
z.enum(['OK', 'ALARM', 'INSUFFICIENT_DATA']).optional()
|
||||
),
|
||||
alarmType: z.preprocess(
|
||||
(v) => (v === '' ? undefined : v),
|
||||
z.enum(['MetricAlarm', 'CompositeAlarm']).optional()
|
||||
),
|
||||
limit: z.preprocess(
|
||||
(v) => (v === '' || v === undefined || v === null ? undefined : v),
|
||||
z.number({ coerce: true }).int().positive().optional()
|
||||
),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = DescribeAlarmsSchema.parse(body)
|
||||
|
||||
const client = new CloudWatchClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const command = new DescribeAlarmsCommand({
|
||||
...(validatedData.alarmNamePrefix && { AlarmNamePrefix: validatedData.alarmNamePrefix }),
|
||||
...(validatedData.stateValue && { StateValue: validatedData.stateValue as StateValue }),
|
||||
...(validatedData.alarmType && { AlarmTypes: [validatedData.alarmType as AlarmType] }),
|
||||
...(validatedData.limit !== undefined && { MaxRecords: validatedData.limit }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const metricAlarms = (response.MetricAlarms ?? []).map((a) => ({
|
||||
alarmName: a.AlarmName ?? '',
|
||||
alarmArn: a.AlarmArn ?? '',
|
||||
stateValue: a.StateValue ?? 'UNKNOWN',
|
||||
stateReason: a.StateReason ?? '',
|
||||
metricName: a.MetricName,
|
||||
namespace: a.Namespace,
|
||||
comparisonOperator: a.ComparisonOperator,
|
||||
threshold: a.Threshold,
|
||||
evaluationPeriods: a.EvaluationPeriods,
|
||||
stateUpdatedTimestamp: a.StateUpdatedTimestamp?.getTime(),
|
||||
}))
|
||||
|
||||
const compositeAlarms = (response.CompositeAlarms ?? []).map((a) => ({
|
||||
alarmName: a.AlarmName ?? '',
|
||||
alarmArn: a.AlarmArn ?? '',
|
||||
stateValue: a.StateValue ?? 'UNKNOWN',
|
||||
stateReason: a.StateReason ?? '',
|
||||
metricName: undefined,
|
||||
namespace: undefined,
|
||||
comparisonOperator: undefined,
|
||||
threshold: undefined,
|
||||
evaluationPeriods: undefined,
|
||||
stateUpdatedTimestamp: a.StateUpdatedTimestamp?.getTime(),
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { alarms: [...metricAlarms, ...compositeAlarms] },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to describe CloudWatch alarms'
|
||||
logger.error('DescribeAlarms failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { DescribeLogGroupsCommand } from '@aws-sdk/client-cloudwatch-logs'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createCloudWatchLogsClient } from '@/app/api/tools/cloudwatch/utils'
|
||||
|
||||
const logger = createLogger('CloudWatchDescribeLogGroups')
|
||||
|
||||
const DescribeLogGroupsSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
prefix: z.string().optional(),
|
||||
limit: z.preprocess(
|
||||
(v) => (v === '' || v === undefined || v === null ? undefined : v),
|
||||
z.number({ coerce: true }).int().positive().optional()
|
||||
),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = DescribeLogGroupsSchema.parse(body)
|
||||
|
||||
const client = createCloudWatchLogsClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const command = new DescribeLogGroupsCommand({
|
||||
...(validatedData.prefix && { logGroupNamePrefix: validatedData.prefix }),
|
||||
...(validatedData.limit !== undefined && { limit: validatedData.limit }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const logGroups = (response.logGroups ?? []).map((lg) => ({
|
||||
logGroupName: lg.logGroupName ?? '',
|
||||
arn: lg.arn ?? '',
|
||||
storedBytes: lg.storedBytes ?? 0,
|
||||
retentionInDays: lg.retentionInDays,
|
||||
creationTime: lg.creationTime,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { logGroups },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to describe CloudWatch log groups'
|
||||
logger.error('DescribeLogGroups failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createCloudWatchLogsClient, describeLogStreams } from '@/app/api/tools/cloudwatch/utils'
|
||||
|
||||
const logger = createLogger('CloudWatchDescribeLogStreams')
|
||||
|
||||
const DescribeLogStreamsSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
logGroupName: z.string().min(1, 'Log group name is required'),
|
||||
prefix: z.string().optional(),
|
||||
limit: z.preprocess(
|
||||
(v) => (v === '' || v === undefined || v === null ? undefined : v),
|
||||
z.number({ coerce: true }).int().positive().optional()
|
||||
),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = DescribeLogStreamsSchema.parse(body)
|
||||
|
||||
const client = createCloudWatchLogsClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const result = await describeLogStreams(client, validatedData.logGroupName, {
|
||||
prefix: validatedData.prefix,
|
||||
limit: validatedData.limit,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { logStreams: result.logStreams },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to describe CloudWatch log streams'
|
||||
logger.error('DescribeLogStreams failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
60
apps/sim/app/api/tools/cloudwatch/get-log-events/route.ts
Normal file
60
apps/sim/app/api/tools/cloudwatch/get-log-events/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createCloudWatchLogsClient, getLogEvents } from '@/app/api/tools/cloudwatch/utils'
|
||||
|
||||
const logger = createLogger('CloudWatchGetLogEvents')
|
||||
|
||||
const GetLogEventsSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
logGroupName: z.string().min(1, 'Log group name is required'),
|
||||
logStreamName: z.string().min(1, 'Log stream name is required'),
|
||||
startTime: z.number({ coerce: true }).int().optional(),
|
||||
endTime: z.number({ coerce: true }).int().optional(),
|
||||
limit: z.preprocess(
|
||||
(v) => (v === '' || v === undefined || v === null ? undefined : v),
|
||||
z.number({ coerce: true }).int().positive().optional()
|
||||
),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = GetLogEventsSchema.parse(body)
|
||||
|
||||
const client = createCloudWatchLogsClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const result = await getLogEvents(
|
||||
client,
|
||||
validatedData.logGroupName,
|
||||
validatedData.logStreamName,
|
||||
{
|
||||
startTime: validatedData.startTime,
|
||||
endTime: validatedData.endTime,
|
||||
limit: validatedData.limit,
|
||||
}
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { events: result.events },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get CloudWatch log events'
|
||||
logger.error('GetLogEvents failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { CloudWatchClient, GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
const logger = createLogger('CloudWatchGetMetricStatistics')
|
||||
|
||||
const GetMetricStatisticsSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
namespace: z.string().min(1, 'Namespace is required'),
|
||||
metricName: z.string().min(1, 'Metric name is required'),
|
||||
startTime: z.number({ coerce: true }).int(),
|
||||
endTime: z.number({ coerce: true }).int(),
|
||||
period: z.number({ coerce: true }).int().min(1),
|
||||
statistics: z.array(z.enum(['Average', 'Sum', 'Minimum', 'Maximum', 'SampleCount'])).min(1),
|
||||
dimensions: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = GetMetricStatisticsSchema.parse(body)
|
||||
|
||||
const client = new CloudWatchClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
let parsedDimensions: { Name: string; Value: string }[] | undefined
|
||||
if (validatedData.dimensions) {
|
||||
try {
|
||||
const dims = JSON.parse(validatedData.dimensions)
|
||||
if (Array.isArray(dims)) {
|
||||
parsedDimensions = dims.map((d: Record<string, string>) => ({
|
||||
Name: d.name,
|
||||
Value: d.value,
|
||||
}))
|
||||
} else if (typeof dims === 'object') {
|
||||
parsedDimensions = Object.entries(dims).map(([name, value]) => ({
|
||||
Name: name,
|
||||
Value: String(value),
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
throw new Error('Invalid dimensions JSON')
|
||||
}
|
||||
}
|
||||
|
||||
const command = new GetMetricStatisticsCommand({
|
||||
Namespace: validatedData.namespace,
|
||||
MetricName: validatedData.metricName,
|
||||
StartTime: new Date(validatedData.startTime * 1000),
|
||||
EndTime: new Date(validatedData.endTime * 1000),
|
||||
Period: validatedData.period,
|
||||
Statistics: validatedData.statistics,
|
||||
...(parsedDimensions && { Dimensions: parsedDimensions }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const datapoints = (response.Datapoints ?? [])
|
||||
.sort((a, b) => (a.Timestamp?.getTime() ?? 0) - (b.Timestamp?.getTime() ?? 0))
|
||||
.map((dp) => ({
|
||||
timestamp: dp.Timestamp ? Math.floor(dp.Timestamp.getTime() / 1000) : 0,
|
||||
average: dp.Average,
|
||||
sum: dp.Sum,
|
||||
minimum: dp.Minimum,
|
||||
maximum: dp.Maximum,
|
||||
sampleCount: dp.SampleCount,
|
||||
unit: dp.Unit,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
label: response.Label ?? validatedData.metricName,
|
||||
datapoints,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get CloudWatch metric statistics'
|
||||
logger.error('GetMetricStatistics failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
67
apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts
Normal file
67
apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { CloudWatchClient, ListMetricsCommand } from '@aws-sdk/client-cloudwatch'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
|
||||
const logger = createLogger('CloudWatchListMetrics')
|
||||
|
||||
const ListMetricsSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
namespace: z.string().optional(),
|
||||
metricName: z.string().optional(),
|
||||
recentlyActive: z.boolean().optional(),
|
||||
limit: z.preprocess(
|
||||
(v) => (v === '' || v === undefined || v === null ? undefined : v),
|
||||
z.number({ coerce: true }).int().positive().optional()
|
||||
),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = ListMetricsSchema.parse(body)
|
||||
|
||||
const client = new CloudWatchClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const command = new ListMetricsCommand({
|
||||
...(validatedData.namespace && { Namespace: validatedData.namespace }),
|
||||
...(validatedData.metricName && { MetricName: validatedData.metricName }),
|
||||
...(validatedData.recentlyActive && { RecentlyActive: 'PT3H' }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const metrics = (response.Metrics ?? []).slice(0, validatedData.limit ?? 500).map((m) => ({
|
||||
namespace: m.Namespace ?? '',
|
||||
metricName: m.MetricName ?? '',
|
||||
dimensions: (m.Dimensions ?? []).map((d) => ({
|
||||
name: d.Name ?? '',
|
||||
value: d.Value ?? '',
|
||||
})),
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { metrics },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to list CloudWatch metrics'
|
||||
logger.error('ListMetrics failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
71
apps/sim/app/api/tools/cloudwatch/query-logs/route.ts
Normal file
71
apps/sim/app/api/tools/cloudwatch/query-logs/route.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createCloudWatchLogsClient, pollQueryResults } from '@/app/api/tools/cloudwatch/utils'
|
||||
|
||||
const logger = createLogger('CloudWatchQueryLogs')
|
||||
|
||||
const QueryLogsSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
logGroupNames: z.array(z.string().min(1)).min(1, 'At least one log group name is required'),
|
||||
queryString: z.string().min(1, 'Query string is required'),
|
||||
startTime: z.number({ coerce: true }).int(),
|
||||
endTime: z.number({ coerce: true }).int(),
|
||||
limit: z.preprocess(
|
||||
(v) => (v === '' || v === undefined || v === null ? undefined : v),
|
||||
z.number({ coerce: true }).int().positive().optional()
|
||||
),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = QueryLogsSchema.parse(body)
|
||||
|
||||
const client = createCloudWatchLogsClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const startQueryCommand = new StartQueryCommand({
|
||||
logGroupNames: validatedData.logGroupNames,
|
||||
queryString: validatedData.queryString,
|
||||
startTime: validatedData.startTime,
|
||||
endTime: validatedData.endTime,
|
||||
...(validatedData.limit !== undefined && { limit: validatedData.limit }),
|
||||
})
|
||||
|
||||
const startQueryResponse = await client.send(startQueryCommand)
|
||||
const queryId = startQueryResponse.queryId
|
||||
|
||||
if (!queryId) {
|
||||
throw new Error('Failed to start CloudWatch Log Insights query: no queryId returned')
|
||||
}
|
||||
|
||||
const result = await pollQueryResults(client, queryId)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
results: result.results,
|
||||
statistics: result.statistics,
|
||||
status: result.status,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'CloudWatch Log Insights query failed'
|
||||
logger.error('QueryLogs failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
161
apps/sim/app/api/tools/cloudwatch/utils.ts
Normal file
161
apps/sim/app/api/tools/cloudwatch/utils.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
CloudWatchLogsClient,
|
||||
DescribeLogStreamsCommand,
|
||||
GetLogEventsCommand,
|
||||
GetQueryResultsCommand,
|
||||
type ResultField,
|
||||
} from '@aws-sdk/client-cloudwatch-logs'
|
||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
||||
|
||||
interface AwsCredentials {
|
||||
region: string
|
||||
accessKeyId: string
|
||||
secretAccessKey: string
|
||||
}
|
||||
|
||||
export function createCloudWatchLogsClient(config: AwsCredentials): CloudWatchLogsClient {
|
||||
return new CloudWatchLogsClient({
|
||||
region: config.region,
|
||||
credentials: {
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface PollOptions {
|
||||
maxWaitMs?: number
|
||||
pollIntervalMs?: number
|
||||
}
|
||||
|
||||
interface PollResult {
|
||||
results: Record<string, string>[]
|
||||
statistics: {
|
||||
bytesScanned: number
|
||||
recordsMatched: number
|
||||
recordsScanned: number
|
||||
}
|
||||
status: string
|
||||
}
|
||||
|
||||
function parseResultFields(fields: ResultField[] | undefined): Record<string, string> {
|
||||
const record: Record<string, string> = {}
|
||||
if (!fields) return record
|
||||
for (const field of fields) {
|
||||
if (field.field && field.value !== undefined) {
|
||||
record[field.field] = field.value ?? ''
|
||||
}
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
export async function pollQueryResults(
|
||||
client: CloudWatchLogsClient,
|
||||
queryId: string,
|
||||
options: PollOptions = {}
|
||||
): Promise<PollResult> {
|
||||
const { maxWaitMs = DEFAULT_EXECUTION_TIMEOUT_MS, pollIntervalMs = 1_000 } = options
|
||||
const startTime = Date.now()
|
||||
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
const command = new GetQueryResultsCommand({ queryId })
|
||||
const response = await client.send(command)
|
||||
|
||||
const status = response.status ?? 'Unknown'
|
||||
|
||||
if (status === 'Complete') {
|
||||
return {
|
||||
results: (response.results ?? []).map(parseResultFields),
|
||||
statistics: {
|
||||
bytesScanned: response.statistics?.bytesScanned ?? 0,
|
||||
recordsMatched: response.statistics?.recordsMatched ?? 0,
|
||||
recordsScanned: response.statistics?.recordsScanned ?? 0,
|
||||
},
|
||||
status,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'Failed' || status === 'Cancelled') {
|
||||
throw new Error(`CloudWatch Log Insights query ${status.toLowerCase()}`)
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
|
||||
}
|
||||
|
||||
// Timeout -- fetch one last time for partial results
|
||||
const finalResponse = await client.send(new GetQueryResultsCommand({ queryId }))
|
||||
return {
|
||||
results: (finalResponse.results ?? []).map(parseResultFields),
|
||||
statistics: {
|
||||
bytesScanned: finalResponse.statistics?.bytesScanned ?? 0,
|
||||
recordsMatched: finalResponse.statistics?.recordsMatched ?? 0,
|
||||
recordsScanned: finalResponse.statistics?.recordsScanned ?? 0,
|
||||
},
|
||||
status: `Timeout (last status: ${finalResponse.status ?? 'Unknown'})`,
|
||||
}
|
||||
}
|
||||
|
||||
export async function describeLogStreams(
|
||||
client: CloudWatchLogsClient,
|
||||
logGroupName: string,
|
||||
options?: { prefix?: string; limit?: number }
|
||||
): Promise<{
|
||||
logStreams: {
|
||||
logStreamName: string
|
||||
lastEventTimestamp: number | undefined
|
||||
firstEventTimestamp: number | undefined
|
||||
creationTime: number | undefined
|
||||
storedBytes: number
|
||||
}[]
|
||||
}> {
|
||||
const hasPrefix = Boolean(options?.prefix)
|
||||
const command = new DescribeLogStreamsCommand({
|
||||
logGroupName,
|
||||
...(hasPrefix
|
||||
? { orderBy: 'LogStreamName', logStreamNamePrefix: options!.prefix }
|
||||
: { orderBy: 'LastEventTime', descending: true }),
|
||||
...(options?.limit !== undefined && { limit: options.limit }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
return {
|
||||
logStreams: (response.logStreams ?? []).map((ls) => ({
|
||||
logStreamName: ls.logStreamName ?? '',
|
||||
lastEventTimestamp: ls.lastEventTimestamp,
|
||||
firstEventTimestamp: ls.firstEventTimestamp,
|
||||
creationTime: ls.creationTime,
|
||||
storedBytes: ls.storedBytes ?? 0,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLogEvents(
|
||||
client: CloudWatchLogsClient,
|
||||
logGroupName: string,
|
||||
logStreamName: string,
|
||||
options?: { startTime?: number; endTime?: number; limit?: number }
|
||||
): Promise<{
|
||||
events: {
|
||||
timestamp: number | undefined
|
||||
message: string | undefined
|
||||
ingestionTime: number | undefined
|
||||
}[]
|
||||
}> {
|
||||
const command = new GetLogEventsCommand({
|
||||
logGroupIdentifier: logGroupName,
|
||||
logStreamName,
|
||||
...(options?.startTime !== undefined && { startTime: options.startTime * 1000 }),
|
||||
...(options?.endTime !== undefined && { endTime: options.endTime * 1000 }),
|
||||
...(options?.limit !== undefined && { limit: options.limit }),
|
||||
startFromHead: true,
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
return {
|
||||
events: (response.events ?? []).map((e) => ({
|
||||
timestamp: e.timestamp,
|
||||
message: e.message,
|
||||
ingestionTime: e.ingestionTime,
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,44 @@
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
|
||||
export async function GET() {
|
||||
export function GET() {
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
const llmsContent = `# Sim
|
||||
const content = `# Sim
|
||||
|
||||
> Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.
|
||||
> Sim is the open-source platform to build AI agents and run your agentic workforce. Connect integrations and LLMs to deploy and orchestrate agentic workflows.
|
||||
|
||||
Sim lets teams create agents, workflows, knowledge bases, tables, and docs. Over 100,000 builders use Sim — from startups to Fortune 500 companies. SOC2 compliant.
|
||||
Sim lets teams create agents, workflows, knowledge bases, tables, and docs. It supports both product discovery pages and deeper technical documentation.
|
||||
|
||||
## Core Pages
|
||||
## Preferred URLs
|
||||
|
||||
- [Homepage](${baseUrl}): Product overview, features, and pricing
|
||||
- [Homepage](${baseUrl}): Product overview and primary entry point
|
||||
- [Integrations directory](${baseUrl}/integrations): Public catalog of integrations and automation capabilities
|
||||
- [Models directory](${baseUrl}/models): Public catalog of AI models, pricing, context windows, and capabilities
|
||||
- [Blog](${baseUrl}/blog): Announcements, guides, and product context
|
||||
- [Changelog](${baseUrl}/changelog): Product updates and release notes
|
||||
- [Sim Blog](${baseUrl}/blog): Announcements, insights, and guides
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Documentation](https://docs.sim.ai): Complete guides and API reference
|
||||
- [Quickstart](https://docs.sim.ai/quickstart): Get started in 5 minutes
|
||||
- [API Reference](https://docs.sim.ai/api): REST API documentation
|
||||
- [Documentation](https://docs.sim.ai): Product guides and technical reference
|
||||
- [Quickstart](https://docs.sim.ai/quickstart): Fastest path to getting started
|
||||
- [API Reference](https://docs.sim.ai/api): API documentation
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Workspace**: Container for workflows, data sources, and executions
|
||||
- **Workflow**: Directed graph of blocks defining an agentic process
|
||||
- **Block**: Individual step (LLM call, tool call, HTTP request, code execution)
|
||||
- **Block**: Individual step such as an LLM call, tool call, HTTP request, or code execution
|
||||
- **Trigger**: Event or schedule that initiates workflow execution
|
||||
- **Execution**: A single run of a workflow with logs and outputs
|
||||
- **Knowledge Base**: Vector-indexed document store for retrieval-augmented generation
|
||||
- **Knowledge Base**: Document store used for retrieval-augmented generation
|
||||
|
||||
## Capabilities
|
||||
|
||||
- AI agent creation and deployment
|
||||
- Agentic workflow orchestration
|
||||
- 1,000+ integrations (Slack, Gmail, Notion, Airtable, databases, and more)
|
||||
- Multi-model LLM orchestration (OpenAI, Anthropic, Google, Mistral, xAI, Perplexity)
|
||||
- Knowledge base creation with retrieval-augmented generation (RAG)
|
||||
- Integrations across business tools, databases, and communication platforms
|
||||
- Multi-model LLM orchestration
|
||||
- Knowledge bases and retrieval-augmented generation
|
||||
- Table creation and management
|
||||
- Document creation and processing
|
||||
- Scheduled and webhook-triggered executions
|
||||
@@ -45,24 +47,19 @@ Sim lets teams create agents, workflows, knowledge bases, tables, and docs. Over
|
||||
|
||||
- AI agent deployment and orchestration
|
||||
- Knowledge bases and RAG pipelines
|
||||
- Document creation and processing
|
||||
- Customer support automation
|
||||
- Internal operations (sales, marketing, legal, finance)
|
||||
- Internal operations workflows across sales, marketing, legal, and finance
|
||||
|
||||
## Links
|
||||
## Additional Links
|
||||
|
||||
- [GitHub Repository](https://github.com/simstudioai/sim): Open-source codebase
|
||||
- [Discord Community](https://discord.gg/Hr4UWYEcTT): Get help and connect with 100,000+ builders
|
||||
- [X/Twitter](https://x.com/simdotai): Product updates and announcements
|
||||
|
||||
## Optional
|
||||
|
||||
- [Careers](https://jobs.ashbyhq.com/sim): Join the Sim team
|
||||
- [Docs](https://docs.sim.ai): Canonical documentation source
|
||||
- [Terms of Service](${baseUrl}/terms): Legal terms
|
||||
- [Privacy Policy](${baseUrl}/privacy): Data handling practices
|
||||
- [Sitemap](${baseUrl}/sitemap.xml): Public URL inventory
|
||||
`
|
||||
|
||||
return new Response(llmsContent, {
|
||||
return new Response(content, {
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown; charset=utf-8',
|
||||
'Cache-Control': 'public, max-age=86400, s-maxage=86400',
|
||||
|
||||
@@ -8,6 +8,34 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
const now = new Date()
|
||||
const integrationPages: MetadataRoute.Sitemap = integrations.map((integration) => ({
|
||||
url: `${baseUrl}/integrations/${integration.slug}`,
|
||||
lastModified: now,
|
||||
}))
|
||||
const modelHubPages: MetadataRoute.Sitemap = [
|
||||
{
|
||||
url: `${baseUrl}/integrations`,
|
||||
lastModified: now,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/models`,
|
||||
lastModified: now,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/partners`,
|
||||
lastModified: now,
|
||||
},
|
||||
]
|
||||
const providerPages: MetadataRoute.Sitemap = MODEL_PROVIDERS_WITH_CATALOGS.map((provider) => ({
|
||||
url: `${baseUrl}${provider.href}`,
|
||||
lastModified: new Date(
|
||||
Math.max(...provider.models.map((model) => new Date(model.pricing.updatedAt).getTime()))
|
||||
),
|
||||
}))
|
||||
const modelPages: MetadataRoute.Sitemap = ALL_CATALOG_MODELS.map((model) => ({
|
||||
url: `${baseUrl}${model.href}`,
|
||||
lastModified: new Date(model.pricing.updatedAt),
|
||||
}))
|
||||
|
||||
const staticPages: MetadataRoute.Sitemap = [
|
||||
{
|
||||
@@ -26,14 +54,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
// url: `${baseUrl}/templates`,
|
||||
// lastModified: now,
|
||||
// },
|
||||
{
|
||||
url: `${baseUrl}/integrations`,
|
||||
lastModified: now,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/models`,
|
||||
lastModified: now,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/changelog`,
|
||||
lastModified: now,
|
||||
@@ -54,20 +74,12 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
lastModified: new Date(p.updated ?? p.date),
|
||||
}))
|
||||
|
||||
const integrationPages: MetadataRoute.Sitemap = integrations.map((i) => ({
|
||||
url: `${baseUrl}/integrations/${i.slug}`,
|
||||
lastModified: now,
|
||||
}))
|
||||
|
||||
const providerPages: MetadataRoute.Sitemap = MODEL_PROVIDERS_WITH_CATALOGS.map((provider) => ({
|
||||
url: `${baseUrl}${provider.href}`,
|
||||
lastModified: now,
|
||||
}))
|
||||
|
||||
const modelPages: MetadataRoute.Sitemap = ALL_CATALOG_MODELS.map((model) => ({
|
||||
url: `${baseUrl}${model.href}`,
|
||||
lastModified: new Date(model.pricing.updatedAt),
|
||||
}))
|
||||
|
||||
return [...staticPages, ...blogPages, ...integrationPages, ...providerPages, ...modelPages]
|
||||
return [
|
||||
...staticPages,
|
||||
...modelHubPages,
|
||||
...integrationPages,
|
||||
...providerPages,
|
||||
...modelPages,
|
||||
...blogPages,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,22 +1,59 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Check, Copy, Ellipsis, Hash } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Button,
|
||||
Check,
|
||||
Copy,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Textarea,
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
} from '@/components/emcn'
|
||||
import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback'
|
||||
|
||||
const SPECIAL_TAGS = 'thinking|options|usage_upgrade|credential|mothership-error|file'
|
||||
|
||||
function toPlainText(raw: string): string {
|
||||
return (
|
||||
raw
|
||||
// Strip special tags and their contents
|
||||
.replace(new RegExp(`<\\/?(${SPECIAL_TAGS})(?:>[\\s\\S]*?<\\/(${SPECIAL_TAGS})>|>)`, 'g'), '')
|
||||
// Strip markdown
|
||||
.replace(/^#{1,6}\s+/gm, '')
|
||||
.replace(/\*\*(.+?)\*\*/g, '$1')
|
||||
.replace(/\*(.+?)\*/g, '$1')
|
||||
.replace(/`{3}[\s\S]*?`{3}/g, '')
|
||||
.replace(/`(.+?)`/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
.replace(/^[>\-*]\s+/gm, '')
|
||||
.replace(/!\[[^\]]*\]\([^)]+\)/g, '')
|
||||
// Normalize whitespace
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
)
|
||||
}
|
||||
|
||||
const ICON_CLASS = 'h-[14px] w-[14px]'
|
||||
const BUTTON_CLASS =
|
||||
'flex h-[26px] w-[26px] items-center justify-center rounded-[6px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-hover)] focus-visible:outline-none'
|
||||
|
||||
interface MessageActionsProps {
|
||||
content: string
|
||||
requestId?: string
|
||||
chatId?: string
|
||||
userQuery?: string
|
||||
}
|
||||
|
||||
export function MessageActions({ content, requestId }: MessageActionsProps) {
|
||||
const [copied, setCopied] = useState<'message' | 'request' | null>(null)
|
||||
export function MessageActions({ content, chatId, userQuery }: MessageActionsProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [pendingFeedback, setPendingFeedback] = useState<'up' | 'down' | null>(null)
|
||||
const [feedbackText, setFeedbackText] = useState('')
|
||||
const resetTimeoutRef = useRef<number | null>(null)
|
||||
const submitFeedback = useSubmitCopilotFeedback()
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -26,59 +63,119 @@ export function MessageActions({ content, requestId }: MessageActionsProps) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const copyToClipboard = useCallback(async (text: string, type: 'message' | 'request') => {
|
||||
const copyToClipboard = useCallback(async () => {
|
||||
if (!content) return
|
||||
const text = toPlainText(content)
|
||||
if (!text) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(type)
|
||||
setCopied(true)
|
||||
if (resetTimeoutRef.current !== null) {
|
||||
window.clearTimeout(resetTimeoutRef.current)
|
||||
}
|
||||
resetTimeoutRef.current = window.setTimeout(() => setCopied(null), 1500)
|
||||
resetTimeoutRef.current = window.setTimeout(() => setCopied(false), 1500)
|
||||
} catch {
|
||||
/* clipboard unavailable */
|
||||
}
|
||||
}, [content])
|
||||
|
||||
const handleFeedbackClick = useCallback(
|
||||
(type: 'up' | 'down') => {
|
||||
if (chatId && userQuery) {
|
||||
setPendingFeedback(type)
|
||||
setFeedbackText('')
|
||||
}
|
||||
},
|
||||
[chatId, userQuery]
|
||||
)
|
||||
|
||||
const handleSubmitFeedback = useCallback(() => {
|
||||
if (!pendingFeedback || !chatId || !userQuery) return
|
||||
const text = feedbackText.trim()
|
||||
if (!text) {
|
||||
setPendingFeedback(null)
|
||||
setFeedbackText('')
|
||||
return
|
||||
}
|
||||
submitFeedback.mutate({
|
||||
chatId,
|
||||
userQuery,
|
||||
agentResponse: content,
|
||||
isPositiveFeedback: pendingFeedback === 'up',
|
||||
feedback: text,
|
||||
})
|
||||
setPendingFeedback(null)
|
||||
setFeedbackText('')
|
||||
}, [pendingFeedback, chatId, userQuery, content, feedbackText])
|
||||
|
||||
const handleModalClose = useCallback((open: boolean) => {
|
||||
if (!open) {
|
||||
setPendingFeedback(null)
|
||||
setFeedbackText('')
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!content && !requestId) {
|
||||
return null
|
||||
}
|
||||
if (!content) return null
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<>
|
||||
<div className='flex items-center gap-0.5'>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='More options'
|
||||
className='flex h-5 w-5 items-center justify-center rounded-sm text-[var(--text-icon)] opacity-0 transition-colors transition-opacity hover-hover:bg-[var(--surface-3)] hover-hover:text-[var(--text-primary)] focus-visible:opacity-100 focus-visible:outline-none group-hover/msg:opacity-100 data-[state=open]:opacity-100'
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label='Copy message'
|
||||
onClick={copyToClipboard}
|
||||
className={BUTTON_CLASS}
|
||||
>
|
||||
<Ellipsis className='h-3 w-3' strokeWidth={2} />
|
||||
{copied ? <Check className={ICON_CLASS} /> : <Copy className={ICON_CLASS} />}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' side='top' sideOffset={4}>
|
||||
<DropdownMenuItem
|
||||
disabled={!content}
|
||||
onSelect={(event) => {
|
||||
event.stopPropagation()
|
||||
void copyToClipboard(content, 'message')
|
||||
}}
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Like'
|
||||
onClick={() => handleFeedbackClick('up')}
|
||||
className={BUTTON_CLASS}
|
||||
>
|
||||
{copied === 'message' ? <Check /> : <Copy />}
|
||||
<span>Copy Message</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={!requestId}
|
||||
onSelect={(event) => {
|
||||
event.stopPropagation()
|
||||
if (requestId) {
|
||||
void copyToClipboard(requestId, 'request')
|
||||
}
|
||||
}}
|
||||
<ThumbsUp className={ICON_CLASS} />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Dislike'
|
||||
onClick={() => handleFeedbackClick('down')}
|
||||
className={BUTTON_CLASS}
|
||||
>
|
||||
{copied === 'request' ? <Check /> : <Hash />}
|
||||
<span>Copy Request ID</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ThumbsDown className={ICON_CLASS} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Modal open={pendingFeedback !== null} onOpenChange={handleModalClose}>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Give feedback</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<p className='font-medium text-[var(--text-secondary)] text-sm'>
|
||||
{pendingFeedback === 'up' ? 'What did you like?' : 'What could be improved?'}
|
||||
</p>
|
||||
<Textarea
|
||||
placeholder={
|
||||
pendingFeedback === 'up'
|
||||
? 'Tell us what was helpful...'
|
||||
: 'Tell us what went wrong...'
|
||||
}
|
||||
value={feedbackText}
|
||||
onChange={(e) => setFeedbackText(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='default' onClick={() => handleModalClose(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='primary' onClick={handleSubmitFeedback}>
|
||||
Submit
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, memo, useContext, useMemo, useRef } from 'react'
|
||||
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import type { Components, ExtraProps } from 'react-markdown'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import remarkBreaks from 'remark-breaks'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Checkbox } from '@/components/emcn'
|
||||
@@ -70,6 +72,7 @@ export const PreviewPanel = memo(function PreviewPanel({
|
||||
})
|
||||
|
||||
const REMARK_PLUGINS = [remarkGfm, remarkBreaks]
|
||||
const REHYPE_PLUGINS = [rehypeSlug]
|
||||
|
||||
/**
|
||||
* Carries the contentRef and toggle handler from MarkdownPreview down to the
|
||||
@@ -83,29 +86,43 @@ const MarkdownCheckboxCtx = createContext<{
|
||||
/** Carries the resolved checkbox index from LiRenderer to InputRenderer. */
|
||||
const CheckboxIndexCtx = createContext(-1)
|
||||
|
||||
const NavigateCtx = createContext<((path: string) => void) | null>(null)
|
||||
|
||||
const STATIC_MARKDOWN_COMPONENTS = {
|
||||
p: ({ children }: { children?: React.ReactNode }) => (
|
||||
<p className='mb-3 break-words text-[14px] text-[var(--text-primary)] leading-[1.6] last:mb-0'>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
h1: ({ children }: { children?: React.ReactNode }) => (
|
||||
<h1 className='mt-6 mb-4 break-words font-semibold text-[24px] text-[var(--text-primary)] first:mt-0'>
|
||||
h1: ({ id, children }: { id?: string; children?: React.ReactNode }) => (
|
||||
<h1
|
||||
id={id}
|
||||
className='mt-6 mb-4 break-words font-semibold text-[24px] text-[var(--text-primary)] first:mt-0'
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }: { children?: React.ReactNode }) => (
|
||||
<h2 className='mt-5 mb-3 break-words font-semibold text-[20px] text-[var(--text-primary)] first:mt-0'>
|
||||
h2: ({ id, children }: { id?: string; children?: React.ReactNode }) => (
|
||||
<h2
|
||||
id={id}
|
||||
className='mt-5 mb-3 break-words font-semibold text-[20px] text-[var(--text-primary)] first:mt-0'
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }: { children?: React.ReactNode }) => (
|
||||
<h3 className='mt-4 mb-2 break-words font-semibold text-[16px] text-[var(--text-primary)] first:mt-0'>
|
||||
h3: ({ id, children }: { id?: string; children?: React.ReactNode }) => (
|
||||
<h3
|
||||
id={id}
|
||||
className='mt-4 mb-2 break-words font-semibold text-[16px] text-[var(--text-primary)] first:mt-0'
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children }: { children?: React.ReactNode }) => (
|
||||
<h4 className='mt-3 mb-2 break-words font-semibold text-[14px] text-[var(--text-primary)] first:mt-0'>
|
||||
h4: ({ id, children }: { id?: string; children?: React.ReactNode }) => (
|
||||
<h4
|
||||
id={id}
|
||||
className='mt-3 mb-2 break-words font-semibold text-[14px] text-[var(--text-primary)] first:mt-0'
|
||||
>
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
@@ -138,16 +155,6 @@ const STATIC_MARKDOWN_COMPONENTS = {
|
||||
)
|
||||
},
|
||||
pre: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
|
||||
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => (
|
||||
<a
|
||||
href={href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='break-all text-[var(--brand-secondary)] underline-offset-2 hover:underline'
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
strong: ({ children }: { children?: React.ReactNode }) => (
|
||||
<strong className='break-words font-semibold text-[var(--text-primary)]'>{children}</strong>
|
||||
),
|
||||
@@ -267,8 +274,75 @@ function InputRenderer({
|
||||
)
|
||||
}
|
||||
|
||||
function isInternalHref(
|
||||
href: string,
|
||||
origin = window.location.origin
|
||||
): { pathname: string; hash: string } | null {
|
||||
if (href.startsWith('#')) return { pathname: '', hash: href }
|
||||
try {
|
||||
const url = new URL(href, origin)
|
||||
if (url.origin === origin && url.pathname.startsWith('/workspace/')) {
|
||||
return { pathname: url.pathname, hash: url.hash }
|
||||
}
|
||||
} catch {
|
||||
if (href.startsWith('/workspace/')) {
|
||||
const hashIdx = href.indexOf('#')
|
||||
if (hashIdx === -1) return { pathname: href, hash: '' }
|
||||
return { pathname: href.slice(0, hashIdx), hash: href.slice(hashIdx) }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function AnchorRenderer({ href, children }: { href?: string; children?: React.ReactNode }) {
|
||||
const navigate = useContext(NavigateCtx)
|
||||
const parsed = useMemo(() => (href ? isInternalHref(href) : null), [href])
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (!parsed || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (parsed.pathname === '' && parsed.hash) {
|
||||
const el = document.getElementById(parsed.hash.slice(1))
|
||||
if (el) {
|
||||
const container = el.closest('.overflow-auto') as HTMLElement | null
|
||||
if (container) {
|
||||
container.scrollTo({ top: el.offsetTop - container.offsetTop, behavior: 'smooth' })
|
||||
} else {
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const destination = parsed.pathname + parsed.hash
|
||||
if (navigate) {
|
||||
navigate(destination)
|
||||
} else {
|
||||
window.location.assign(destination)
|
||||
}
|
||||
},
|
||||
[parsed, navigate]
|
||||
)
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={parsed ? undefined : '_blank'}
|
||||
rel={parsed ? undefined : 'noopener noreferrer'}
|
||||
onClick={handleClick}
|
||||
className='break-all text-[var(--brand-secondary)] underline-offset-2 hover:underline'
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
const MARKDOWN_COMPONENTS = {
|
||||
...STATIC_MARKDOWN_COMPONENTS,
|
||||
a: AnchorRenderer,
|
||||
ul: UlRenderer,
|
||||
ol: OlRenderer,
|
||||
li: LiRenderer,
|
||||
@@ -284,6 +358,7 @@ const MarkdownPreview = memo(function MarkdownPreview({
|
||||
isStreaming?: boolean
|
||||
onCheckboxToggle?: (checkboxIndex: number, checked: boolean) => void
|
||||
}) {
|
||||
const { push: navigate } = useRouter()
|
||||
const { ref: scrollRef } = useAutoScroll(isStreaming)
|
||||
const { committed, incoming, generation } = useStreamingReveal(content, isStreaming)
|
||||
|
||||
@@ -295,10 +370,30 @@ const MarkdownPreview = memo(function MarkdownPreview({
|
||||
[onCheckboxToggle]
|
||||
)
|
||||
|
||||
const hasScrolledToHash = useRef(false)
|
||||
useEffect(() => {
|
||||
const hash = window.location.hash
|
||||
if (!hash || hasScrolledToHash.current) return
|
||||
const id = hash.slice(1)
|
||||
const el = document.getElementById(id)
|
||||
if (!el) return
|
||||
hasScrolledToHash.current = true
|
||||
const container = el.closest('.overflow-auto') as HTMLElement | null
|
||||
if (container) {
|
||||
container.scrollTo({ top: el.offsetTop - container.offsetTop, behavior: 'smooth' })
|
||||
} else {
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}, [content])
|
||||
|
||||
const committedMarkdown = useMemo(
|
||||
() =>
|
||||
committed ? (
|
||||
<ReactMarkdown remarkPlugins={REMARK_PLUGINS} components={MARKDOWN_COMPONENTS}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={REMARK_PLUGINS}
|
||||
rehypePlugins={REHYPE_PLUGINS}
|
||||
components={MARKDOWN_COMPONENTS}
|
||||
>
|
||||
{committed}
|
||||
</ReactMarkdown>
|
||||
) : null,
|
||||
@@ -307,30 +402,42 @@ const MarkdownPreview = memo(function MarkdownPreview({
|
||||
|
||||
if (onCheckboxToggle) {
|
||||
return (
|
||||
<MarkdownCheckboxCtx.Provider value={ctxValue}>
|
||||
<div ref={scrollRef} className='h-full overflow-auto p-6'>
|
||||
<ReactMarkdown remarkPlugins={REMARK_PLUGINS} components={MARKDOWN_COMPONENTS}>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</MarkdownCheckboxCtx.Provider>
|
||||
<NavigateCtx.Provider value={navigate}>
|
||||
<MarkdownCheckboxCtx.Provider value={ctxValue}>
|
||||
<div ref={scrollRef} className='h-full overflow-auto p-6'>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={REMARK_PLUGINS}
|
||||
rehypePlugins={REHYPE_PLUGINS}
|
||||
components={MARKDOWN_COMPONENTS}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</MarkdownCheckboxCtx.Provider>
|
||||
</NavigateCtx.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} className='h-full overflow-auto p-6'>
|
||||
{committedMarkdown}
|
||||
{incoming && (
|
||||
<div
|
||||
key={generation}
|
||||
className={cn(isStreaming && 'animate-stream-fade-in', '[&>:first-child]:mt-0')}
|
||||
>
|
||||
<ReactMarkdown remarkPlugins={REMARK_PLUGINS} components={MARKDOWN_COMPONENTS}>
|
||||
{incoming}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<NavigateCtx.Provider value={navigate}>
|
||||
<div ref={scrollRef} className='h-full overflow-auto p-6'>
|
||||
{committedMarkdown}
|
||||
{incoming && (
|
||||
<div
|
||||
key={generation}
|
||||
className={cn(isStreaming && 'animate-stream-fade-in', '[&>:first-child]:mt-0')}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={REMARK_PLUGINS}
|
||||
rehypePlugins={REHYPE_PLUGINS}
|
||||
components={MARKDOWN_COMPONENTS}
|
||||
>
|
||||
{incoming}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</NavigateCtx.Provider>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -473,9 +473,9 @@ function MothershipErrorDisplay({ data }: { data: MothershipErrorTagData }) {
|
||||
const detail = data.code ? `${data.message} (${data.code})` : data.message
|
||||
|
||||
return (
|
||||
<span className='animate-stream-fade-in font-base text-[13px] text-[var(--text-secondary)] italic leading-[20px]'>
|
||||
<p className='animate-stream-fade-in font-base text-[13px] text-[var(--text-secondary)] italic leading-[20px]'>
|
||||
{detail}
|
||||
</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ interface MothershipChatProps {
|
||||
onSendQueuedMessage: (id: string) => Promise<void>
|
||||
onEditQueuedMessage: (id: string) => void
|
||||
userId?: string
|
||||
chatId?: string
|
||||
onContextAdd?: (context: ChatContext) => void
|
||||
editValue?: string
|
||||
onEditValueConsumed?: () => void
|
||||
@@ -53,7 +54,7 @@ const LAYOUT_STYLES = {
|
||||
userRow: 'flex flex-col items-end gap-[6px] pt-3',
|
||||
attachmentWidth: 'max-w-[70%]',
|
||||
userBubble: 'max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2',
|
||||
assistantRow: 'group/msg relative pb-5',
|
||||
assistantRow: 'group/msg',
|
||||
footer: 'flex-shrink-0 px-[24px] pb-[16px]',
|
||||
footerInner: 'mx-auto max-w-[42rem]',
|
||||
},
|
||||
@@ -63,7 +64,7 @@ const LAYOUT_STYLES = {
|
||||
userRow: 'flex flex-col items-end gap-[6px] pt-2',
|
||||
attachmentWidth: 'max-w-[85%]',
|
||||
userBubble: 'max-w-[85%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3 py-2',
|
||||
assistantRow: 'group/msg relative pb-3',
|
||||
assistantRow: 'group/msg',
|
||||
footer: 'flex-shrink-0 px-3 pb-3',
|
||||
footerInner: '',
|
||||
},
|
||||
@@ -80,6 +81,7 @@ export function MothershipChat({
|
||||
onSendQueuedMessage,
|
||||
onEditQueuedMessage,
|
||||
userId,
|
||||
chatId,
|
||||
onContextAdd,
|
||||
editValue,
|
||||
onEditValueConsumed,
|
||||
@@ -147,20 +149,28 @@ export function MothershipChat({
|
||||
}
|
||||
|
||||
const isLastMessage = index === messages.length - 1
|
||||
const precedingUserMsg = [...messages]
|
||||
.slice(0, index)
|
||||
.reverse()
|
||||
.find((m) => m.role === 'user')
|
||||
|
||||
return (
|
||||
<div key={msg.id} className={styles.assistantRow}>
|
||||
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
|
||||
<div className='absolute right-0 bottom-0 z-10'>
|
||||
<MessageActions content={msg.content} requestId={msg.requestId} />
|
||||
</div>
|
||||
)}
|
||||
<MessageContent
|
||||
blocks={msg.contentBlocks || []}
|
||||
fallbackContent={msg.content}
|
||||
isStreaming={isThisStreaming}
|
||||
onOptionSelect={isLastMessage ? onSubmit : undefined}
|
||||
/>
|
||||
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
|
||||
<div className='mt-2.5'>
|
||||
<MessageActions
|
||||
content={msg.content}
|
||||
chatId={chatId}
|
||||
userQuery={precedingUserMsg?.content}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -115,7 +115,7 @@ export const MothershipView = memo(
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative z-10 flex h-full flex-col overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]',
|
||||
'relative z-10 flex h-full flex-col overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]',
|
||||
isCollapsed ? 'w-0 min-w-0 border-l-0' : 'w-1/2 border-l',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { PanelLeft } from '@/components/emcn/icons'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
@@ -28,6 +28,8 @@ interface HomeProps {
|
||||
export function Home({ chatId }: HomeProps = {}) {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const initialResourceId = searchParams.get('resource')
|
||||
const { data: session } = useSession()
|
||||
const posthog = usePostHog()
|
||||
const posthogRef = useRef(posthog)
|
||||
@@ -160,7 +162,10 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
} = useChat(
|
||||
workspaceId,
|
||||
chatId,
|
||||
getMothershipUseChatOptions({ onResourceEvent: handleResourceEvent })
|
||||
getMothershipUseChatOptions({
|
||||
onResourceEvent: handleResourceEvent,
|
||||
initialActiveResourceId: initialResourceId,
|
||||
})
|
||||
)
|
||||
|
||||
const [editingInputValue, setEditingInputValue] = useState('')
|
||||
@@ -183,6 +188,16 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
[editQueuedMessage]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const url = new URL(window.location.href)
|
||||
if (activeResourceId) {
|
||||
url.searchParams.set('resource', activeResourceId)
|
||||
} else {
|
||||
url.searchParams.delete('resource')
|
||||
}
|
||||
window.history.replaceState(null, '', url.toString())
|
||||
}, [activeResourceId])
|
||||
|
||||
useEffect(() => {
|
||||
wasSendingRef.current = false
|
||||
if (resolvedChatId) markRead(resolvedChatId)
|
||||
@@ -348,6 +363,7 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
onSendQueuedMessage={sendNow}
|
||||
onEditQueuedMessage={handleEditQueuedMessage}
|
||||
userId={session?.user?.id}
|
||||
chatId={resolvedChatId}
|
||||
onContextAdd={handleContextAdd}
|
||||
editValue={editingInputValue}
|
||||
onEditValueConsumed={clearEditingValue}
|
||||
|
||||
@@ -377,10 +377,11 @@ export interface UseChatOptions {
|
||||
onToolResult?: (toolName: string, success: boolean, result: unknown) => void
|
||||
onTitleUpdate?: () => void
|
||||
onStreamEnd?: (chatId: string, messages: ChatMessage[]) => void
|
||||
initialActiveResourceId?: string | null
|
||||
}
|
||||
|
||||
export function getMothershipUseChatOptions(
|
||||
options: Pick<UseChatOptions, 'onResourceEvent' | 'onStreamEnd'> = {}
|
||||
options: Pick<UseChatOptions, 'onResourceEvent' | 'onStreamEnd' | 'initialActiveResourceId'> = {}
|
||||
): UseChatOptions {
|
||||
return {
|
||||
apiPath: MOTHERSHIP_CHAT_API_PATH,
|
||||
@@ -416,6 +417,7 @@ export function useChat(
|
||||
const [resolvedChatId, setResolvedChatId] = useState<string | undefined>(initialChatId)
|
||||
const [resources, setResources] = useState<MothershipResource[]>([])
|
||||
const [activeResourceId, setActiveResourceId] = useState<string | null>(null)
|
||||
const initialActiveResourceIdRef = useRef(options?.initialActiveResourceId)
|
||||
const onResourceEventRef = useRef(options?.onResourceEvent)
|
||||
onResourceEventRef.current = options?.onResourceEvent
|
||||
const apiPathRef = useRef(options?.apiPath ?? MOTHERSHIP_CHAT_API_PATH)
|
||||
@@ -845,7 +847,12 @@ export function useChat(
|
||||
const persistedResources = history.resources.filter((r) => r.id !== 'streaming-file')
|
||||
if (persistedResources.length > 0) {
|
||||
setResources(persistedResources)
|
||||
setActiveResourceId(persistedResources[persistedResources.length - 1].id)
|
||||
const initialId = initialActiveResourceIdRef.current
|
||||
const restoredId =
|
||||
initialId && persistedResources.some((r) => r.id === initialId)
|
||||
? initialId
|
||||
: persistedResources[persistedResources.length - 1].id
|
||||
setActiveResourceId(restoredId)
|
||||
|
||||
for (const resource of persistedResources) {
|
||||
if (resource.type !== 'workflow') continue
|
||||
|
||||
@@ -839,6 +839,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
|
||||
onSendQueuedMessage={copilotSendNow}
|
||||
onEditQueuedMessage={handleCopilotEditQueuedMessage}
|
||||
userId={session?.user?.id}
|
||||
chatId={copilotResolvedChatId}
|
||||
editValue={copilotEditingInputValue}
|
||||
onEditValueConsumed={clearCopilotEditingValue}
|
||||
layout='copilot-view'
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Button,
|
||||
Combobox,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
@@ -432,7 +431,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
|
||||
<div ref={scrollContainerRef} className='min-h-0 flex-1 overflow-y-auto'>
|
||||
<div className='space-y-3'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Label htmlFor='type'>Request</Label>
|
||||
<p className='font-medium text-[var(--text-secondary)] text-sm'>Request</p>
|
||||
<Combobox
|
||||
id='type'
|
||||
options={REQUEST_TYPE_OPTIONS}
|
||||
@@ -447,7 +446,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Label htmlFor='subject'>Subject</Label>
|
||||
<p className='font-medium text-[var(--text-secondary)] text-sm'>Subject</p>
|
||||
<Input
|
||||
id='subject'
|
||||
placeholder='Brief description of your request'
|
||||
@@ -457,7 +456,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Label htmlFor='message'>Message</Label>
|
||||
<p className='font-medium text-[var(--text-secondary)] text-sm'>Message</p>
|
||||
<Textarea
|
||||
id='message'
|
||||
placeholder='Please provide details about your request...'
|
||||
@@ -468,7 +467,9 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Label>Attach Images (Optional)</Label>
|
||||
<p className='font-medium text-[var(--text-secondary)] text-sm'>
|
||||
Attach Images (Optional)
|
||||
</p>
|
||||
<Button
|
||||
type='button'
|
||||
variant='default'
|
||||
@@ -505,7 +506,9 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
|
||||
|
||||
{images.length > 0 && (
|
||||
<div className='space-y-2'>
|
||||
<Label>Uploaded Images</Label>
|
||||
<p className='font-medium text-[var(--text-secondary)] text-sm'>
|
||||
Uploaded Images
|
||||
</p>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
{images.map((image, index) => (
|
||||
<div
|
||||
|
||||
@@ -316,6 +316,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
const sidebarRef = useRef<HTMLElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const scrollContentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const posthog = usePostHog()
|
||||
const { data: sessionData, isPending: sessionLoading } = useSession()
|
||||
@@ -894,6 +895,9 @@ export const Sidebar = memo(function Sidebar() {
|
||||
container.addEventListener('scroll', updateScrollState, { passive: true })
|
||||
const observer = new ResizeObserver(updateScrollState)
|
||||
observer.observe(container)
|
||||
if (scrollContentRef.current) {
|
||||
observer.observe(scrollContentRef.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('scroll', updateScrollState)
|
||||
@@ -1336,275 +1340,286 @@ export const Sidebar = memo(function Sidebar() {
|
||||
!hasOverflowTop && 'border-transparent'
|
||||
)}
|
||||
>
|
||||
<div className='tasks-section flex flex-shrink-0 flex-col' data-tour='nav-tasks'>
|
||||
<div className='flex h-[18px] flex-shrink-0 items-center justify-between px-4'>
|
||||
<div className='font-base text-[var(--text-icon)] text-small'>All tasks</div>
|
||||
{!isCollapsed && (
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='h-[18px] w-[18px] rounded-sm p-0 hover-hover:bg-[var(--surface-hover)]'
|
||||
onClick={handleNewTask}
|
||||
>
|
||||
<Plus className='h-[16px] w-[16px]' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<Tooltip.Shortcut keys={isMac ? '⌘⇧K' : 'Ctrl+Shift+K'}>
|
||||
New task
|
||||
</Tooltip.Shortcut>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<div ref={scrollContentRef} className='flex flex-col'>
|
||||
<div
|
||||
className='tasks-section flex flex-shrink-0 flex-col'
|
||||
data-tour='nav-tasks'
|
||||
>
|
||||
<div className='flex h-[18px] flex-shrink-0 items-center justify-between px-4'>
|
||||
<div className='font-base text-[var(--text-icon)] text-small'>
|
||||
All tasks
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<CollapsedSidebarMenu
|
||||
icon={tasksCollapsedIcon}
|
||||
hover={tasksHover}
|
||||
ariaLabel='Tasks'
|
||||
className='mt-1.5'
|
||||
primaryAction={tasksPrimaryAction}
|
||||
>
|
||||
{tasksLoading ? (
|
||||
<DropdownMenuItem disabled>
|
||||
<Loader className='h-[14px] w-[14px]' animate />
|
||||
Loading...
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
tasks.map((task) => (
|
||||
<CollapsedTaskFlyoutItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
isCurrentRoute={task.id !== 'new' && pathname === task.href}
|
||||
isMenuOpen={menuOpenTaskId === task.id}
|
||||
isEditing={task.id === taskFlyoutRename.editingId}
|
||||
editValue={taskFlyoutRename.value}
|
||||
inputRef={taskFlyoutRename.inputRef}
|
||||
isRenaming={taskFlyoutRename.isSaving}
|
||||
onEditValueChange={taskFlyoutRename.setValue}
|
||||
onEditKeyDown={taskFlyoutRename.handleKeyDown}
|
||||
onEditBlur={handleTaskRenameBlur}
|
||||
onContextMenu={handleTaskContextMenu}
|
||||
onMorePointerDown={handleTaskMorePointerDown}
|
||||
onMoreClick={handleTaskMoreClick}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</CollapsedSidebarMenu>
|
||||
) : (
|
||||
<div className='mt-1.5 flex flex-col gap-0.5 px-2'>
|
||||
{tasksLoading ? (
|
||||
<SidebarItemSkeleton />
|
||||
) : (
|
||||
<>
|
||||
{tasks.slice(0, visibleTaskCount).map((task) => {
|
||||
const isCurrentRoute = task.id !== 'new' && pathname === task.href
|
||||
const isRenaming = taskFlyoutRename.editingId === task.id
|
||||
const isSelected = task.id !== 'new' && selectedTasks.has(task.id)
|
||||
|
||||
if (isRenaming) {
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
className='mx-0.5 flex h-[30px] items-center gap-2 rounded-lg bg-[var(--surface-active)] px-2 text-sm'
|
||||
>
|
||||
<Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
<input
|
||||
ref={taskFlyoutRename.inputRef}
|
||||
value={taskFlyoutRename.value}
|
||||
onChange={(e) => taskFlyoutRename.setValue(e.target.value)}
|
||||
onKeyDown={taskFlyoutRename.handleKeyDown}
|
||||
onBlur={handleTaskRenameBlur}
|
||||
className='min-w-0 flex-1 border-none bg-transparent font-base text-[14px] text-[var(--text-body)] outline-none'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarTaskItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
isCurrentRoute={isCurrentRoute}
|
||||
isSelected={isSelected}
|
||||
isActive={!!task.isActive}
|
||||
isUnread={!!task.isUnread}
|
||||
isMenuOpen={menuOpenTaskId === task.id}
|
||||
showCollapsedTooltips={showCollapsedTooltips}
|
||||
onMultiSelectClick={handleTaskClick}
|
||||
onContextMenu={handleTaskContextMenu}
|
||||
onMorePointerDown={handleTaskMorePointerDown}
|
||||
onMoreClick={handleTaskMoreClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{tasks.length > visibleTaskCount && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSeeMoreTasks}
|
||||
className='mx-0.5 flex h-[30px] items-center gap-2 rounded-lg px-2 text-[var(--text-icon)] text-sm hover-hover:bg-[var(--surface-hover)]'
|
||||
>
|
||||
<MoreHorizontal className='h-[16px] w-[16px] flex-shrink-0' />
|
||||
<span className='font-base'>See more</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='workflows-section relative mt-3.5 flex flex-col'
|
||||
data-tour='nav-workflows'
|
||||
>
|
||||
<div className='flex h-[18px] flex-shrink-0 items-center justify-between px-4'>
|
||||
<div className='font-base text-[var(--text-icon)] text-small'>Workflows</div>
|
||||
{!isCollapsed && (
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<DropdownMenu>
|
||||
{!isCollapsed && (
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='h-[18px] w-[18px] rounded-sm p-0 hover-hover:bg-[var(--surface-hover)]'
|
||||
disabled={!canEdit}
|
||||
>
|
||||
{isImporting || isCreatingFolder ? (
|
||||
<Loader className='h-[16px] w-[16px]' animate />
|
||||
) : (
|
||||
<MoreHorizontal className='h-[16px] w-[16px]' />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='h-[18px] w-[18px] rounded-sm p-0 hover-hover:bg-[var(--surface-hover)]'
|
||||
onClick={handleNewTask}
|
||||
>
|
||||
<Plus className='h-[16px] w-[16px]' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>More actions</p>
|
||||
<Tooltip.Shortcut keys={isMac ? '⌘⇧K' : 'Ctrl+Shift+K'}>
|
||||
New task
|
||||
</Tooltip.Shortcut>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<DropdownMenuContent
|
||||
align='start'
|
||||
sideOffset={8}
|
||||
className='min-w-[160px]'
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={handleImportWorkflow}
|
||||
disabled={!canEdit || isImporting}
|
||||
>
|
||||
<Download />
|
||||
{isImporting ? 'Importing...' : 'Import workflow'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={handleCreateFolder}
|
||||
disabled={!canEdit || isCreatingFolder}
|
||||
>
|
||||
<FolderPlus />
|
||||
{isCreatingFolder ? 'Creating folder...' : 'Create folder'}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='h-[18px] w-[18px] rounded-sm p-0 hover-hover:bg-[var(--surface-hover)]'
|
||||
onClick={handleCreateWorkflow}
|
||||
disabled={isCreatingWorkflow || !canEdit}
|
||||
>
|
||||
<Plus className='h-[16px] w-[16px]' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
{isCreatingWorkflow ? (
|
||||
<p>Creating workflow...</p>
|
||||
) : (
|
||||
<Tooltip.Shortcut keys={isMac ? '⌘⇧P' : 'Ctrl+Shift+P'}>
|
||||
New workflow
|
||||
</Tooltip.Shortcut>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<CollapsedSidebarMenu
|
||||
icon={tasksCollapsedIcon}
|
||||
hover={tasksHover}
|
||||
ariaLabel='Tasks'
|
||||
className='mt-1.5'
|
||||
primaryAction={tasksPrimaryAction}
|
||||
>
|
||||
{tasksLoading ? (
|
||||
<DropdownMenuItem disabled>
|
||||
<Loader className='h-[14px] w-[14px]' animate />
|
||||
Loading...
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
tasks.map((task) => (
|
||||
<CollapsedTaskFlyoutItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
isCurrentRoute={task.id !== 'new' && pathname === task.href}
|
||||
isMenuOpen={menuOpenTaskId === task.id}
|
||||
isEditing={task.id === taskFlyoutRename.editingId}
|
||||
editValue={taskFlyoutRename.value}
|
||||
inputRef={taskFlyoutRename.inputRef}
|
||||
isRenaming={taskFlyoutRename.isSaving}
|
||||
onEditValueChange={taskFlyoutRename.setValue}
|
||||
onEditKeyDown={taskFlyoutRename.handleKeyDown}
|
||||
onEditBlur={handleTaskRenameBlur}
|
||||
onContextMenu={handleTaskContextMenu}
|
||||
onMorePointerDown={handleTaskMorePointerDown}
|
||||
onMoreClick={handleTaskMoreClick}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</CollapsedSidebarMenu>
|
||||
) : (
|
||||
<div className='mt-1.5 flex flex-col gap-0.5 px-2'>
|
||||
{tasksLoading ? (
|
||||
<SidebarItemSkeleton />
|
||||
) : (
|
||||
<>
|
||||
{tasks.slice(0, visibleTaskCount).map((task) => {
|
||||
const isCurrentRoute = task.id !== 'new' && pathname === task.href
|
||||
const isRenaming = taskFlyoutRename.editingId === task.id
|
||||
const isSelected = task.id !== 'new' && selectedTasks.has(task.id)
|
||||
|
||||
if (isRenaming) {
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
className='mx-0.5 flex h-[30px] items-center gap-2 rounded-lg bg-[var(--surface-active)] px-2 text-sm'
|
||||
>
|
||||
<Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
<input
|
||||
ref={taskFlyoutRename.inputRef}
|
||||
value={taskFlyoutRename.value}
|
||||
onChange={(e) => taskFlyoutRename.setValue(e.target.value)}
|
||||
onKeyDown={taskFlyoutRename.handleKeyDown}
|
||||
onBlur={handleTaskRenameBlur}
|
||||
className='min-w-0 flex-1 border-none bg-transparent font-base text-[14px] text-[var(--text-body)] outline-none'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarTaskItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
isCurrentRoute={isCurrentRoute}
|
||||
isSelected={isSelected}
|
||||
isActive={!!task.isActive}
|
||||
isUnread={!!task.isUnread}
|
||||
isMenuOpen={menuOpenTaskId === task.id}
|
||||
showCollapsedTooltips={showCollapsedTooltips}
|
||||
onMultiSelectClick={handleTaskClick}
|
||||
onContextMenu={handleTaskContextMenu}
|
||||
onMorePointerDown={handleTaskMorePointerDown}
|
||||
onMoreClick={handleTaskMoreClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{tasks.length > visibleTaskCount && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSeeMoreTasks}
|
||||
className='mx-0.5 flex h-[30px] items-center gap-2 rounded-lg px-2 text-[var(--text-icon)] text-sm hover-hover:bg-[var(--surface-hover)]'
|
||||
>
|
||||
<MoreHorizontal className='h-[16px] w-[16px] flex-shrink-0' />
|
||||
<span className='font-base'>See more</span>
|
||||
</button>
|
||||
)}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<CollapsedSidebarMenu
|
||||
icon={workflowsCollapsedIcon}
|
||||
hover={workflowsHover}
|
||||
ariaLabel='Workflows'
|
||||
className='mt-1.5'
|
||||
primaryAction={workflowsPrimaryAction}
|
||||
>
|
||||
{workflowsLoading && regularWorkflows.length === 0 ? (
|
||||
<DropdownMenuItem disabled>
|
||||
<Loader className='h-[14px] w-[14px]' animate />
|
||||
Loading...
|
||||
</DropdownMenuItem>
|
||||
) : regularWorkflows.length === 0 ? (
|
||||
<DropdownMenuItem disabled>No workflows yet</DropdownMenuItem>
|
||||
) : (
|
||||
<>
|
||||
<CollapsedFolderItems
|
||||
nodes={folderTree}
|
||||
workflowsByFolder={workflowsByFolder}
|
||||
workspaceId={workspaceId}
|
||||
currentWorkflowId={workflowId}
|
||||
editingWorkflowId={workflowFlyoutRename.editingId}
|
||||
editingValue={workflowFlyoutRename.value}
|
||||
editInputRef={workflowFlyoutRename.inputRef}
|
||||
isRenamingWorkflow={workflowFlyoutRename.isSaving}
|
||||
onEditValueChange={workflowFlyoutRename.setValue}
|
||||
onEditKeyDown={workflowFlyoutRename.handleKeyDown}
|
||||
onEditBlur={handleWorkflowRenameBlur}
|
||||
onWorkflowOpenInNewTab={handleCollapsedWorkflowOpenInNewTab}
|
||||
onWorkflowRename={handleCollapsedWorkflowRename}
|
||||
canRenameWorkflow={canEdit}
|
||||
/>
|
||||
{(workflowsByFolder.root || []).map((workflow) => (
|
||||
<CollapsedWorkflowFlyoutItem
|
||||
key={workflow.id}
|
||||
workflow={workflow}
|
||||
href={`/workspace/${workspaceId}/w/${workflow.id}`}
|
||||
isCurrentRoute={workflow.id === workflowId}
|
||||
isEditing={workflow.id === workflowFlyoutRename.editingId}
|
||||
editValue={workflowFlyoutRename.value}
|
||||
inputRef={workflowFlyoutRename.inputRef}
|
||||
isRenaming={workflowFlyoutRename.isSaving}
|
||||
|
||||
<div
|
||||
className='workflows-section relative mt-3.5 flex flex-col'
|
||||
data-tour='nav-workflows'
|
||||
>
|
||||
<div className='flex h-[18px] flex-shrink-0 items-center justify-between px-4'>
|
||||
<div className='font-base text-[var(--text-icon)] text-small'>
|
||||
Workflows
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<DropdownMenu>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='h-[18px] w-[18px] rounded-sm p-0 hover-hover:bg-[var(--surface-hover)]'
|
||||
disabled={!canEdit}
|
||||
>
|
||||
{isImporting || isCreatingFolder ? (
|
||||
<Loader className='h-[16px] w-[16px]' animate />
|
||||
) : (
|
||||
<MoreHorizontal className='h-[16px] w-[16px]' />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>More actions</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<DropdownMenuContent
|
||||
align='start'
|
||||
sideOffset={8}
|
||||
className='min-w-[160px]'
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={handleImportWorkflow}
|
||||
disabled={!canEdit || isImporting}
|
||||
>
|
||||
<Download />
|
||||
{isImporting ? 'Importing...' : 'Import workflow'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={handleCreateFolder}
|
||||
disabled={!canEdit || isCreatingFolder}
|
||||
>
|
||||
<FolderPlus />
|
||||
{isCreatingFolder ? 'Creating folder...' : 'Create folder'}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='h-[18px] w-[18px] rounded-sm p-0 hover-hover:bg-[var(--surface-hover)]'
|
||||
onClick={handleCreateWorkflow}
|
||||
disabled={isCreatingWorkflow || !canEdit}
|
||||
>
|
||||
<Plus className='h-[16px] w-[16px]' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
{isCreatingWorkflow ? (
|
||||
<p>Creating workflow...</p>
|
||||
) : (
|
||||
<Tooltip.Shortcut keys={isMac ? '⌘⇧P' : 'Ctrl+Shift+P'}>
|
||||
New workflow
|
||||
</Tooltip.Shortcut>
|
||||
)}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<CollapsedSidebarMenu
|
||||
icon={workflowsCollapsedIcon}
|
||||
hover={workflowsHover}
|
||||
ariaLabel='Workflows'
|
||||
className='mt-1.5'
|
||||
primaryAction={workflowsPrimaryAction}
|
||||
>
|
||||
{workflowsLoading && regularWorkflows.length === 0 ? (
|
||||
<DropdownMenuItem disabled>
|
||||
<Loader className='h-[14px] w-[14px]' animate />
|
||||
Loading...
|
||||
</DropdownMenuItem>
|
||||
) : regularWorkflows.length === 0 ? (
|
||||
<DropdownMenuItem disabled>No workflows yet</DropdownMenuItem>
|
||||
) : (
|
||||
<>
|
||||
<CollapsedFolderItems
|
||||
nodes={folderTree}
|
||||
workflowsByFolder={workflowsByFolder}
|
||||
workspaceId={workspaceId}
|
||||
currentWorkflowId={workflowId}
|
||||
editingWorkflowId={workflowFlyoutRename.editingId}
|
||||
editingValue={workflowFlyoutRename.value}
|
||||
editInputRef={workflowFlyoutRename.inputRef}
|
||||
isRenamingWorkflow={workflowFlyoutRename.isSaving}
|
||||
onEditValueChange={workflowFlyoutRename.setValue}
|
||||
onEditKeyDown={workflowFlyoutRename.handleKeyDown}
|
||||
onEditBlur={handleWorkflowRenameBlur}
|
||||
onOpenInNewTab={() => handleCollapsedWorkflowOpenInNewTab(workflow)}
|
||||
onRename={() => handleCollapsedWorkflowRename(workflow)}
|
||||
canRename={canEdit}
|
||||
onWorkflowOpenInNewTab={handleCollapsedWorkflowOpenInNewTab}
|
||||
onWorkflowRename={handleCollapsedWorkflowRename}
|
||||
canRenameWorkflow={canEdit}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</CollapsedSidebarMenu>
|
||||
) : (
|
||||
<div className='mt-1.5 px-2'>
|
||||
{workflowsLoading && regularWorkflows.length === 0 && (
|
||||
<SidebarItemSkeleton />
|
||||
)}
|
||||
<WorkflowList
|
||||
workspaceId={workspaceId}
|
||||
workflowId={workflowId}
|
||||
regularWorkflows={regularWorkflows}
|
||||
isLoading={isLoading}
|
||||
canReorder={canEdit}
|
||||
handleFileChange={handleImportFileChange}
|
||||
fileInputRef={fileInputRef}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
onCreateWorkflow={handleCreateWorkflow}
|
||||
onCreateFolder={handleCreateFolder}
|
||||
disableCreate={!canEdit || isCreatingWorkflow || isCreatingFolder}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(workflowsByFolder.root || []).map((workflow) => (
|
||||
<CollapsedWorkflowFlyoutItem
|
||||
key={workflow.id}
|
||||
workflow={workflow}
|
||||
href={`/workspace/${workspaceId}/w/${workflow.id}`}
|
||||
isCurrentRoute={workflow.id === workflowId}
|
||||
isEditing={workflow.id === workflowFlyoutRename.editingId}
|
||||
editValue={workflowFlyoutRename.value}
|
||||
inputRef={workflowFlyoutRename.inputRef}
|
||||
isRenaming={workflowFlyoutRename.isSaving}
|
||||
onEditValueChange={workflowFlyoutRename.setValue}
|
||||
onEditKeyDown={workflowFlyoutRename.handleKeyDown}
|
||||
onEditBlur={handleWorkflowRenameBlur}
|
||||
onOpenInNewTab={() =>
|
||||
handleCollapsedWorkflowOpenInNewTab(workflow)
|
||||
}
|
||||
onRename={() => handleCollapsedWorkflowRename(workflow)}
|
||||
canRename={canEdit}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</CollapsedSidebarMenu>
|
||||
) : (
|
||||
<div className='mt-1.5 px-2'>
|
||||
{workflowsLoading && regularWorkflows.length === 0 && (
|
||||
<SidebarItemSkeleton />
|
||||
)}
|
||||
<WorkflowList
|
||||
workspaceId={workspaceId}
|
||||
workflowId={workflowId}
|
||||
regularWorkflows={regularWorkflows}
|
||||
isLoading={isLoading}
|
||||
canReorder={canEdit}
|
||||
handleFileChange={handleImportFileChange}
|
||||
fileInputRef={fileInputRef}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
onCreateWorkflow={handleCreateWorkflow}
|
||||
onCreateFolder={handleCreateFolder}
|
||||
disableCreate={!canEdit || isCreatingWorkflow || isCreatingFolder}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
571
apps/sim/blocks/blocks/cloudwatch.ts
Normal file
571
apps/sim/blocks/blocks/cloudwatch.ts
Normal file
@@ -0,0 +1,571 @@
|
||||
import { CloudWatchIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import type {
|
||||
CloudWatchDescribeAlarmsResponse,
|
||||
CloudWatchDescribeLogGroupsResponse,
|
||||
CloudWatchDescribeLogStreamsResponse,
|
||||
CloudWatchGetLogEventsResponse,
|
||||
CloudWatchGetMetricStatisticsResponse,
|
||||
CloudWatchListMetricsResponse,
|
||||
CloudWatchQueryLogsResponse,
|
||||
} from '@/tools/cloudwatch/types'
|
||||
|
||||
export const CloudWatchBlock: BlockConfig<
|
||||
| CloudWatchQueryLogsResponse
|
||||
| CloudWatchDescribeLogGroupsResponse
|
||||
| CloudWatchDescribeLogStreamsResponse
|
||||
| CloudWatchGetLogEventsResponse
|
||||
| CloudWatchDescribeAlarmsResponse
|
||||
| CloudWatchListMetricsResponse
|
||||
| CloudWatchGetMetricStatisticsResponse
|
||||
> = {
|
||||
type: 'cloudwatch',
|
||||
name: 'CloudWatch',
|
||||
description: 'Query and monitor AWS CloudWatch logs, metrics, and alarms',
|
||||
longDescription:
|
||||
'Integrate AWS CloudWatch into workflows. Run Log Insights queries, list log groups, retrieve log events, list and get metrics, and monitor alarms. Requires AWS access key and secret access key.',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Analytics,
|
||||
tags: ['cloud', 'monitoring'],
|
||||
bgColor: 'linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)',
|
||||
icon: CloudWatchIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Query Logs (Insights)', id: 'query_logs' },
|
||||
{ label: 'Describe Log Groups', id: 'describe_log_groups' },
|
||||
{ label: 'Get Log Events', id: 'get_log_events' },
|
||||
{ label: 'Describe Log Streams', id: 'describe_log_streams' },
|
||||
{ label: 'List Metrics', id: 'list_metrics' },
|
||||
{ label: 'Get Metric Statistics', id: 'get_metric_statistics' },
|
||||
{ label: 'Describe Alarms', id: 'describe_alarms' },
|
||||
],
|
||||
value: () => 'query_logs',
|
||||
},
|
||||
{
|
||||
id: 'awsRegion',
|
||||
title: 'AWS Region',
|
||||
type: 'short-input',
|
||||
placeholder: 'us-east-1',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'awsAccessKeyId',
|
||||
title: 'AWS Access Key ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'AKIA...',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'awsSecretAccessKey',
|
||||
title: 'AWS Secret Access Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Your secret access key',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
// Query Logs fields
|
||||
{
|
||||
id: 'logGroupSelector',
|
||||
title: 'Log Group',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'logGroupNames',
|
||||
selectorKey: 'cloudwatch.logGroups',
|
||||
dependsOn: ['awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion'],
|
||||
placeholder: 'Select a log group',
|
||||
condition: { field: 'operation', value: 'query_logs' },
|
||||
required: { field: 'operation', value: 'query_logs' },
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'logGroupNamesInput',
|
||||
title: 'Log Group Names',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'logGroupNames',
|
||||
placeholder: '/aws/lambda/my-func, /aws/ecs/my-service',
|
||||
condition: { field: 'operation', value: 'query_logs' },
|
||||
required: { field: 'operation', value: 'query_logs' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'queryString',
|
||||
title: 'Query',
|
||||
type: 'code',
|
||||
placeholder: 'fields @timestamp, @message\n| sort @timestamp desc\n| limit 20',
|
||||
condition: { field: 'operation', value: 'query_logs' },
|
||||
required: { field: 'operation', value: 'query_logs' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a CloudWatch Log Insights query based on the user's description.
|
||||
The query language supports: fields, filter, stats, sort, limit, parse, display.
|
||||
Common patterns:
|
||||
- fields @timestamp, @message | sort @timestamp desc | limit 20
|
||||
- filter @message like /ERROR/ | stats count(*) by bin(1h)
|
||||
- stats avg(duration) as avgDuration by functionName | sort avgDuration desc
|
||||
- filter @message like /Exception/ | parse @message "* Exception: *" as prefix, errorMsg
|
||||
- stats count(*) as requestCount by status | sort requestCount desc
|
||||
|
||||
Return ONLY the query — no explanations, no markdown code blocks.`,
|
||||
placeholder: 'Describe what you want to find in the logs...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'startTime',
|
||||
title: 'Start Time (Unix epoch seconds)',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., 1711900800',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['query_logs', 'get_log_events', 'get_metric_statistics'],
|
||||
},
|
||||
required: { field: 'operation', value: ['query_logs', 'get_metric_statistics'] },
|
||||
},
|
||||
{
|
||||
id: 'endTime',
|
||||
title: 'End Time (Unix epoch seconds)',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., 1711987200',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['query_logs', 'get_log_events', 'get_metric_statistics'],
|
||||
},
|
||||
required: { field: 'operation', value: ['query_logs', 'get_metric_statistics'] },
|
||||
},
|
||||
// Describe Log Groups fields
|
||||
{
|
||||
id: 'prefix',
|
||||
title: 'Log Group Name Prefix',
|
||||
type: 'short-input',
|
||||
placeholder: '/aws/lambda/',
|
||||
condition: { field: 'operation', value: 'describe_log_groups' },
|
||||
},
|
||||
// Get Log Events / Describe Log Streams — shared log group selector
|
||||
{
|
||||
id: 'logGroupNameSelector',
|
||||
title: 'Log Group',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'logGroupName',
|
||||
selectorKey: 'cloudwatch.logGroups',
|
||||
dependsOn: ['awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion'],
|
||||
placeholder: 'Select a log group',
|
||||
condition: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] },
|
||||
required: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] },
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'logGroupNameInput',
|
||||
title: 'Log Group Name',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'logGroupName',
|
||||
placeholder: '/aws/lambda/my-func',
|
||||
condition: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] },
|
||||
required: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Describe Log Streams — stream prefix filter
|
||||
{
|
||||
id: 'streamPrefix',
|
||||
title: 'Stream Name Prefix',
|
||||
type: 'short-input',
|
||||
placeholder: '2024/03/31/',
|
||||
condition: { field: 'operation', value: 'describe_log_streams' },
|
||||
},
|
||||
// Get Log Events — log stream selector (cascading: depends on log group)
|
||||
{
|
||||
id: 'logStreamNameSelector',
|
||||
title: 'Log Stream',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'logStreamName',
|
||||
selectorKey: 'cloudwatch.logStreams',
|
||||
dependsOn: ['awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion', 'logGroupNameSelector'],
|
||||
placeholder: 'Select a log stream',
|
||||
condition: { field: 'operation', value: 'get_log_events' },
|
||||
required: { field: 'operation', value: 'get_log_events' },
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'logStreamNameInput',
|
||||
title: 'Log Stream Name',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'logStreamName',
|
||||
placeholder: '2024/03/31/[$LATEST]abc123',
|
||||
condition: { field: 'operation', value: 'get_log_events' },
|
||||
required: { field: 'operation', value: 'get_log_events' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// List Metrics fields
|
||||
{
|
||||
id: 'metricNamespace',
|
||||
title: 'Namespace',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., AWS/EC2, AWS/Lambda, AWS/RDS',
|
||||
condition: { field: 'operation', value: ['list_metrics', 'get_metric_statistics'] },
|
||||
required: { field: 'operation', value: 'get_metric_statistics' },
|
||||
},
|
||||
{
|
||||
id: 'metricName',
|
||||
title: 'Metric Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., CPUUtilization, Invocations',
|
||||
condition: { field: 'operation', value: ['list_metrics', 'get_metric_statistics'] },
|
||||
required: { field: 'operation', value: 'get_metric_statistics' },
|
||||
},
|
||||
{
|
||||
id: 'recentlyActive',
|
||||
title: 'Recently Active Only',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'list_metrics' },
|
||||
},
|
||||
// Get Metric Statistics fields
|
||||
{
|
||||
id: 'metricPeriod',
|
||||
title: 'Period (seconds)',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., 60, 300, 3600',
|
||||
condition: { field: 'operation', value: 'get_metric_statistics' },
|
||||
required: { field: 'operation', value: 'get_metric_statistics' },
|
||||
},
|
||||
{
|
||||
id: 'metricStatistics',
|
||||
title: 'Statistics',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Average', id: 'Average' },
|
||||
{ label: 'Sum', id: 'Sum' },
|
||||
{ label: 'Minimum', id: 'Minimum' },
|
||||
{ label: 'Maximum', id: 'Maximum' },
|
||||
{ label: 'Sample Count', id: 'SampleCount' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'get_metric_statistics' },
|
||||
required: { field: 'operation', value: 'get_metric_statistics' },
|
||||
},
|
||||
{
|
||||
id: 'metricDimensions',
|
||||
title: 'Dimensions',
|
||||
type: 'table',
|
||||
columns: ['name', 'value'],
|
||||
condition: { field: 'operation', value: 'get_metric_statistics' },
|
||||
},
|
||||
// Describe Alarms fields
|
||||
{
|
||||
id: 'alarmNamePrefix',
|
||||
title: 'Alarm Name Prefix',
|
||||
type: 'short-input',
|
||||
placeholder: 'my-service-',
|
||||
condition: { field: 'operation', value: 'describe_alarms' },
|
||||
},
|
||||
{
|
||||
id: 'stateValue',
|
||||
title: 'State',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'All States', id: '' },
|
||||
{ label: 'OK', id: 'OK' },
|
||||
{ label: 'ALARM', id: 'ALARM' },
|
||||
{ label: 'INSUFFICIENT_DATA', id: 'INSUFFICIENT_DATA' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'describe_alarms' },
|
||||
},
|
||||
{
|
||||
id: 'alarmType',
|
||||
title: 'Alarm Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'All Types', id: '' },
|
||||
{ label: 'Metric Alarm', id: 'MetricAlarm' },
|
||||
{ label: 'Composite Alarm', id: 'CompositeAlarm' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'describe_alarms' },
|
||||
},
|
||||
// Shared limit field
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: '100',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'query_logs',
|
||||
'describe_log_groups',
|
||||
'get_log_events',
|
||||
'describe_log_streams',
|
||||
'list_metrics',
|
||||
'describe_alarms',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'cloudwatch_query_logs',
|
||||
'cloudwatch_describe_log_groups',
|
||||
'cloudwatch_get_log_events',
|
||||
'cloudwatch_describe_log_streams',
|
||||
'cloudwatch_list_metrics',
|
||||
'cloudwatch_get_metric_statistics',
|
||||
'cloudwatch_describe_alarms',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'query_logs':
|
||||
return 'cloudwatch_query_logs'
|
||||
case 'describe_log_groups':
|
||||
return 'cloudwatch_describe_log_groups'
|
||||
case 'get_log_events':
|
||||
return 'cloudwatch_get_log_events'
|
||||
case 'describe_log_streams':
|
||||
return 'cloudwatch_describe_log_streams'
|
||||
case 'list_metrics':
|
||||
return 'cloudwatch_list_metrics'
|
||||
case 'get_metric_statistics':
|
||||
return 'cloudwatch_get_metric_statistics'
|
||||
case 'describe_alarms':
|
||||
return 'cloudwatch_describe_alarms'
|
||||
default:
|
||||
throw new Error(`Invalid CloudWatch operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, startTime, endTime, limit, ...rest } = params
|
||||
|
||||
const awsRegion = rest.awsRegion
|
||||
const awsAccessKeyId = rest.awsAccessKeyId
|
||||
const awsSecretAccessKey = rest.awsSecretAccessKey
|
||||
const parsedLimit = limit ? Number.parseInt(String(limit), 10) : undefined
|
||||
|
||||
switch (operation) {
|
||||
case 'query_logs': {
|
||||
const logGroupNames = rest.logGroupNames
|
||||
if (!logGroupNames) {
|
||||
throw new Error('Log group names are required')
|
||||
}
|
||||
if (!startTime) {
|
||||
throw new Error('Start time is required')
|
||||
}
|
||||
if (!endTime) {
|
||||
throw new Error('End time is required')
|
||||
}
|
||||
|
||||
const groupNames =
|
||||
typeof logGroupNames === 'string'
|
||||
? logGroupNames
|
||||
.split(',')
|
||||
.map((n: string) => n.trim())
|
||||
.filter(Boolean)
|
||||
: logGroupNames
|
||||
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
logGroupNames: groupNames,
|
||||
queryString: rest.queryString,
|
||||
startTime: Number(startTime),
|
||||
endTime: Number(endTime),
|
||||
...(parsedLimit !== undefined && { limit: parsedLimit }),
|
||||
}
|
||||
}
|
||||
|
||||
case 'describe_log_groups':
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
...(rest.prefix && { prefix: rest.prefix }),
|
||||
...(parsedLimit !== undefined && { limit: parsedLimit }),
|
||||
}
|
||||
|
||||
case 'get_log_events': {
|
||||
if (!rest.logGroupName) {
|
||||
throw new Error('Log group name is required')
|
||||
}
|
||||
if (!rest.logStreamName) {
|
||||
throw new Error('Log stream name is required')
|
||||
}
|
||||
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
logGroupName: rest.logGroupName,
|
||||
logStreamName: rest.logStreamName,
|
||||
...(startTime && { startTime: Number(startTime) }),
|
||||
...(endTime && { endTime: Number(endTime) }),
|
||||
...(parsedLimit !== undefined && { limit: parsedLimit }),
|
||||
}
|
||||
}
|
||||
|
||||
case 'describe_log_streams': {
|
||||
if (!rest.logGroupName) {
|
||||
throw new Error('Log group name is required')
|
||||
}
|
||||
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
logGroupName: rest.logGroupName,
|
||||
...(rest.streamPrefix && { prefix: rest.streamPrefix }),
|
||||
...(parsedLimit !== undefined && { limit: parsedLimit }),
|
||||
}
|
||||
}
|
||||
|
||||
case 'list_metrics':
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
...(rest.metricNamespace && { namespace: rest.metricNamespace }),
|
||||
...(rest.metricName && { metricName: rest.metricName }),
|
||||
...(rest.recentlyActive && { recentlyActive: true }),
|
||||
...(parsedLimit !== undefined && { limit: parsedLimit }),
|
||||
}
|
||||
|
||||
case 'get_metric_statistics': {
|
||||
if (!rest.metricNamespace) {
|
||||
throw new Error('Namespace is required')
|
||||
}
|
||||
if (!rest.metricName) {
|
||||
throw new Error('Metric name is required')
|
||||
}
|
||||
if (!startTime) {
|
||||
throw new Error('Start time is required')
|
||||
}
|
||||
if (!endTime) {
|
||||
throw new Error('End time is required')
|
||||
}
|
||||
if (!rest.metricPeriod) {
|
||||
throw new Error('Period is required')
|
||||
}
|
||||
|
||||
const stat = rest.metricStatistics
|
||||
if (!stat) {
|
||||
throw new Error('Statistics is required')
|
||||
}
|
||||
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
namespace: rest.metricNamespace,
|
||||
metricName: rest.metricName,
|
||||
startTime: Number(startTime),
|
||||
endTime: Number(endTime),
|
||||
period: Number(rest.metricPeriod),
|
||||
statistics: Array.isArray(stat) ? stat : [stat],
|
||||
...(rest.metricDimensions && {
|
||||
dimensions: (() => {
|
||||
const dims = rest.metricDimensions
|
||||
if (typeof dims === 'string') return dims
|
||||
if (Array.isArray(dims)) {
|
||||
const obj: Record<string, string> = {}
|
||||
for (const row of dims) {
|
||||
const name = row.cells?.name
|
||||
const value = row.cells?.value
|
||||
if (name && value !== undefined) obj[name] = String(value)
|
||||
}
|
||||
return JSON.stringify(obj)
|
||||
}
|
||||
return JSON.stringify(dims)
|
||||
})(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
case 'describe_alarms':
|
||||
return {
|
||||
awsRegion,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
...(rest.alarmNamePrefix && { alarmNamePrefix: rest.alarmNamePrefix }),
|
||||
...(rest.stateValue && { stateValue: rest.stateValue }),
|
||||
...(rest.alarmType && { alarmType: rest.alarmType }),
|
||||
...(parsedLimit !== undefined && { limit: parsedLimit }),
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid CloudWatch operation: ${operation}`)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'CloudWatch operation to perform' },
|
||||
awsRegion: { type: 'string', description: 'AWS region' },
|
||||
awsAccessKeyId: { type: 'string', description: 'AWS access key ID' },
|
||||
awsSecretAccessKey: { type: 'string', description: 'AWS secret access key' },
|
||||
logGroupNames: { type: 'string', description: 'Log group name(s) for query' },
|
||||
queryString: { type: 'string', description: 'CloudWatch Log Insights query string' },
|
||||
startTime: { type: 'string', description: 'Start time as Unix epoch seconds' },
|
||||
endTime: { type: 'string', description: 'End time as Unix epoch seconds' },
|
||||
prefix: { type: 'string', description: 'Log group name prefix filter' },
|
||||
logGroupName: {
|
||||
type: 'string',
|
||||
description: 'Log group name for get events / describe streams',
|
||||
},
|
||||
logStreamName: { type: 'string', description: 'Log stream name for get events' },
|
||||
streamPrefix: { type: 'string', description: 'Log stream name prefix filter' },
|
||||
metricNamespace: { type: 'string', description: 'Metric namespace (e.g., AWS/EC2)' },
|
||||
metricName: { type: 'string', description: 'Metric name (e.g., CPUUtilization)' },
|
||||
recentlyActive: { type: 'boolean', description: 'Only show recently active metrics' },
|
||||
metricPeriod: { type: 'number', description: 'Granularity in seconds' },
|
||||
metricStatistics: { type: 'string', description: 'Statistic type (Average, Sum, etc.)' },
|
||||
metricDimensions: { type: 'json', description: 'Metric dimensions (Name/Value pairs)' },
|
||||
alarmNamePrefix: { type: 'string', description: 'Alarm name prefix filter' },
|
||||
stateValue: {
|
||||
type: 'string',
|
||||
description: 'Alarm state filter (OK, ALARM, INSUFFICIENT_DATA)',
|
||||
},
|
||||
alarmType: { type: 'string', description: 'Alarm type filter (MetricAlarm, CompositeAlarm)' },
|
||||
limit: { type: 'number', description: 'Maximum number of results' },
|
||||
},
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Log Insights query result rows',
|
||||
},
|
||||
statistics: {
|
||||
type: 'json',
|
||||
description: 'Query statistics (bytesScanned, recordsMatched, recordsScanned)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Query completion status',
|
||||
},
|
||||
logGroups: {
|
||||
type: 'array',
|
||||
description: 'List of CloudWatch log groups',
|
||||
},
|
||||
events: {
|
||||
type: 'array',
|
||||
description: 'Log events with timestamp and message',
|
||||
},
|
||||
logStreams: {
|
||||
type: 'array',
|
||||
description: 'Log streams with metadata',
|
||||
},
|
||||
metrics: {
|
||||
type: 'array',
|
||||
description: 'List of available metrics',
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
description: 'Metric label',
|
||||
},
|
||||
datapoints: {
|
||||
type: 'array',
|
||||
description: 'Metric datapoints with timestamps and values',
|
||||
},
|
||||
alarms: {
|
||||
type: 'array',
|
||||
description: 'CloudWatch alarms with state and configuration',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { CirclebackBlock } from '@/blocks/blocks/circleback'
|
||||
import { ClayBlock } from '@/blocks/blocks/clay'
|
||||
import { ClerkBlock } from '@/blocks/blocks/clerk'
|
||||
import { CloudflareBlock } from '@/blocks/blocks/cloudflare'
|
||||
import { CloudWatchBlock } from '@/blocks/blocks/cloudwatch'
|
||||
import { ConditionBlock } from '@/blocks/blocks/condition'
|
||||
import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence'
|
||||
import { CredentialBlock } from '@/blocks/blocks/credential'
|
||||
@@ -241,6 +242,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
chat_trigger: ChatTriggerBlock,
|
||||
circleback: CirclebackBlock,
|
||||
cloudflare: CloudflareBlock,
|
||||
cloudwatch: CloudWatchBlock,
|
||||
clay: ClayBlock,
|
||||
clerk: ClerkBlock,
|
||||
condition: ConditionBlock,
|
||||
|
||||
@@ -72,6 +72,8 @@ export { Table } from './table'
|
||||
export { TableX } from './table-x'
|
||||
export { TagIcon } from './tag'
|
||||
export { TerminalWindow } from './terminal-window'
|
||||
export { ThumbsDown } from './thumbs-down'
|
||||
export { ThumbsUp } from './thumbs-up'
|
||||
export { Trash } from './trash'
|
||||
export { TrashOutline } from './trash-outline'
|
||||
export { Trash2 } from './trash2'
|
||||
|
||||
28
apps/sim/components/emcn/icons/thumbs-down.tsx
Normal file
28
apps/sim/components/emcn/icons/thumbs-down.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
/**
|
||||
* ThumbsDown icon component
|
||||
* @param props - SVG properties including className, fill, etc.
|
||||
*/
|
||||
export function ThumbsDown(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='-1 -2 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.75'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
aria-hidden='true'
|
||||
{...props}
|
||||
>
|
||||
<g transform='scale(1,-1) translate(0,-20)'>
|
||||
<path d='M6 8v12' />
|
||||
<path d='M14 3.88L13 8h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 16.5 20H3a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L11 0a3.13 3.13 0 0 1 3 3.88Z' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
26
apps/sim/components/emcn/icons/thumbs-up.tsx
Normal file
26
apps/sim/components/emcn/icons/thumbs-up.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
/**
|
||||
* ThumbsUp icon component
|
||||
* @param props - SVG properties including className, fill, etc.
|
||||
*/
|
||||
export function ThumbsUp(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='-1 -2 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.75'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
aria-hidden='true'
|
||||
{...props}
|
||||
>
|
||||
<path d='M6 8v12' />
|
||||
<path d='M14 3.88L13 8h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 16.5 20H3a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L11 0a3.13 3.13 0 0 1 3 3.88Z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -4653,6 +4653,33 @@ export function SQSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function CloudWatchIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 80 80'
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
>
|
||||
<g
|
||||
id='Icon-Architecture/64/Arch_Amazon-CloudWatch_64'
|
||||
stroke='none'
|
||||
strokeWidth='1'
|
||||
fill='none'
|
||||
fillRule='evenodd'
|
||||
transform='translate(40, 40) scale(1.25) translate(-40, -40)'
|
||||
>
|
||||
<path
|
||||
d='M53,42 L41,42 L41,24 L43,24 L43,40 L53,40 L53,42 Z M40,66 C24.561,66 12,53.439 12,38 C12,22.561 24.561,10 40,10 C55.439,10 68,22.561 68,38 C68,53.439 55.439,66 40,66 M40,8 C23.458,8 10,21.458 10,38 C10,54.542 23.458,68 40,68 C56.542,68 70,54.542 70,38 C70,21.458 56.542,8 40,8'
|
||||
id='Amazon-CloudWatch_Icon_64_Squid'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function TextractIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
39
apps/sim/hooks/queries/copilot-feedback.ts
Normal file
39
apps/sim/hooks/queries/copilot-feedback.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
|
||||
const logger = createLogger('CopilotFeedbackMutation')
|
||||
|
||||
interface SubmitFeedbackVariables {
|
||||
chatId: string
|
||||
userQuery: string
|
||||
agentResponse: string
|
||||
isPositiveFeedback: boolean
|
||||
feedback?: string
|
||||
}
|
||||
|
||||
interface SubmitFeedbackResponse {
|
||||
success: boolean
|
||||
feedbackId: string
|
||||
}
|
||||
|
||||
export function useSubmitCopilotFeedback() {
|
||||
return useMutation({
|
||||
mutationFn: async (variables: SubmitFeedbackVariables): Promise<SubmitFeedbackResponse> => {
|
||||
const response = await fetch('/api/copilot/feedback', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(variables),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
throw new Error(data.error || 'Failed to submit feedback')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error('Failed to submit copilot feedback:', error)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1716,6 +1716,81 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
}))
|
||||
},
|
||||
},
|
||||
'cloudwatch.logGroups': {
|
||||
key: 'cloudwatch.logGroups',
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'cloudwatch.logGroups',
|
||||
context.awsAccessKeyId ?? 'none',
|
||||
context.awsRegion ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(context.awsAccessKeyId && context.awsSecretAccessKey && context.awsRegion),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({
|
||||
accessKeyId: context.awsAccessKeyId,
|
||||
secretAccessKey: context.awsSecretAccessKey,
|
||||
region: context.awsRegion,
|
||||
...(search && { prefix: search }),
|
||||
})
|
||||
const data = await fetchJson<{
|
||||
output: { logGroups: { logGroupName: string }[] }
|
||||
}>('/api/tools/cloudwatch/describe-log-groups', {
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
return (data.output?.logGroups || []).map((lg) => ({
|
||||
id: lg.logGroupName,
|
||||
label: lg.logGroupName,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ detailId }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
return { id: detailId, label: detailId }
|
||||
},
|
||||
},
|
||||
'cloudwatch.logStreams': {
|
||||
key: 'cloudwatch.logStreams',
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'cloudwatch.logStreams',
|
||||
context.awsAccessKeyId ?? 'none',
|
||||
context.awsRegion ?? 'none',
|
||||
context.logGroupName ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(
|
||||
context.awsAccessKeyId &&
|
||||
context.awsSecretAccessKey &&
|
||||
context.awsRegion &&
|
||||
context.logGroupName
|
||||
),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({
|
||||
accessKeyId: context.awsAccessKeyId,
|
||||
secretAccessKey: context.awsSecretAccessKey,
|
||||
region: context.awsRegion,
|
||||
logGroupName: context.logGroupName,
|
||||
...(search && { prefix: search }),
|
||||
})
|
||||
const data = await fetchJson<{
|
||||
output: { logStreams: { logStreamName: string; lastEventTimestamp?: number }[] }
|
||||
}>('/api/tools/cloudwatch/describe-log-streams', {
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
return (data.output?.logStreams || []).map((ls) => ({
|
||||
id: ls.logStreamName,
|
||||
label: ls.logStreamName,
|
||||
}))
|
||||
},
|
||||
fetchById: async ({ detailId }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
return { id: detailId, label: detailId }
|
||||
},
|
||||
},
|
||||
'sim.workflows': {
|
||||
key: 'sim.workflows',
|
||||
staleTime: SELECTOR_STALE,
|
||||
|
||||
@@ -49,6 +49,8 @@ export type SelectorKey =
|
||||
| 'webflow.sites'
|
||||
| 'webflow.collections'
|
||||
| 'webflow.items'
|
||||
| 'cloudwatch.logGroups'
|
||||
| 'cloudwatch.logStreams'
|
||||
| 'sim.workflows'
|
||||
|
||||
export interface SelectorOption {
|
||||
@@ -78,6 +80,10 @@ export interface SelectorContext {
|
||||
datasetId?: string
|
||||
serviceDeskId?: string
|
||||
impersonateUserEmail?: string
|
||||
awsAccessKeyId?: string
|
||||
awsSecretAccessKey?: string
|
||||
awsRegion?: string
|
||||
logGroupName?: string
|
||||
}
|
||||
|
||||
export interface SelectorQueryArgs {
|
||||
|
||||
@@ -140,25 +140,17 @@ async function initializeOpenTelemetry() {
|
||||
|
||||
sdk.start()
|
||||
|
||||
const shutdownHandler = async () => {
|
||||
const shutdownOtel = async () => {
|
||||
try {
|
||||
await sdk.shutdown()
|
||||
logger.info('OpenTelemetry SDK shut down successfully')
|
||||
} catch (err) {
|
||||
logger.error('Error shutting down OpenTelemetry SDK', err)
|
||||
}
|
||||
|
||||
try {
|
||||
const { getPostHogClient } = await import('@/lib/posthog/server')
|
||||
await getPostHogClient()?.shutdown()
|
||||
logger.info('PostHog client shut down successfully')
|
||||
} catch (err) {
|
||||
logger.error('Error shutting down PostHog client', err)
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGTERM', shutdownHandler)
|
||||
process.on('SIGINT', shutdownHandler)
|
||||
process.on('SIGTERM', shutdownOtel)
|
||||
process.on('SIGINT', shutdownOtel)
|
||||
|
||||
logger.info('OpenTelemetry instrumentation initialized with business span filtering')
|
||||
} catch (error) {
|
||||
@@ -169,6 +161,19 @@ async function initializeOpenTelemetry() {
|
||||
export async function register() {
|
||||
await initializeOpenTelemetry()
|
||||
|
||||
const shutdownPostHog = async () => {
|
||||
try {
|
||||
const { getPostHogClient } = await import('@/lib/posthog/server')
|
||||
await getPostHogClient()?.shutdown()
|
||||
logger.info('PostHog client shut down successfully')
|
||||
} catch (err) {
|
||||
logger.error('Error shutting down PostHog client', err)
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGTERM', shutdownPostHog)
|
||||
process.on('SIGINT', shutdownPostHog)
|
||||
|
||||
const { startMemoryTelemetry } = await import('./lib/monitoring/memory-telemetry')
|
||||
startMemoryTelemetry()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export const buildTimeCSPDirectives: CSPDirectives = {
|
||||
'https://*.google.com',
|
||||
'https://apis.google.com',
|
||||
'https://assets.onedollarstats.com',
|
||||
'https://challenges.cloudflare.com',
|
||||
...(isReactGrabEnabled ? ['https://unpkg.com'] : []),
|
||||
],
|
||||
|
||||
@@ -102,6 +103,7 @@ export const buildTimeCSPDirectives: CSPDirectives = {
|
||||
'https://*.supabase.co',
|
||||
'https://api.github.com',
|
||||
'https://github.com/*',
|
||||
'https://challenges.cloudflare.com',
|
||||
'https://collector.onedollarstats.com',
|
||||
...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL),
|
||||
...getHostnameFromUrl(env.NEXT_PUBLIC_PRIVACY_URL),
|
||||
@@ -110,6 +112,7 @@ export const buildTimeCSPDirectives: CSPDirectives = {
|
||||
|
||||
'frame-src': [
|
||||
"'self'",
|
||||
'https://challenges.cloudflare.com',
|
||||
'https://drive.google.com',
|
||||
'https://docs.google.com',
|
||||
'https://*.google.com',
|
||||
@@ -171,13 +174,13 @@ export function generateRuntimeCSP(): string {
|
||||
|
||||
return `
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.google.com https://apis.google.com https://assets.onedollarstats.com ${reactGrabScript};
|
||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.google.com https://apis.google.com https://assets.onedollarstats.com https://challenges.cloudflare.com ${reactGrabScript};
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||
img-src 'self' data: blob: https://*.googleusercontent.com https://*.google.com https://*.atlassian.com https://cdn.discordapp.com https://*.githubusercontent.com https://*.s3.amazonaws.com https://s3.amazonaws.com https://*.amazonaws.com https://*.blob.core.windows.net https://github.com/* https://collector.onedollarstats.com ${brandLogoDomain} ${brandFaviconDomain};
|
||||
media-src 'self' blob:;
|
||||
font-src 'self' https://fonts.gstatic.com;
|
||||
connect-src 'self' ${appUrl} ${ollamaUrl} ${socketUrl} ${socketWsUrl} https://api.browser-use.com https://api.exa.ai https://api.firecrawl.dev https://*.googleapis.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.blob.core.windows.net https://api.github.com https://github.com/* https://*.atlassian.com https://*.supabase.co https://collector.onedollarstats.com ${dynamicDomainsStr};
|
||||
frame-src 'self' https://drive.google.com https://docs.google.com https://*.google.com;
|
||||
connect-src 'self' ${appUrl} ${ollamaUrl} ${socketUrl} ${socketWsUrl} https://api.browser-use.com https://api.exa.ai https://api.firecrawl.dev https://*.googleapis.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.blob.core.windows.net https://api.github.com https://github.com/* https://*.atlassian.com https://*.supabase.co https://challenges.cloudflare.com https://collector.onedollarstats.com ${dynamicDomainsStr};
|
||||
frame-src 'self' https://challenges.cloudflare.com https://drive.google.com https://docs.google.com https://*.google.com;
|
||||
frame-ancestors 'self';
|
||||
form-action 'self';
|
||||
base-uri 'self';
|
||||
|
||||
@@ -123,6 +123,7 @@ describe('executeWorkflowCore terminal finalization sequencing', () => {
|
||||
requestId: 'req-1',
|
||||
workflowId: 'workflow-1',
|
||||
userId: 'user-1',
|
||||
workflowUserId: 'workflow-owner',
|
||||
workspaceId: 'workspace-1',
|
||||
triggerType: 'api',
|
||||
executionId: 'execution-1',
|
||||
@@ -755,4 +756,92 @@ describe('executeWorkflowCore terminal finalization sequencing', () => {
|
||||
expect(safeCompleteWithErrorMock).not.toHaveBeenCalled()
|
||||
expect(wasExecutionFinalizedByCore(envError, 'execution-no-log-start')).toBe(false)
|
||||
})
|
||||
|
||||
it('uses sessionUserId for env resolution when isClientSession is true', async () => {
|
||||
const snapshot = {
|
||||
...createSnapshot(),
|
||||
metadata: {
|
||||
...createSnapshot().metadata,
|
||||
isClientSession: true,
|
||||
sessionUserId: 'session-user',
|
||||
workflowUserId: 'workflow-owner',
|
||||
},
|
||||
}
|
||||
|
||||
getPersonalAndWorkspaceEnvMock.mockResolvedValue({
|
||||
personalEncrypted: {},
|
||||
workspaceEncrypted: {},
|
||||
personalDecrypted: {},
|
||||
workspaceDecrypted: {},
|
||||
})
|
||||
safeStartMock.mockResolvedValue(true)
|
||||
executorExecuteMock.mockResolvedValue({
|
||||
output: { done: true },
|
||||
logs: [],
|
||||
metadata: { duration: 123, startTime: 'start', endTime: 'end' },
|
||||
})
|
||||
|
||||
await executeWorkflowCore({
|
||||
snapshot: snapshot as any,
|
||||
callbacks: {},
|
||||
loggingSession: loggingSession as any,
|
||||
})
|
||||
|
||||
expect(getPersonalAndWorkspaceEnvMock).toHaveBeenCalledWith('session-user', 'workspace-1')
|
||||
})
|
||||
|
||||
it('uses workflowUserId for env resolution in server-side execution', async () => {
|
||||
const snapshot = {
|
||||
...createSnapshot(),
|
||||
metadata: {
|
||||
...createSnapshot().metadata,
|
||||
isClientSession: false,
|
||||
sessionUserId: undefined,
|
||||
workflowUserId: 'workflow-owner',
|
||||
userId: 'billing-actor',
|
||||
},
|
||||
}
|
||||
|
||||
getPersonalAndWorkspaceEnvMock.mockResolvedValue({
|
||||
personalEncrypted: {},
|
||||
workspaceEncrypted: {},
|
||||
personalDecrypted: {},
|
||||
workspaceDecrypted: {},
|
||||
})
|
||||
safeStartMock.mockResolvedValue(true)
|
||||
executorExecuteMock.mockResolvedValue({
|
||||
output: { done: true },
|
||||
logs: [],
|
||||
metadata: { duration: 123, startTime: 'start', endTime: 'end' },
|
||||
})
|
||||
|
||||
await executeWorkflowCore({
|
||||
snapshot: snapshot as any,
|
||||
callbacks: {},
|
||||
loggingSession: loggingSession as any,
|
||||
})
|
||||
|
||||
expect(getPersonalAndWorkspaceEnvMock).toHaveBeenCalledWith('workflow-owner', 'workspace-1')
|
||||
})
|
||||
|
||||
it('throws when workflowUserId is missing in server-side execution', async () => {
|
||||
const snapshot = {
|
||||
...createSnapshot(),
|
||||
metadata: {
|
||||
...createSnapshot().metadata,
|
||||
isClientSession: false,
|
||||
sessionUserId: undefined,
|
||||
workflowUserId: undefined,
|
||||
userId: 'billing-actor',
|
||||
},
|
||||
}
|
||||
|
||||
await expect(
|
||||
executeWorkflowCore({
|
||||
snapshot: snapshot as any,
|
||||
callbacks: {},
|
||||
loggingSession: loggingSession as any,
|
||||
})
|
||||
).rejects.toThrow('Missing workflowUserId in execution metadata')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -325,10 +325,13 @@ export async function executeWorkflowCore(
|
||||
|
||||
const mergedStates = mergeSubblockStateWithValues(blocks)
|
||||
|
||||
const personalEnvUserId = metadata.sessionUserId || metadata.userId
|
||||
const personalEnvUserId =
|
||||
metadata.isClientSession && metadata.sessionUserId
|
||||
? metadata.sessionUserId
|
||||
: metadata.workflowUserId
|
||||
|
||||
if (!personalEnvUserId) {
|
||||
throw new Error('Missing execution actor for environment resolution')
|
||||
throw new Error('Missing workflowUserId in execution metadata')
|
||||
}
|
||||
|
||||
const { personalEncrypted, workspaceEncrypted, personalDecrypted, workspaceDecrypted } =
|
||||
|
||||
@@ -22,6 +22,10 @@ export const SELECTOR_CONTEXT_FIELDS = new Set<keyof SelectorContext>([
|
||||
'datasetId',
|
||||
'serviceDeskId',
|
||||
'impersonateUserEmail',
|
||||
'awsAccessKeyId',
|
||||
'awsSecretAccessKey',
|
||||
'awsRegion',
|
||||
'logGroupName',
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
"@a2a-js/sdk": "0.3.7",
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.940.0",
|
||||
"@aws-sdk/client-cloudwatch": "3.940.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "3.940.0",
|
||||
"@aws-sdk/client-dynamodb": "3.940.0",
|
||||
"@aws-sdk/client-rds-data": "3.940.0",
|
||||
"@aws-sdk/client-s3": "^3.779.0",
|
||||
@@ -154,8 +156,8 @@
|
||||
"papaparse": "5.5.3",
|
||||
"pdf-lib": "1.17.1",
|
||||
"postgres": "^3.4.5",
|
||||
"posthog-js": "1.334.1",
|
||||
"posthog-node": "5.9.2",
|
||||
"posthog-js": "1.364.4",
|
||||
"posthog-node": "5.28.9",
|
||||
"pptxgenjs": "4.0.1",
|
||||
"pptxviewjs": "1.1.8",
|
||||
"prismjs": "^1.30.0",
|
||||
|
||||
@@ -271,6 +271,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -290,6 +291,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -324,6 +326,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -342,6 +345,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -360,6 +364,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -373,6 +378,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
},
|
||||
@@ -449,6 +455,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
reasoningEffort: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 100000,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
},
|
||||
@@ -463,6 +470,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
},
|
||||
@@ -509,7 +517,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 128000,
|
||||
maxOutputTokens: 64000,
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high', 'max'],
|
||||
default: 'high',
|
||||
@@ -741,6 +749,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -759,6 +768,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -777,6 +787,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -795,6 +806,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -813,6 +825,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -831,6 +844,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
maxOutputTokens: 128000,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
@@ -1067,6 +1081,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
levels: ['minimal', 'low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
},
|
||||
maxOutputTokens: 65536,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
},
|
||||
@@ -1084,6 +1099,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
levels: ['minimal', 'low', 'medium', 'high'],
|
||||
default: 'minimal',
|
||||
},
|
||||
maxOutputTokens: 65536,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
},
|
||||
@@ -1101,6 +1117,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
levels: ['minimal', 'low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
},
|
||||
maxOutputTokens: 65536,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
},
|
||||
@@ -1114,6 +1131,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
maxOutputTokens: 65536,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
},
|
||||
@@ -1127,6 +1145,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
maxOutputTokens: 65536,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
},
|
||||
@@ -1140,6 +1159,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
maxOutputTokens: 65536,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
},
|
||||
@@ -1153,6 +1173,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
},
|
||||
@@ -1165,6 +1186,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
},
|
||||
@@ -1178,6 +1200,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
deepResearch: true,
|
||||
memory: false,
|
||||
maxOutputTokens: 65536,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
},
|
||||
@@ -2094,6 +2117,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
},
|
||||
@@ -2107,6 +2131,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
},
|
||||
@@ -2120,6 +2145,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
},
|
||||
@@ -2133,6 +2159,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
},
|
||||
@@ -2337,6 +2364,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
maxOutputTokens: 32768,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
},
|
||||
@@ -2373,6 +2401,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
},
|
||||
@@ -2385,6 +2414,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
maxOutputTokens: 40000,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
},
|
||||
@@ -2397,6 +2427,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
},
|
||||
@@ -2409,6 +2440,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
},
|
||||
@@ -2421,6 +2453,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
},
|
||||
@@ -2863,13 +2896,17 @@ export function getModelsWithoutMemory(): string[] {
|
||||
export function getMaxOutputTokensForModel(modelId: string): number {
|
||||
const normalizedModelId = modelId.toLowerCase()
|
||||
const STANDARD_MAX_OUTPUT_TOKENS = 4096
|
||||
const allModels = Object.values(PROVIDER_DEFINITIONS).flatMap((provider) => provider.models)
|
||||
|
||||
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
|
||||
for (const model of provider.models) {
|
||||
const baseModelId = model.id.toLowerCase()
|
||||
if (normalizedModelId === baseModelId || normalizedModelId.startsWith(`${baseModelId}-`)) {
|
||||
return model.capabilities.maxOutputTokens || STANDARD_MAX_OUTPUT_TOKENS
|
||||
}
|
||||
const exactMatch = allModels.find((model) => model.id.toLowerCase() === normalizedModelId)
|
||||
if (exactMatch) {
|
||||
return exactMatch.capabilities.maxOutputTokens || STANDARD_MAX_OUTPUT_TOKENS
|
||||
}
|
||||
|
||||
for (const model of allModels) {
|
||||
const baseModelId = model.id.toLowerCase()
|
||||
if (normalizedModelId.startsWith(`${baseModelId}-`)) {
|
||||
return model.capabilities.maxOutputTokens || STANDARD_MAX_OUTPUT_TOKENS
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -664,6 +664,45 @@ describe('Model Capabilities', () => {
|
||||
|
||||
describe('Max Output Tokens', () => {
|
||||
describe('getMaxOutputTokensForModel', () => {
|
||||
it.concurrent('should return published max for OpenAI GPT-4o', () => {
|
||||
expect(getMaxOutputTokensForModel('gpt-4o')).toBe(16384)
|
||||
})
|
||||
|
||||
it.concurrent('should return published max for OpenAI GPT-5.1', () => {
|
||||
expect(getMaxOutputTokensForModel('gpt-5.1')).toBe(128000)
|
||||
})
|
||||
|
||||
it.concurrent('should return published max for OpenAI GPT-5 Chat', () => {
|
||||
expect(getMaxOutputTokensForModel('gpt-5-chat-latest')).toBe(16384)
|
||||
})
|
||||
|
||||
it.concurrent('should return published max for OpenAI o1', () => {
|
||||
expect(getMaxOutputTokensForModel('o1')).toBe(100000)
|
||||
})
|
||||
|
||||
it.concurrent('should return updated max for Claude Sonnet 4.6', () => {
|
||||
expect(getMaxOutputTokensForModel('claude-sonnet-4-6')).toBe(64000)
|
||||
})
|
||||
|
||||
it.concurrent('should return published max for Gemini 2.5 Pro', () => {
|
||||
expect(getMaxOutputTokensForModel('gemini-2.5-pro')).toBe(65536)
|
||||
})
|
||||
|
||||
it.concurrent('should return published max for Azure GPT-5.2', () => {
|
||||
expect(getMaxOutputTokensForModel('azure/gpt-5.2')).toBe(128000)
|
||||
})
|
||||
|
||||
it.concurrent('should return standard default for models without maxOutputTokens', () => {
|
||||
expect(getMaxOutputTokensForModel('deepseek-reasoner')).toBe(4096)
|
||||
expect(getMaxOutputTokensForModel('grok-4-latest')).toBe(4096)
|
||||
})
|
||||
|
||||
it.concurrent('should return published max for Bedrock Claude Opus 4.1', () => {
|
||||
expect(getMaxOutputTokensForModel('bedrock/anthropic.claude-opus-4-1-20250805-v1:0')).toBe(
|
||||
64000
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should return correct max for Claude Opus 4.6', () => {
|
||||
expect(getMaxOutputTokensForModel('claude-opus-4-6')).toBe(128000)
|
||||
})
|
||||
@@ -676,10 +715,6 @@ describe('Max Output Tokens', () => {
|
||||
expect(getMaxOutputTokensForModel('claude-opus-4-1')).toBe(32000)
|
||||
})
|
||||
|
||||
it.concurrent('should return standard default for models without maxOutputTokens', () => {
|
||||
expect(getMaxOutputTokensForModel('gpt-4o')).toBe(4096)
|
||||
})
|
||||
|
||||
it.concurrent('should return standard default for unknown models', () => {
|
||||
expect(getMaxOutputTokensForModel('unknown-model')).toBe(4096)
|
||||
})
|
||||
|
||||
99
apps/sim/tools/cloudwatch/describe_alarms.ts
Normal file
99
apps/sim/tools/cloudwatch/describe_alarms.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type {
|
||||
CloudWatchDescribeAlarmsParams,
|
||||
CloudWatchDescribeAlarmsResponse,
|
||||
} from '@/tools/cloudwatch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const describeAlarmsTool: ToolConfig<
|
||||
CloudWatchDescribeAlarmsParams,
|
||||
CloudWatchDescribeAlarmsResponse
|
||||
> = {
|
||||
id: 'cloudwatch_describe_alarms',
|
||||
name: 'CloudWatch Describe Alarms',
|
||||
description: 'List and filter CloudWatch alarms',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
alarmNamePrefix: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter alarms by name prefix',
|
||||
},
|
||||
stateValue: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by alarm state (OK, ALARM, INSUFFICIENT_DATA)',
|
||||
},
|
||||
alarmType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by alarm type (MetricAlarm, CompositeAlarm)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of alarms to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudwatch/describe-alarms',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
...(params.alarmNamePrefix && { alarmNamePrefix: params.alarmNamePrefix }),
|
||||
...(params.stateValue && { stateValue: params.stateValue }),
|
||||
...(params.alarmType && { alarmType: params.alarmType }),
|
||||
...(params.limit !== undefined && { limit: params.limit }),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to describe CloudWatch alarms')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
alarms: data.output.alarms,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
alarms: {
|
||||
type: 'array',
|
||||
description: 'List of CloudWatch alarms with state and configuration',
|
||||
},
|
||||
},
|
||||
}
|
||||
82
apps/sim/tools/cloudwatch/describe_log_groups.ts
Normal file
82
apps/sim/tools/cloudwatch/describe_log_groups.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type {
|
||||
CloudWatchDescribeLogGroupsParams,
|
||||
CloudWatchDescribeLogGroupsResponse,
|
||||
} from '@/tools/cloudwatch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const describeLogGroupsTool: ToolConfig<
|
||||
CloudWatchDescribeLogGroupsParams,
|
||||
CloudWatchDescribeLogGroupsResponse
|
||||
> = {
|
||||
id: 'cloudwatch_describe_log_groups',
|
||||
name: 'CloudWatch Describe Log Groups',
|
||||
description: 'List available CloudWatch log groups',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
prefix: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter log groups by name prefix',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of log groups to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudwatch/describe-log-groups',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
...(params.prefix && { prefix: params.prefix }),
|
||||
...(params.limit !== undefined && { limit: params.limit }),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to describe CloudWatch log groups')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
logGroups: data.output.logGroups,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
logGroups: { type: 'array', description: 'List of CloudWatch log groups with metadata' },
|
||||
},
|
||||
}
|
||||
92
apps/sim/tools/cloudwatch/describe_log_streams.ts
Normal file
92
apps/sim/tools/cloudwatch/describe_log_streams.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type {
|
||||
CloudWatchDescribeLogStreamsParams,
|
||||
CloudWatchDescribeLogStreamsResponse,
|
||||
} from '@/tools/cloudwatch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const describeLogStreamsTool: ToolConfig<
|
||||
CloudWatchDescribeLogStreamsParams,
|
||||
CloudWatchDescribeLogStreamsResponse
|
||||
> = {
|
||||
id: 'cloudwatch_describe_log_streams',
|
||||
name: 'CloudWatch Describe Log Streams',
|
||||
description: 'List log streams within a CloudWatch log group',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
logGroupName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'CloudWatch log group name',
|
||||
},
|
||||
prefix: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter log streams by name prefix',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of log streams to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudwatch/describe-log-streams',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
logGroupName: params.logGroupName,
|
||||
...(params.prefix && { prefix: params.prefix }),
|
||||
...(params.limit !== undefined && { limit: params.limit }),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to describe CloudWatch log streams')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
logStreams: data.output.logStreams,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
logStreams: {
|
||||
type: 'array',
|
||||
description: 'List of log streams with metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
106
apps/sim/tools/cloudwatch/get_log_events.ts
Normal file
106
apps/sim/tools/cloudwatch/get_log_events.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
CloudWatchGetLogEventsParams,
|
||||
CloudWatchGetLogEventsResponse,
|
||||
} from '@/tools/cloudwatch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getLogEventsTool: ToolConfig<
|
||||
CloudWatchGetLogEventsParams,
|
||||
CloudWatchGetLogEventsResponse
|
||||
> = {
|
||||
id: 'cloudwatch_get_log_events',
|
||||
name: 'CloudWatch Get Log Events',
|
||||
description: 'Retrieve log events from a specific CloudWatch log stream',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
logGroupName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'CloudWatch log group name',
|
||||
},
|
||||
logStreamName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'CloudWatch log stream name',
|
||||
},
|
||||
startTime: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Start time as Unix epoch seconds',
|
||||
},
|
||||
endTime: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End time as Unix epoch seconds',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of events to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudwatch/get-log-events',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
logGroupName: params.logGroupName,
|
||||
logStreamName: params.logStreamName,
|
||||
...(params.startTime !== undefined && { startTime: params.startTime }),
|
||||
...(params.endTime !== undefined && { endTime: params.endTime }),
|
||||
...(params.limit !== undefined && { limit: params.limit }),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to get CloudWatch log events')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
events: data.output.events,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
events: {
|
||||
type: 'array',
|
||||
description: 'Log events with timestamp, message, and ingestion time',
|
||||
},
|
||||
},
|
||||
}
|
||||
119
apps/sim/tools/cloudwatch/get_metric_statistics.ts
Normal file
119
apps/sim/tools/cloudwatch/get_metric_statistics.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type {
|
||||
CloudWatchGetMetricStatisticsParams,
|
||||
CloudWatchGetMetricStatisticsResponse,
|
||||
} from '@/tools/cloudwatch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getMetricStatisticsTool: ToolConfig<
|
||||
CloudWatchGetMetricStatisticsParams,
|
||||
CloudWatchGetMetricStatisticsResponse
|
||||
> = {
|
||||
id: 'cloudwatch_get_metric_statistics',
|
||||
name: 'CloudWatch Get Metric Statistics',
|
||||
description: 'Get statistics for a CloudWatch metric over a time range',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
namespace: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Metric namespace (e.g., AWS/EC2, AWS/Lambda)',
|
||||
},
|
||||
metricName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Metric name (e.g., CPUUtilization, Invocations)',
|
||||
},
|
||||
startTime: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Start time as Unix epoch seconds',
|
||||
},
|
||||
endTime: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End time as Unix epoch seconds',
|
||||
},
|
||||
period: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Granularity in seconds (e.g., 60, 300, 3600)',
|
||||
},
|
||||
statistics: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Statistics to retrieve (Average, Sum, Minimum, Maximum, SampleCount)',
|
||||
},
|
||||
dimensions: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Dimensions as JSON (e.g., {"InstanceId": "i-1234"})',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudwatch/get-metric-statistics',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
namespace: params.namespace,
|
||||
metricName: params.metricName,
|
||||
startTime: params.startTime,
|
||||
endTime: params.endTime,
|
||||
period: params.period,
|
||||
statistics: params.statistics,
|
||||
...(params.dimensions && { dimensions: params.dimensions }),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to get CloudWatch metric statistics')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
label: data.output.label,
|
||||
datapoints: data.output.datapoints,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
label: { type: 'string', description: 'Metric label' },
|
||||
datapoints: { type: 'array', description: 'Datapoints with timestamp and statistics values' },
|
||||
},
|
||||
}
|
||||
15
apps/sim/tools/cloudwatch/index.ts
Normal file
15
apps/sim/tools/cloudwatch/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { describeAlarmsTool } from '@/tools/cloudwatch/describe_alarms'
|
||||
import { describeLogGroupsTool } from '@/tools/cloudwatch/describe_log_groups'
|
||||
import { describeLogStreamsTool } from '@/tools/cloudwatch/describe_log_streams'
|
||||
import { getLogEventsTool } from '@/tools/cloudwatch/get_log_events'
|
||||
import { getMetricStatisticsTool } from '@/tools/cloudwatch/get_metric_statistics'
|
||||
import { listMetricsTool } from '@/tools/cloudwatch/list_metrics'
|
||||
import { queryLogsTool } from '@/tools/cloudwatch/query_logs'
|
||||
|
||||
export const cloudwatchDescribeAlarmsTool = describeAlarmsTool
|
||||
export const cloudwatchDescribeLogGroupsTool = describeLogGroupsTool
|
||||
export const cloudwatchDescribeLogStreamsTool = describeLogStreamsTool
|
||||
export const cloudwatchGetLogEventsTool = getLogEventsTool
|
||||
export const cloudwatchGetMetricStatisticsTool = getMetricStatisticsTool
|
||||
export const cloudwatchListMetricsTool = listMetricsTool
|
||||
export const cloudwatchQueryLogsTool = queryLogsTool
|
||||
96
apps/sim/tools/cloudwatch/list_metrics.ts
Normal file
96
apps/sim/tools/cloudwatch/list_metrics.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type {
|
||||
CloudWatchListMetricsParams,
|
||||
CloudWatchListMetricsResponse,
|
||||
} from '@/tools/cloudwatch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listMetricsTool: ToolConfig<
|
||||
CloudWatchListMetricsParams,
|
||||
CloudWatchListMetricsResponse
|
||||
> = {
|
||||
id: 'cloudwatch_list_metrics',
|
||||
name: 'CloudWatch List Metrics',
|
||||
description: 'List available CloudWatch metrics',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
namespace: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by namespace (e.g., AWS/EC2, AWS/Lambda)',
|
||||
},
|
||||
metricName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by metric name',
|
||||
},
|
||||
recentlyActive: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only show metrics active in the last 3 hours',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of metrics to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudwatch/list-metrics',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
...(params.namespace && { namespace: params.namespace }),
|
||||
...(params.metricName && { metricName: params.metricName }),
|
||||
...(params.recentlyActive && { recentlyActive: params.recentlyActive }),
|
||||
...(params.limit !== undefined && { limit: params.limit }),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to list CloudWatch metrics')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
metrics: data.output.metrics,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
metrics: { type: 'array', description: 'List of metrics with namespace, name, and dimensions' },
|
||||
},
|
||||
}
|
||||
107
apps/sim/tools/cloudwatch/query_logs.ts
Normal file
107
apps/sim/tools/cloudwatch/query_logs.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type {
|
||||
CloudWatchQueryLogsParams,
|
||||
CloudWatchQueryLogsResponse,
|
||||
} from '@/tools/cloudwatch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const queryLogsTool: ToolConfig<CloudWatchQueryLogsParams, CloudWatchQueryLogsResponse> = {
|
||||
id: 'cloudwatch_query_logs',
|
||||
name: 'CloudWatch Query Logs',
|
||||
description: 'Run a CloudWatch Log Insights query against one or more log groups',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
awsRegion: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS region (e.g., us-east-1)',
|
||||
},
|
||||
awsAccessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS access key ID',
|
||||
},
|
||||
awsSecretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'AWS secret access key',
|
||||
},
|
||||
logGroupNames: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Log group names to query',
|
||||
},
|
||||
queryString: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'CloudWatch Log Insights query string',
|
||||
},
|
||||
startTime: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Start time as Unix epoch seconds',
|
||||
},
|
||||
endTime: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End time as Unix epoch seconds',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/cloudwatch/query-logs',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
region: params.awsRegion,
|
||||
accessKeyId: params.awsAccessKeyId,
|
||||
secretAccessKey: params.awsSecretAccessKey,
|
||||
logGroupNames: params.logGroupNames,
|
||||
queryString: params.queryString,
|
||||
startTime: params.startTime,
|
||||
endTime: params.endTime,
|
||||
...(params.limit !== undefined && { limit: params.limit }),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'CloudWatch Log Insights query failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results: data.output.results,
|
||||
statistics: data.output.statistics,
|
||||
status: data.output.status,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'array', description: 'Query result rows' },
|
||||
statistics: {
|
||||
type: 'object',
|
||||
description: 'Query statistics (bytesScanned, recordsMatched, recordsScanned)',
|
||||
},
|
||||
status: { type: 'string', description: 'Query completion status' },
|
||||
},
|
||||
}
|
||||
146
apps/sim/tools/cloudwatch/types.ts
Normal file
146
apps/sim/tools/cloudwatch/types.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface CloudWatchConnectionConfig {
|
||||
awsRegion: string
|
||||
awsAccessKeyId: string
|
||||
awsSecretAccessKey: string
|
||||
}
|
||||
|
||||
export interface CloudWatchQueryLogsParams extends CloudWatchConnectionConfig {
|
||||
logGroupNames: string[]
|
||||
queryString: string
|
||||
startTime: number
|
||||
endTime: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface CloudWatchDescribeLogGroupsParams extends CloudWatchConnectionConfig {
|
||||
prefix?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface CloudWatchGetLogEventsParams extends CloudWatchConnectionConfig {
|
||||
logGroupName: string
|
||||
logStreamName: string
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface CloudWatchQueryLogsResponse extends ToolResponse {
|
||||
output: {
|
||||
results: Record<string, string>[]
|
||||
statistics: {
|
||||
bytesScanned: number
|
||||
recordsMatched: number
|
||||
recordsScanned: number
|
||||
}
|
||||
status: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudWatchDescribeLogGroupsResponse extends ToolResponse {
|
||||
output: {
|
||||
logGroups: {
|
||||
logGroupName: string
|
||||
arn: string
|
||||
storedBytes: number
|
||||
retentionInDays: number | undefined
|
||||
creationTime: number | undefined
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudWatchGetLogEventsResponse extends ToolResponse {
|
||||
output: {
|
||||
events: {
|
||||
timestamp: number | undefined
|
||||
message: string | undefined
|
||||
ingestionTime: number | undefined
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudWatchDescribeLogStreamsParams extends CloudWatchConnectionConfig {
|
||||
logGroupName: string
|
||||
prefix?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface CloudWatchDescribeLogStreamsResponse extends ToolResponse {
|
||||
output: {
|
||||
logStreams: {
|
||||
logStreamName: string
|
||||
lastEventTimestamp: number | undefined
|
||||
firstEventTimestamp: number | undefined
|
||||
creationTime: number | undefined
|
||||
storedBytes: number
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudWatchListMetricsParams extends CloudWatchConnectionConfig {
|
||||
namespace?: string
|
||||
metricName?: string
|
||||
recentlyActive?: boolean
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface CloudWatchListMetricsResponse extends ToolResponse {
|
||||
output: {
|
||||
metrics: {
|
||||
namespace: string
|
||||
metricName: string
|
||||
dimensions: { name: string; value: string }[]
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudWatchGetMetricStatisticsParams extends CloudWatchConnectionConfig {
|
||||
namespace: string
|
||||
metricName: string
|
||||
startTime: number
|
||||
endTime: number
|
||||
period: number
|
||||
statistics: string[]
|
||||
dimensions?: string
|
||||
}
|
||||
|
||||
export interface CloudWatchGetMetricStatisticsResponse extends ToolResponse {
|
||||
output: {
|
||||
label: string
|
||||
datapoints: {
|
||||
timestamp: number
|
||||
average?: number
|
||||
sum?: number
|
||||
minimum?: number
|
||||
maximum?: number
|
||||
sampleCount?: number
|
||||
unit?: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudWatchDescribeAlarmsParams extends CloudWatchConnectionConfig {
|
||||
alarmNamePrefix?: string
|
||||
stateValue?: string
|
||||
alarmType?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface CloudWatchDescribeAlarmsResponse extends ToolResponse {
|
||||
output: {
|
||||
alarms: {
|
||||
alarmName: string
|
||||
alarmArn: string
|
||||
stateValue: string
|
||||
stateReason: string
|
||||
metricName: string | undefined
|
||||
namespace: string | undefined
|
||||
comparisonOperator: string | undefined
|
||||
threshold: number | undefined
|
||||
evaluationPeriods: number | undefined
|
||||
stateUpdatedTimestamp: number | undefined
|
||||
}[]
|
||||
}
|
||||
}
|
||||
@@ -275,6 +275,15 @@ import {
|
||||
cloudflareUpdateDnsRecordTool,
|
||||
cloudflareUpdateZoneSettingTool,
|
||||
} from '@/tools/cloudflare'
|
||||
import {
|
||||
cloudwatchDescribeAlarmsTool,
|
||||
cloudwatchDescribeLogGroupsTool,
|
||||
cloudwatchDescribeLogStreamsTool,
|
||||
cloudwatchGetLogEventsTool,
|
||||
cloudwatchGetMetricStatisticsTool,
|
||||
cloudwatchListMetricsTool,
|
||||
cloudwatchQueryLogsTool,
|
||||
} from '@/tools/cloudwatch'
|
||||
import {
|
||||
confluenceAddLabelTool,
|
||||
confluenceCreateBlogPostTool,
|
||||
@@ -3376,6 +3385,13 @@ export const tools: Record<string, ToolConfig> = {
|
||||
rds_delete: rdsDeleteTool,
|
||||
rds_execute: rdsExecuteTool,
|
||||
rds_introspect: rdsIntrospectTool,
|
||||
cloudwatch_query_logs: cloudwatchQueryLogsTool,
|
||||
cloudwatch_describe_log_groups: cloudwatchDescribeLogGroupsTool,
|
||||
cloudwatch_describe_alarms: cloudwatchDescribeAlarmsTool,
|
||||
cloudwatch_describe_log_streams: cloudwatchDescribeLogStreamsTool,
|
||||
cloudwatch_get_log_events: cloudwatchGetLogEventsTool,
|
||||
cloudwatch_list_metrics: cloudwatchListMetricsTool,
|
||||
cloudwatch_get_metric_statistics: cloudwatchGetMetricStatisticsTool,
|
||||
dynamodb_get: dynamodbGetTool,
|
||||
dynamodb_put: dynamodbPutTool,
|
||||
dynamodb_query: dynamodbQueryTool,
|
||||
|
||||
30
bun.lock
30
bun.lock
@@ -57,6 +57,8 @@
|
||||
"@a2a-js/sdk": "0.3.7",
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.940.0",
|
||||
"@aws-sdk/client-cloudwatch": "3.940.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "3.940.0",
|
||||
"@aws-sdk/client-dynamodb": "3.940.0",
|
||||
"@aws-sdk/client-rds-data": "3.940.0",
|
||||
"@aws-sdk/client-s3": "^3.779.0",
|
||||
@@ -174,8 +176,8 @@
|
||||
"papaparse": "5.5.3",
|
||||
"pdf-lib": "1.17.1",
|
||||
"postgres": "^3.4.5",
|
||||
"posthog-js": "1.334.1",
|
||||
"posthog-node": "5.9.2",
|
||||
"posthog-js": "1.364.4",
|
||||
"posthog-node": "5.28.9",
|
||||
"pptxgenjs": "4.0.1",
|
||||
"pptxviewjs": "1.1.8",
|
||||
"prismjs": "^1.30.0",
|
||||
@@ -414,6 +416,10 @@
|
||||
|
||||
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/eventstream-handler-node": "3.936.0", "@aws-sdk/middleware-eventstream": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/middleware-websocket": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/token-providers": "3.940.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Gs6UUQP1zt8vahOxJ3BADcb3B+2KldUNA3bKa+KdK58de7N7tLJFJfZuXhFGGtwyNPh1aw6phtdP6dauq3OLWA=="],
|
||||
|
||||
"@aws-sdk/client-cloudwatch": ["@aws-sdk/client-cloudwatch@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-compression": "^4.3.12", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-C35xpPntRAGdEg3X5iKpSUCBaP3yxYNo1U95qipN/X1e0/TYIDWHwGt8Z1ntRafK19jp5oVzhRQ+PD1JAPSEzA=="],
|
||||
|
||||
"@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7dEIO3D98IxA9IhqixPJbzQsBkk4TchHHpFdd0JOhlSlihWhiwbf3ijUePJVXYJxcpRRtMmAMtDRLDzCSO+ZHg=="],
|
||||
|
||||
"@aws-sdk/client-dynamodb": ["@aws-sdk/client-dynamodb@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-endpoint-discovery": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-u2sXsNJazJbuHeWICvsj6RvNyJh3isedEfPvB21jK/kxcriK+dE/izlKC2cyxUjERCmku0zTFNzY9FhrLbYHjQ=="],
|
||||
|
||||
"@aws-sdk/client-rds-data": ["@aws-sdk/client-rds-data@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-68NH61MvS48CVPfzBNCPdCG4KnNjM+Uj/3DSw7rT9PJvdML9ARS4M2Uqco9POPw+Aj20KBumsEUd6FMVcYBXAA=="],
|
||||
@@ -1018,9 +1024,9 @@
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@posthog/core": ["@posthog/core@1.13.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-knjncrk7qRmssFRbGzBl1Tunt21GRpe0Wv+uVelyL0Rh7PdQUsgguulzXFTps8hA6wPwTU4kq85qnbAJ3eH6Wg=="],
|
||||
"@posthog/core": ["@posthog/core@1.24.4", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-S+TolwBHSSJz7WWtgaELQWQqXviSm3uf1e+qorWUts0bZcgPwWzhnmhCUZAhvn0NVpTQHDJ3epv+hHbPLl5dHg=="],
|
||||
|
||||
"@posthog/types": ["@posthog/types@1.334.1", "", {}, "sha512-ypFnwTO7qbV7icylLbujbamPdQXbJq0a61GUUBnJAeTbBw/qYPIss5IRYICcbCj0uunQrwD7/CGxVb5TOYKWgA=="],
|
||||
"@posthog/types": ["@posthog/types@1.364.4", "", {}, "sha512-U7NpIy9XWrzz1q/66xyDu8Wm12a7avNRKRn5ISPT5kuCJQRaeAaHuf+dpgrFnuqjCCgxg+oIY/ReJdlZ+8/z4Q=="],
|
||||
|
||||
"@prisma/config": ["@prisma/config@6.19.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ=="],
|
||||
|
||||
@@ -1348,6 +1354,8 @@
|
||||
|
||||
"@smithy/md5-js": ["@smithy/md5-js@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ=="],
|
||||
|
||||
"@smithy/middleware-compression": ["@smithy/middleware-compression@4.3.42", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "fflate": "0.8.1", "tslib": "^2.6.2" } }, "sha512-Ys2R8N7oZ3b6p063lhk7paRbX1F9Ju8a8Bsrw2nFfsG8iHYpgfW6ijd7hJKqRe+Wq9ABfcmX3luBlEd+B5/jVA=="],
|
||||
|
||||
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="],
|
||||
|
||||
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="],
|
||||
@@ -3146,9 +3154,9 @@
|
||||
|
||||
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
|
||||
|
||||
"posthog-js": ["posthog-js@1.334.1", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.13.0", "@posthog/types": "1.334.1", "core-js": "^3.38.1", "dompurify": "^3.3.1", "fflate": "^0.4.8", "preact": "^10.28.0", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-5cDzLICr2afnwX/cR9fwoLC0vN0Nb5gP5HiCigzHkgHdO+E3WsYefla3EFMQz7U4r01CBPZ+nZ9/srkzeACxtQ=="],
|
||||
"posthog-js": ["posthog-js@1.364.4", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.24.4", "@posthog/types": "1.364.4", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-T71zr06gH5YcrjS7c+sdzqfZKMxqqXC/a0w++zMQIPbL1ejvF9PdfUi0Kyd6Sy78Ocbb2smobdzBh8vXLwC+lQ=="],
|
||||
|
||||
"posthog-node": ["posthog-node@5.9.2", "", { "dependencies": { "@posthog/core": "1.2.2" } }, "sha512-oU7FbFcH5cn40nhP04cBeT67zE76EiGWjKKzDvm6IOm5P83sqM0Ij0wMJQSHp+QI6ZN7MLzb+4xfMPUEZ4q6CA=="],
|
||||
"posthog-node": ["posthog-node@5.28.9", "", { "dependencies": { "@posthog/core": "1.24.4" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-iZWyAYkIAq5QqcYz4q2nXOX+Ivn04Yh8AuKqfFVw0SvBpfli49bNAjyE97qbRTLr+irrzRUELgGIkDC14NgugA=="],
|
||||
|
||||
"pptxgenjs": ["pptxgenjs@4.0.1", "", { "dependencies": { "@types/node": "^22.8.1", "https": "^1.0.0", "image-size": "^1.2.1", "jszip": "^3.10.1" } }, "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A=="],
|
||||
|
||||
@@ -4122,6 +4130,10 @@
|
||||
|
||||
"@shuding/opentype.js/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="],
|
||||
|
||||
"@smithy/middleware-compression/@smithy/core": ["@smithy/core@3.23.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q=="],
|
||||
|
||||
"@smithy/middleware-compression/fflate": ["fflate@0.8.1", "", {}, "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ=="],
|
||||
|
||||
"@socket.io/redis-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
@@ -4408,8 +4420,6 @@
|
||||
|
||||
"posthog-js/fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="],
|
||||
|
||||
"posthog-node/@posthog/core": ["@posthog/core@1.2.2", "", {}, "sha512-f16Ozx6LIigRG+HsJdt+7kgSxZTHeX5f1JlCGKI1lXcvlZgfsCR338FuMI2QRYXGl+jg/vYFzGOTQBxl90lnBg=="],
|
||||
|
||||
"pptxgenjs/image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="],
|
||||
|
||||
"protobufjs/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
|
||||
@@ -4662,6 +4672,8 @@
|
||||
|
||||
"@shikijs/rehype/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
|
||||
|
||||
"@smithy/middleware-compression/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="],
|
||||
|
||||
"@trigger.dev/core/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||
|
||||
"@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="],
|
||||
@@ -5130,6 +5142,8 @@
|
||||
|
||||
"@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"@smithy/middleware-compression/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.1", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw=="],
|
||||
|
||||
"@trigger.dev/core/socket.io-client/engine.io-client/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||
|
||||
"@trigger.dev/core/socket.io-client/engine.io-client/xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.0.0", "", {}, "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="],
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# Database URL (Required for migrations and database operations)
|
||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/simstudio"
|
||||
DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"db:push": "bunx drizzle-kit push --config=./drizzle.config.ts",
|
||||
"db:migrate": "bun --env-file=.env --bun x drizzle-kit migrate --config=./drizzle.config.ts",
|
||||
"db:migrate": "bun --env-file=.env run ./scripts/migrate.ts",
|
||||
"db:studio": "bunx drizzle-kit studio --config=./drizzle.config.ts",
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "biome check --write --unsafe .",
|
||||
|
||||
23
packages/db/scripts/migrate.ts
Normal file
23
packages/db/scripts/migrate.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import { migrate } from 'drizzle-orm/postgres-js/migrator'
|
||||
import postgres from 'postgres'
|
||||
|
||||
const url = process.env.DATABASE_URL
|
||||
if (!url) {
|
||||
console.error('ERROR: Missing DATABASE_URL environment variable.')
|
||||
console.error('Ensure packages/db/.env is configured.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const client = postgres(url, { max: 1, connect_timeout: 10 })
|
||||
|
||||
try {
|
||||
await migrate(drizzle(client), { migrationsFolder: './migrations' })
|
||||
console.log('Migrations applied successfully.')
|
||||
} catch (error) {
|
||||
console.error('ERROR: Migration failed.')
|
||||
console.error(error instanceof Error ? error.message : error)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
await client.end()
|
||||
}
|
||||
Reference in New Issue
Block a user