Skip to content

Commit 3b63006

Browse files
committed
feat(app): add automated tasks part 1
1 parent 92b2dc9 commit 3b63006

File tree

126 files changed

+12230
-15
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+12230
-15
lines changed

apps/app/instrumentation-client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { initBotId } from 'botid/client/core';
2+
3+
initBotId({
4+
protect: [
5+
{
6+
path: '/api/tasks-automations/chat',
7+
method: 'POST',
8+
},
9+
],
10+
});

apps/app/markdown.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module '*.md' {
2+
const content: string
3+
export default content
4+
}

apps/app/next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const config: NextConfig = {
1111
? `${process.env.STATIC_ASSETS_URL}/app`
1212
: '',
1313
reactStrictMode: true,
14-
transpilePackages: ['@trycompai/db'],
14+
transpilePackages: ['@trycompai/db', '@prisma/client'],
1515
images: {
1616
remotePatterns: [
1717
{

apps/app/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"@ai-sdk/provider": "^2.0.0",
99
"@ai-sdk/react": "^2.0.0",
1010
"@ai-sdk/rsc": "^1.0.0",
11+
"@aws-sdk/client-lambda": "^3.891.0",
1112
"@aws-sdk/client-s3": "^3.859.0",
1213
"@aws-sdk/client-sts": "^3.808.0",
1314
"@aws-sdk/s3-request-presigner": "^3.859.0",
@@ -49,13 +50,16 @@
4950
"@trycompai/db": "^1.3.4",
5051
"@trycompai/email": "workspace:*",
5152
"@types/canvas-confetti": "^1.9.0",
53+
"@types/react-syntax-highlighter": "^15.5.13",
5254
"@types/three": "^0.180.0",
5355
"@uploadthing/react": "^7.3.0",
5456
"@upstash/ratelimit": "^2.0.5",
57+
"@vercel/sandbox": "^0.0.21",
5558
"@vercel/sdk": "^1.7.1",
5659
"ai": "^5.0.0",
5760
"axios": "^1.9.0",
5861
"better-auth": "^1.2.8",
62+
"botid": "^1.5.5",
5963
"canvas-confetti": "^1.9.3",
6064
"d3": "^7.9.0",
6165
"dub": "^0.66.1",
@@ -79,9 +83,12 @@
7983
"react-hotkeys-hook": "^5.1.0",
8084
"react-intersection-observer": "^9.16.0",
8185
"react-markdown": "10.1.0",
86+
"react-spinners": "^0.17.0",
87+
"react-syntax-highlighter": "^15.6.6",
8288
"react-textarea-autosize": "^8.5.9",
8389
"react-use-draggable-scroll": "^0.4.7",
8490
"react-wrap-balancer": "^1.1.1",
91+
"rehype-raw": "^7.0.0",
8592
"remark-gfm": "^4.0.1",
8693
"remark-parse": "^11.0.0",
8794
"resend": "^4.4.1",
@@ -91,6 +98,7 @@
9198
"ts-pattern": "^5.7.0",
9299
"use-debounce": "^10.0.4",
93100
"use-long-press": "^3.3.0",
101+
"use-stick-to-bottom": "^1.1.1",
94102
"xml2js": "^0.6.2",
95103
"zaraz-ts": "^1.2.0",
96104
"zod": "^3.25.76",

apps/app/src/ai/constants.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { type GatewayModelId } from '@ai-sdk/gateway';
2+
3+
export enum Models {
4+
AmazonNovaPro = 'amazon/nova-pro',
5+
AnthropicClaude4Sonnet = 'anthropic/claude-4-sonnet',
6+
GoogleGeminiFlash = 'google/gemini-2.5-flash',
7+
MoonshotKimiK2 = 'moonshotai/kimi-k2',
8+
OpenAIGPT5 = 'gpt-5',
9+
XaiGrok3Fast = 'xai/grok-3-fast',
10+
}
11+
12+
export const DEFAULT_MODEL = Models.OpenAIGPT5;
13+
14+
export const SUPPORTED_MODELS: GatewayModelId[] = [
15+
Models.AmazonNovaPro,
16+
Models.AnthropicClaude4Sonnet,
17+
Models.GoogleGeminiFlash,
18+
Models.MoonshotKimiK2,
19+
Models.OpenAIGPT5,
20+
Models.XaiGrok3Fast,
21+
];
22+
23+
export const TEST_PROMPTS = [
24+
'I need an automation that calls github to check if trycompai/comp has dependabot enabled.',
25+
'Create an automation to list all open issues in a GitHub repository.',
26+
];

apps/app/src/ai/gateway.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { createGatewayProvider } from '@ai-sdk/gateway'
2+
import { Models } from './constants'
3+
import type { JSONValue } from 'ai'
4+
import type { OpenAIResponsesProviderOptions } from '@ai-sdk/openai'
5+
import type { LanguageModelV2 } from '@ai-sdk/provider'
6+
7+
export async function getAvailableModels() {
8+
const gateway = gatewayInstance()
9+
const response = await gateway.getAvailableModels()
10+
return response.models
11+
.map((model) => ({ id: model.id, name: model.name }))
12+
.concat([{ id: Models.OpenAIGPT5, name: 'GPT-5' }])
13+
}
14+
15+
export interface ModelOptions {
16+
model: LanguageModelV2
17+
providerOptions?: Record<string, Record<string, JSONValue>>
18+
headers?: Record<string, string>
19+
}
20+
21+
export function getModelOptions(
22+
modelId: string,
23+
options?: { reasoningEffort?: 'minimal' | 'low' | 'medium' }
24+
): ModelOptions {
25+
const gateway = gatewayInstance()
26+
if (modelId === Models.OpenAIGPT5) {
27+
return {
28+
model: gateway(modelId),
29+
providerOptions: {
30+
openai: {
31+
include: ['reasoning.encrypted_content'],
32+
reasoningEffort: options?.reasoningEffort ?? 'low',
33+
reasoningSummary: 'auto',
34+
serviceTier: 'priority',
35+
} satisfies OpenAIResponsesProviderOptions,
36+
},
37+
}
38+
}
39+
40+
if (modelId === Models.AnthropicClaude4Sonnet) {
41+
return {
42+
model: gateway(modelId),
43+
headers: { 'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14' },
44+
providerOptions: {
45+
anthropic: {
46+
cacheControl: { type: 'ephemeral' },
47+
},
48+
},
49+
}
50+
}
51+
52+
return {
53+
model: gateway(modelId),
54+
}
55+
}
56+
57+
function gatewayInstance() {
58+
return createGatewayProvider({
59+
baseURL: process.env.AI_GATEWAY_BASE_URL,
60+
})
61+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import z from 'zod/v3';
2+
3+
export const errorSchema = z.object({
4+
message: z.string(),
5+
});
6+
7+
export const dataPartSchema = z.object({
8+
'create-sandbox': z.object({
9+
sandboxId: z.string().optional(),
10+
status: z.enum(['loading', 'done', 'error']),
11+
error: errorSchema.optional(),
12+
}),
13+
'generating-files': z.object({
14+
paths: z.array(z.string()),
15+
status: z.enum(['generating', 'uploading', 'uploaded', 'done', 'error']),
16+
error: errorSchema.optional(),
17+
}),
18+
'run-command': z.object({
19+
sandboxId: z.string(),
20+
commandId: z.string().optional(),
21+
command: z.string(),
22+
args: z.array(z.string()),
23+
status: z.enum(['executing', 'running', 'waiting', 'done', 'error']),
24+
exitCode: z.number().optional(),
25+
error: errorSchema.optional(),
26+
}),
27+
'get-sandbox-url': z.object({
28+
url: z.string().optional(),
29+
status: z.enum(['loading', 'done']),
30+
}),
31+
'report-errors': z.object({
32+
summary: z.string(),
33+
paths: z.array(z.string()).optional(),
34+
}),
35+
'store-to-s3': z.object({
36+
status: z.enum(['uploading', 'done', 'error']),
37+
bucket: z.string().optional(),
38+
key: z.string().optional(),
39+
region: z.string().optional(),
40+
error: errorSchema.optional(),
41+
}),
42+
});
43+
44+
export type DataPart = z.infer<typeof dataPartSchema>;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import z from 'zod/v3'
2+
3+
export const metadataSchema = z.object({
4+
model: z.string(),
5+
})
6+
7+
export type Metadata = z.infer<typeof metadataSchema>

apps/app/src/ai/secrets.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Central registry for standard secrets used by automations and tools.
2+
// AI-friendly design: a flat list with simple fields and helper lookups.
3+
4+
export type SecretProvider = 'github';
5+
6+
export interface SecretEntry {
7+
id: string; // stable identifier, e.g. 'github.token'
8+
provider: SecretProvider;
9+
name: string; // short name within provider, e.g. 'token'
10+
envVar: string; // environment variable name, e.g. 'GITHUB_TOKEN'
11+
description: string;
12+
required: boolean;
13+
docsUrl?: string;
14+
aliases?: readonly string[]; // additional phrases an AI/user might use
15+
}
16+
17+
export const SECRETS: readonly SecretEntry[] = [
18+
{
19+
id: 'github.token',
20+
provider: 'github',
21+
name: 'token',
22+
envVar: 'GITHUB_TOKEN',
23+
description:
24+
'GitHub token (PAT or App installation token) with read access to repository contents and metadata.',
25+
required: true,
26+
docsUrl:
27+
'https://docs.github.com/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token',
28+
aliases: ['github token', 'gh token', 'github_pat', 'github personal access token'],
29+
},
30+
] as const;
31+
32+
// Lightweight indexes for fast lookup
33+
const SECRET_BY_ID: Readonly<Record<string, SecretEntry>> = Object.freeze(
34+
Object.fromEntries(SECRETS.map((s) => [s.id, s])),
35+
);
36+
37+
const SECRET_BY_ENV: Readonly<Record<string, SecretEntry>> = Object.freeze(
38+
Object.fromEntries(SECRETS.map((s) => [s.envVar.toUpperCase(), s])),
39+
);
40+
41+
export function listSecrets(): readonly SecretEntry[] {
42+
return SECRETS;
43+
}
44+
45+
export function listProviderSecrets(provider: SecretProvider): readonly SecretEntry[] {
46+
return SECRETS.filter((s) => s.provider === provider);
47+
}
48+
49+
export function getSecretById(id: string): SecretEntry | undefined {
50+
return SECRET_BY_ID[id];
51+
}
52+
53+
export function getSecretByEnvVar(envVar: string): SecretEntry | undefined {
54+
return SECRET_BY_ENV[envVar.toUpperCase()];
55+
}
56+
57+
export function getEnvVarNameById(id: string): string | undefined {
58+
return getSecretById(id)?.envVar;
59+
}
60+
61+
// Flexible resolver that accepts: id, env var, provider.name, or an alias phrase
62+
export function resolveSecretIdentifier(identifier: string): SecretEntry | undefined {
63+
const raw = identifier.trim();
64+
if (!raw) return undefined;
65+
66+
// Exact id
67+
const byId = getSecretById(raw);
68+
if (byId) return byId;
69+
70+
// Exact env var
71+
const byEnv = getSecretByEnvVar(raw);
72+
if (byEnv) return byEnv;
73+
74+
const normalized = raw.toLowerCase().replace(/\s+/g, ' ').trim();
75+
76+
// provider.name form
77+
const dotIdx = normalized.indexOf('.');
78+
if (dotIdx > 0) {
79+
const provider = normalized.slice(0, dotIdx);
80+
const name = normalized.slice(dotIdx + 1);
81+
const match = SECRETS.find(
82+
(s) => s.provider === (provider as SecretProvider) && s.name.toLowerCase() === name,
83+
);
84+
if (match) return match;
85+
}
86+
87+
// Alias match
88+
const byAlias = SECRETS.find((s) =>
89+
(s.aliases ?? []).some((a) => a.toLowerCase() === normalized),
90+
);
91+
if (byAlias) return byAlias;
92+
93+
// Provider-keyword fallback: e.g., 'github token'
94+
const byTokens = SECRETS.find(
95+
(s) => normalized.includes(s.provider) && normalized.includes(s.name.toLowerCase()),
96+
);
97+
return byTokens;
98+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
Use this tool to create a new Vercel Sandbox — an ephemeral, isolated Linux container that serves as your development environment for the current session. This sandbox provides a secure workspace where you can upload files, install dependencies, run commands, start development servers, and preview web apps. Each sandbox is uniquely identified and must be referenced for all subsequent operations (e.g., file generation, command execution, or URL access).
2+
3+
## When to Use This Tool
4+
5+
Use this tool **once per session** when:
6+
7+
1. You begin working on a new user request that requires code execution or file creation
8+
2. No sandbox currently exists for the session
9+
3. The user asks to start a new project, scaffold an application, or test code in a live environment
10+
4. The user requests a fresh or reset environment
11+
12+
## Sandbox Capabilities
13+
14+
After creation, the sandbox allows you to:
15+
16+
- Upload and manage files via `Generate Files`
17+
- Execute shell commands with `Run Command` and `Wait Command`
18+
- Access running servers through public URLs using `Get Sandbox URL`
19+
20+
Each sandbox mimics a real-world development environment and supports rapid iteration and testing without polluting the local system. The base system is Amazon Linux 2023 with the following additional packages:
21+
22+
```
23+
bind-utils bzip2 findutils git gzip iputils libicu libjpeg libpng ncurses-libs openssl openssl-libs pnpm procps tar unzip which whois zstd
24+
```
25+
26+
You can install additional packages using the `dnf` package manager. You can NEVER use port 8080 as it is reserved for internal applications. When requested, you need to use a different port.
27+
28+
## Best Practices
29+
30+
- Create the sandbox at the beginning of the session or when the user initiates a coding task
31+
- Track and reuse the sandbox ID throughout the session
32+
- Do not create a second sandbox unless explicitly instructed
33+
- If the user requests an environment reset, you may create a new sandbox **after confirming their intent**
34+
35+
## Examples of When to Use This Tool
36+
37+
<example>
38+
User: Can we start fresh? I want to rebuild the project from scratch.
39+
Assistant: Got it — I’ll create a new sandbox so we can start clean.
40+
*Calls Create Sandbox*
41+
</example>
42+
43+
## When NOT to Use This Tool
44+
45+
Skip using this tool when:
46+
47+
1. A sandbox has already been created for the current session
48+
2. You only need to upload files (use Generate Files)
49+
3. You want to execute or wait for a command (use Run Command / Wait Command)
50+
4. You want to preview the application (use Get Sandbox URL)
51+
5. The user hasn’t asked to reset the environment
52+
53+
## Summary
54+
55+
Use Create Sandbox to initialize a secure, temporary development environment — but **only once per session**. Treat the sandbox as the core workspace for all follow-up actions unless the user explicitly asks to discard and start anew.

0 commit comments

Comments
 (0)