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
7 changes: 7 additions & 0 deletions packages/api/src/data/repositories/studios.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface Studio {
workType: string | null;
slug: string | null;
roleTemplate: string | null;
defaultProjectId: string | null;
status: StudioStatus;
metadata: Json;
createdAt: string;
Expand All @@ -49,6 +50,7 @@ export interface CreateStudioInput {
purpose?: string;
workType?: WorkType;
roleTemplate?: string;
defaultProjectId?: string | null;
metadata?: Json;
}

Expand All @@ -60,6 +62,7 @@ export interface UpdateStudioInput {
roleTemplate?: string | null;
worktreePath?: string;
slug?: string | null;
defaultProjectId?: string | null;
metadata?: Json;
archivedAt?: string;
cleanedAt?: string;
Expand Down Expand Up @@ -95,6 +98,7 @@ export class StudiosRepository {
workType: (row.work_type as string) || null,
slug: (row.slug as string) || null,
roleTemplate: (row.role_template as string) || null,
defaultProjectId: (row.default_project_id as string) || null,
status: row.status as StudioStatus,
metadata: (row.metadata as Json) || {},
createdAt: row.created_at as string,
Expand All @@ -121,6 +125,7 @@ export class StudiosRepository {
purpose: input.purpose,
work_type: input.workType,
role_template: input.roleTemplate,
default_project_id: input.defaultProjectId ?? null,
slug: deriveStudioSlug(input.worktreePath),
status: 'active',
metadata: input.metadata || {},
Expand Down Expand Up @@ -276,6 +281,8 @@ export class StudiosRepository {
if (input.roleTemplate !== undefined) updateData.role_template = input.roleTemplate;
if (input.worktreePath !== undefined) updateData.worktree_path = input.worktreePath;
if (input.slug !== undefined) updateData.slug = input.slug;
if (input.defaultProjectId !== undefined)
updateData.default_project_id = input.defaultProjectId;
if (input.metadata !== undefined) updateData.metadata = input.metadata;
if (input.archivedAt !== undefined) updateData.archived_at = input.archivedAt;
if (input.cleanedAt !== undefined) updateData.cleaned_at = input.cleanedAt;
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/data/supabase/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3054,6 +3054,7 @@ export type Database = {
branch: string;
cleaned_at: string | null;
created_at: string | null;
default_project_id: string | null;
id: string;
metadata: Json | null;
permissions: Json;
Expand All @@ -3078,6 +3079,7 @@ export type Database = {
branch: string;
cleaned_at?: string | null;
created_at?: string | null;
default_project_id?: string | null;
id?: string;
metadata?: Json | null;
permissions?: Json;
Expand All @@ -3102,6 +3104,7 @@ export type Database = {
branch?: string;
cleaned_at?: string | null;
created_at?: string | null;
default_project_id?: string | null;
id?: string;
metadata?: Json | null;
permissions?: Json;
Expand Down
43 changes: 41 additions & 2 deletions packages/api/src/mcp/tools/studio-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ const createStudioSchema = userIdentifierBaseSchema.extend({
.string()
.optional()
.describe('Role template name used when creating the studio (e.g., "reviewer", "builder")'),
defaultProjectId: z
.string()
.uuid()
.optional()
.describe('Default project ID for task groups created in this studio'),
skipGitOperations: z
.boolean()
.optional()
Expand Down Expand Up @@ -113,6 +118,12 @@ const updateStudioSchema = userIdentifierBaseSchema.extend({
.boolean()
.optional()
.describe('If true, unlink the current session and set status to idle'),
defaultProjectId: z
.string()
.uuid()
.nullable()
.optional()
.describe('Default project ID for task groups created in this studio. Set to null to clear.'),
routePatterns: z
.array(z.string().max(200))
.optional()
Expand Down Expand Up @@ -191,9 +202,17 @@ export async function handleCreateStudio(args: unknown, dataComposer: DataCompos
baseBranch = 'main',
sessionId,
roleTemplate,
defaultProjectId,
skipGitOperations = false,
} = parsed;

if (defaultProjectId) {
const project = await dataComposer.repositories.projects.findById(defaultProjectId);
if (!project || project.user_id !== resolved.user.id) {
return errorResponse('defaultProjectId not found or does not belong to this user');
}
}

// Resolve to the main worktree root (handles case where repoRoot is a linked worktree)
const mainRoot = resolveMainWorktree(repoRoot);

Expand Down Expand Up @@ -251,6 +270,7 @@ export async function handleCreateStudio(args: unknown, dataComposer: DataCompos
purpose,
workType,
roleTemplate,
defaultProjectId,
});
} catch (dbError) {
// If DB insert fails but git succeeded, attempt cleanup
Expand Down Expand Up @@ -293,6 +313,7 @@ export async function handleCreateStudio(args: unknown, dataComposer: DataCompos
purpose: studio.purpose,
workType: studio.workType,
roleTemplate: studio.roleTemplate,
defaultProjectId: studio.defaultProjectId,
status: studio.status,
sessionId: studio.sessionId,
createdAt: studio.createdAt,
Expand Down Expand Up @@ -339,6 +360,7 @@ export async function handleListStudios(args: unknown, dataComposer: DataCompose
status: w.status,
workType: w.workType,
roleTemplate: w.roleTemplate,
defaultProjectId: w.defaultProjectId,
hasLinkedSession: !!w.sessionId,
createdAt: w.createdAt,
})),
Expand Down Expand Up @@ -381,6 +403,7 @@ export async function handleGetStudio(args: unknown, dataComposer: DataComposer)
purpose: studio.purpose,
workType: studio.workType,
roleTemplate: studio.roleTemplate,
defaultProjectId: studio.defaultProjectId,
status: studio.status,
sessionId: studio.sessionId,
metadata: studio.metadata,
Expand All @@ -394,7 +417,7 @@ export async function handleGetStudio(args: unknown, dataComposer: DataComposer)

export async function handleUpdateStudio(args: unknown, dataComposer: DataComposer) {
const parsed = updateStudioSchema.parse(args);
await resolveUserOrThrow(parsed, dataComposer);
const resolved = await resolveUserOrThrow(parsed, dataComposer);

const {
studioId,
Expand All @@ -406,15 +429,26 @@ export async function handleUpdateStudio(args: unknown, dataComposer: DataCompos
slug,
sessionId,
unlinkSession,
defaultProjectId,
routePatterns,
} = parsed;
const studiosRepo = dataComposer.repositories.studios;

// Verify studio exists
// Verify studio exists and belongs to this user
const existing = await studiosRepo.findById(studioId);
if (!existing) {
return errorResponse(`Studio not found: ${studioId}`);
}
if (existing.userId !== resolved.user.id) {
return errorResponse(`Studio not found: ${studioId}`);
}

if (typeof defaultProjectId === 'string') {
Comment thread
conoremclaughlin marked this conversation as resolved.
const project = await dataComposer.repositories.projects.findById(defaultProjectId);
if (!project || project.user_id !== resolved.user.id) {
return errorResponse('defaultProjectId not found or does not belong to this user');
}
}

let updated;

Expand All @@ -439,6 +473,9 @@ export async function handleUpdateStudio(args: unknown, dataComposer: DataCompos
if (slug !== undefined) {
updateObj.slug = slug;
}
if (defaultProjectId !== undefined) {
updateObj.defaultProjectId = defaultProjectId;
}
if (routePatterns !== undefined) {
updateObj.routePatterns = routePatterns;
}
Expand All @@ -458,6 +495,7 @@ export async function handleUpdateStudio(args: unknown, dataComposer: DataCompos
worktreePath: updated.worktreePath,
purpose: updated.purpose,
roleTemplate: updated.roleTemplate,
defaultProjectId: updated.defaultProjectId,
status: updated.status,
sessionId: updated.sessionId,
updatedAt: updated.updatedAt,
Expand Down Expand Up @@ -599,6 +637,7 @@ export async function handleAdoptStudio(args: unknown, dataComposer: DataCompose
worktreePath: updated.worktreePath,
purpose: updated.purpose,
roleTemplate: updated.roleTemplate,
defaultProjectId: updated.defaultProjectId,
status: updated.status,
sessionId: updated.sessionId,
updatedAt: updated.updatedAt,
Expand Down
Loading
Loading