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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ CASCADE stores all project configuration in PostgreSQL (Supabase). The `config/p
- `organizations` - Organization definitions (multi-tenant support)
- `cascade_defaults` - Global defaults per org (model, iterations, timeouts, budget)
- `projects` - Per-project config (repo, base branch, budget, backend)
- `project_integrations` - Integration configs per project with `category` (pm/scm), `provider` (trello/jira/github), `config` JSONB, and `triggers` JSONB. One PM + one SCM per project (enforced by unique constraint)
- `integration_credentials` - Links integration roles to org-scoped credential rows (e.g., `api_key` → credential #5). Roles are provider-specific: trello has `api_key`/`token`, jira has `email`/`api_token`, github has `implementer_token`/`reviewer_token`
- `project_integrations` - Integration configs per project with `category` (pm/scm/email/sms), `provider` (trello/jira/github/imap/gmail/twilio), `config` JSONB, and `triggers` JSONB. One PM + one SCM per project (enforced by unique constraint)
- `integration_credentials` - Links integration roles to org-scoped credential rows (e.g., `api_key` → credential #5). Roles are provider-specific: trello has `api_key`/`token`, jira has `email`/`api_token`, github has `implementer_token`/`reviewer_token`, twilio has `account_sid`/`auth_token`/`phone_number`
- `agent_configs` - Per-agent-type overrides (model, iterations, backend, prompt), scoped globally, per-org, or per-project
- `credentials` - Org-scoped credentials (API keys, tokens)
- `users` - Dashboard users (email, bcrypt password hash, org-scoped)
Expand Down Expand Up @@ -193,6 +193,23 @@ const openrouterKey = await getOrgCredential(projectId, 'OPENROUTER_API_KEY');

Role definitions and env-var-key mappings are in `src/config/integrationRoles.ts`.

### Twilio SMS Integration

CASCADE supports sending and receiving SMS via Twilio. Configure per-project in the dashboard (Project Settings > Integrations > SMS tab) or CLI:

```bash
cascade credentials create --name "Twilio Account SID" --key TWILIO_ACCOUNT_SID --value ACxxx... --default
cascade credentials create --name "Twilio Auth Token" --key TWILIO_AUTH_TOKEN --value xxx... --default
cascade credentials create --name "Twilio Phone Number" --key TWILIO_PHONE_NUMBER --value +15550000001 --default
cascade projects integration-credential-set <project-id> --category sms --role account_sid --credential-id 5
cascade projects integration-credential-set <project-id> --category sms --role auth_token --credential-id 6
cascade projects integration-credential-set <project-id> --category sms --role phone_number --credential-id 7
```

**Inbound webhook**: `POST /twilio/webhook/:projectId` — configure this URL in the Twilio console under *Phone Numbers → Manage → Active Numbers → [your number] → Messaging → A Message Comes In*. The handler validates the Twilio signature and logs incoming messages (agent triggering will be added with a future `sms-responder` agent).

**Outbound SMS**: Agents use the `SendSms` gadget. SMS credentials are scoped automatically during agent execution (mirrors email integration).

### Review Agent Trigger Modes

The review agent supports three independent trigger modes via the `reviewTrigger` config in the SCM integration triggers. **All modes default to `false`** — existing behavior is preserved via a legacy fallback.
Expand Down
125 changes: 124 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"open": "^11.0.0",
"pg": "^8.18.0",
"trello.js": "^1.2.8",
"twilio": "^5.12.2",
"zangief": "latest",
"zod": "^3.24.1"
},
Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { z } from 'zod';
// ============================================================================

// Integration categories (aligned with integrationRoles.ts)
export const IntegrationCategorySchema = z.enum(['pm', 'scm', 'email']);
export const IntegrationCategorySchema = z.enum(['pm', 'scm', 'email', 'sms']);

// Integration requirements schema (REQUIRED field)
const IntegrationsSchema = z
Expand Down
31 changes: 31 additions & 0 deletions src/api/routers/integrationsDiscovery.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TRPCError } from '@trpc/server';
import { and, eq } from 'drizzle-orm';
import { ImapFlow } from 'imapflow';
import twilio from 'twilio';
import { z } from 'zod';
import { getDb } from '../../db/client.js';
import { decryptCredential, encryptCredential } from '../../db/crypto.js';
Expand Down Expand Up @@ -472,6 +473,36 @@ export const integrationsDiscoveryRouter = router({
}
}),

/**
* Verify Twilio credentials by fetching the account details.
*/
verifyTwilio: protectedProcedure
.input(
z.object({
accountSidCredentialId: z.number(),
authTokenCredentialId: z.number(),
}),
)
.mutation(async ({ ctx, input }) => {
logger.debug('integrationsDiscovery.verifyTwilio called', { orgId: ctx.effectiveOrgId });

const [accountSid, authToken] = await Promise.all([
resolveCredentialValue(input.accountSidCredentialId, ctx.effectiveOrgId),
resolveCredentialValue(input.authTokenCredentialId, ctx.effectiveOrgId),
]);

try {
const client = twilio(accountSid, authToken);
const account = await client.api.accounts(accountSid).fetch();
return { friendlyName: account.friendlyName, status: account.status };
} catch (err) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `Failed to verify Twilio credentials: ${err instanceof Error ? err.message : String(err)}`,
});
}
}),

/**
* Verify IMAP connection with password auth.
*/
Expand Down
12 changes: 6 additions & 6 deletions src/api/routers/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const projectsRouter = router({
.input(
z.object({
projectId: z.string(),
category: z.enum(['pm', 'scm', 'email']),
category: z.enum(['pm', 'scm', 'email', 'sms']),
provider: z.string().min(1),
config: z.record(z.unknown()),
triggers: z.record(z.boolean()).optional(),
Expand All @@ -142,7 +142,7 @@ export const projectsRouter = router({
.input(
z.object({
projectId: z.string(),
category: z.enum(['pm', 'scm', 'email']),
category: z.enum(['pm', 'scm', 'email', 'sms']),
triggers: z.record(z.union([z.boolean(), z.string().nullable(), z.record(z.boolean())])),
}),
)
Expand All @@ -152,7 +152,7 @@ export const projectsRouter = router({
}),

delete: protectedProcedure
.input(z.object({ projectId: z.string(), category: z.enum(['pm', 'scm']) }))
.input(z.object({ projectId: z.string(), category: z.enum(['pm', 'scm', 'email', 'sms']) }))
.mutation(async ({ ctx, input }) => {
await verifyProjectOwnership(input.projectId, ctx.effectiveOrgId);
await deleteProjectIntegration(input.projectId, input.category);
Expand All @@ -162,7 +162,7 @@ export const projectsRouter = router({
// Integration Credentials
integrationCredentials: router({
list: protectedProcedure
.input(z.object({ projectId: z.string(), category: z.enum(['pm', 'scm', 'email']) }))
.input(z.object({ projectId: z.string(), category: z.enum(['pm', 'scm', 'email', 'sms']) }))
.query(async ({ ctx, input }) => {
await verifyProjectOwnership(input.projectId, ctx.effectiveOrgId);
const integration = await getIntegrationByProjectAndCategory(
Expand All @@ -177,7 +177,7 @@ export const projectsRouter = router({
.input(
z.object({
projectId: z.string(),
category: z.enum(['pm', 'scm', 'email']),
category: z.enum(['pm', 'scm', 'email', 'sms']),
role: z.string().min(1),
credentialId: z.number(),
}),
Expand All @@ -202,7 +202,7 @@ export const projectsRouter = router({
.input(
z.object({
projectId: z.string(),
category: z.enum(['pm', 'scm', 'email']),
category: z.enum(['pm', 'scm', 'email', 'sms']),
role: z.string().min(1),
}),
)
Expand Down
6 changes: 3 additions & 3 deletions src/cli/dashboard/projects/override-rm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export default class ProjectsIntegrationCredentialRm extends DashboardCommand {
static override flags = {
...DashboardCommand.baseFlags,
category: Flags.string({
description: 'Integration category (pm, scm, or email)',
description: 'Integration category (pm, scm, email, or sms)',
required: true,
options: ['pm', 'scm', 'email'],
options: ['pm', 'scm', 'email', 'sms'],
}),
role: Flags.string({
description: 'Credential role to unlink (e.g. api_key, token, implementer_token)',
Expand All @@ -29,7 +29,7 @@ export default class ProjectsIntegrationCredentialRm extends DashboardCommand {
try {
await this.client.projects.integrationCredentials.remove.mutate({
projectId: args.id,
category: flags.category as 'pm' | 'scm' | 'email',
category: flags.category as 'pm' | 'scm' | 'email' | 'sms',
role: flags.role,
});

Expand Down
6 changes: 3 additions & 3 deletions src/cli/dashboard/projects/override-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export default class ProjectsIntegrationCredentialSet extends DashboardCommand {
static override flags = {
...DashboardCommand.baseFlags,
category: Flags.string({
description: 'Integration category (pm, scm, or email)',
description: 'Integration category (pm, scm, email, or sms)',
required: true,
options: ['pm', 'scm', 'email'],
options: ['pm', 'scm', 'email', 'sms'],
}),
role: Flags.string({
description: 'Credential role (e.g. api_key, token, implementer_token)',
Expand All @@ -30,7 +30,7 @@ export default class ProjectsIntegrationCredentialSet extends DashboardCommand {
try {
await this.client.projects.integrationCredentials.set.mutate({
projectId: args.id,
category: flags.category as 'pm' | 'scm' | 'email',
category: flags.category as 'pm' | 'scm' | 'email' | 'sms',
role: flags.role,
credentialId: flags['credential-id'],
});
Expand Down
8 changes: 4 additions & 4 deletions src/cli/dashboard/projects/overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export default class ProjectsIntegrationCredentials extends DashboardCommand {
static override flags = {
...DashboardCommand.baseFlags,
category: Flags.string({
description: 'Filter by integration category (pm, scm, or email)',
options: ['pm', 'scm', 'email'],
description: 'Filter by integration category (pm, scm, email, or sms)',
options: ['pm', 'scm', 'email', 'sms'],
}),
};

Expand All @@ -23,8 +23,8 @@ export default class ProjectsIntegrationCredentials extends DashboardCommand {

try {
const categories = flags.category
? [flags.category as 'pm' | 'scm' | 'email']
: (['pm', 'scm', 'email'] as const);
? [flags.category as 'pm' | 'scm' | 'email' | 'sms']
: (['pm', 'scm', 'email', 'sms'] as const);

const allCreds: Array<Record<string, unknown>> = [];

Expand Down
Loading