170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
import { v } from 'convex/values';
|
|
|
|
import type { Doc } from './_generated/dataModel';
|
|
import { mutation, query } from './_generated/server';
|
|
import { getOwnedSpoon, getRequiredUserId, optionalText } from './model';
|
|
|
|
const reasoningEffort = v.union(
|
|
v.literal('none'),
|
|
v.literal('minimal'),
|
|
v.literal('low'),
|
|
v.literal('medium'),
|
|
v.literal('high'),
|
|
v.literal('xhigh'),
|
|
);
|
|
|
|
const runtime = v.literal('opencode');
|
|
|
|
const envFilePath = v.union(
|
|
v.literal('.env'),
|
|
v.literal('.env.local'),
|
|
v.literal('.env.production'),
|
|
v.literal('.env.production.local'),
|
|
v.literal('custom'),
|
|
);
|
|
|
|
const defaults = {
|
|
enabled: true,
|
|
runtime: 'opencode' as const,
|
|
branchPrefix: 'spoon/agent',
|
|
agentModel: '',
|
|
reasoningEffort: 'medium' as const,
|
|
maxJobDurationMs: 1_800_000,
|
|
maxOutputBytes: 200_000,
|
|
envFilePath: '.env.local' as const,
|
|
materializeEnvFileByDefault: false,
|
|
autoDetectCommands: true,
|
|
allowUserFileEditing: true,
|
|
};
|
|
|
|
export const getForSpoon = query({
|
|
args: { spoonId: v.id('spoons') },
|
|
handler: async (ctx, { spoonId }) => {
|
|
const ownerId = await getRequiredUserId(ctx);
|
|
const spoon = await getOwnedSpoon(ctx, spoonId, ownerId);
|
|
const settings = await ctx.db
|
|
.query('spoonAgentSettings')
|
|
.withIndex('by_spoon', (q) => q.eq('spoonId', spoonId))
|
|
.first();
|
|
return (
|
|
settings ?? {
|
|
spoonId,
|
|
ownerId,
|
|
...defaults,
|
|
defaultBaseBranch:
|
|
spoon.forkDefaultBranch ?? spoon.upstreamDefaultBranch,
|
|
}
|
|
);
|
|
},
|
|
});
|
|
|
|
export const update = mutation({
|
|
args: {
|
|
spoonId: v.id('spoons'),
|
|
enabled: v.optional(v.boolean()),
|
|
defaultBaseBranch: v.optional(v.string()),
|
|
branchPrefix: v.optional(v.string()),
|
|
installCommand: v.optional(v.string()),
|
|
checkCommand: v.optional(v.string()),
|
|
testCommand: v.optional(v.string()),
|
|
runtime: v.optional(runtime),
|
|
agentModel: v.optional(v.string()),
|
|
reasoningEffort: v.optional(reasoningEffort),
|
|
maxJobDurationMs: v.optional(v.number()),
|
|
maxOutputBytes: v.optional(v.number()),
|
|
envFilePath: v.optional(envFilePath),
|
|
customEnvFilePath: v.optional(v.string()),
|
|
materializeEnvFileByDefault: v.optional(v.boolean()),
|
|
autoDetectCommands: v.optional(v.boolean()),
|
|
allowUserFileEditing: v.optional(v.boolean()),
|
|
aiProviderProfileId: v.optional(v.id('aiProviderProfiles')),
|
|
clearAiProviderProfile: v.optional(v.boolean()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const ownerId = await getRequiredUserId(ctx);
|
|
const spoon = await getOwnedSpoon(ctx, args.spoonId, ownerId);
|
|
const now = Date.now();
|
|
let settings = await ctx.db
|
|
.query('spoonAgentSettings')
|
|
.withIndex('by_spoon', (q) => q.eq('spoonId', args.spoonId))
|
|
.first();
|
|
|
|
if (!settings) {
|
|
const id = await ctx.db.insert('spoonAgentSettings', {
|
|
spoonId: args.spoonId,
|
|
ownerId,
|
|
...defaults,
|
|
defaultBaseBranch:
|
|
spoon.forkDefaultBranch ?? spoon.upstreamDefaultBranch,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
settings = await ctx.db.get(id);
|
|
}
|
|
|
|
if (!settings) throw new Error('Agent settings not found.');
|
|
|
|
const patch: Partial<Doc<'spoonAgentSettings'>> = { updatedAt: now };
|
|
if (args.enabled !== undefined) patch.enabled = args.enabled;
|
|
if (args.defaultBaseBranch !== undefined) {
|
|
patch.defaultBaseBranch = optionalText(args.defaultBaseBranch);
|
|
}
|
|
if (args.branchPrefix !== undefined) {
|
|
patch.branchPrefix =
|
|
optionalText(args.branchPrefix) ?? defaults.branchPrefix;
|
|
}
|
|
if (args.installCommand !== undefined) {
|
|
patch.installCommand = optionalText(args.installCommand);
|
|
}
|
|
if (args.checkCommand !== undefined) {
|
|
patch.checkCommand = optionalText(args.checkCommand);
|
|
}
|
|
if (args.testCommand !== undefined) {
|
|
patch.testCommand = optionalText(args.testCommand);
|
|
}
|
|
if (args.runtime !== undefined) {
|
|
patch.runtime = 'opencode';
|
|
}
|
|
if (args.agentModel !== undefined) {
|
|
patch.agentModel = optionalText(args.agentModel) ?? defaults.agentModel;
|
|
}
|
|
if (args.reasoningEffort !== undefined) {
|
|
patch.reasoningEffort = args.reasoningEffort;
|
|
}
|
|
if (args.maxJobDurationMs !== undefined) {
|
|
patch.maxJobDurationMs = Math.max(60_000, args.maxJobDurationMs);
|
|
}
|
|
if (args.maxOutputBytes !== undefined) {
|
|
patch.maxOutputBytes = Math.max(10_000, args.maxOutputBytes);
|
|
}
|
|
if (args.envFilePath !== undefined) {
|
|
patch.envFilePath = args.envFilePath;
|
|
}
|
|
if (args.customEnvFilePath !== undefined) {
|
|
patch.customEnvFilePath = optionalText(args.customEnvFilePath);
|
|
}
|
|
if (args.materializeEnvFileByDefault !== undefined) {
|
|
patch.materializeEnvFileByDefault = args.materializeEnvFileByDefault;
|
|
}
|
|
if (args.autoDetectCommands !== undefined) {
|
|
patch.autoDetectCommands = args.autoDetectCommands;
|
|
}
|
|
if (args.allowUserFileEditing !== undefined) {
|
|
patch.allowUserFileEditing = args.allowUserFileEditing;
|
|
}
|
|
if (args.aiProviderProfileId !== undefined) {
|
|
const profile = await ctx.db.get(args.aiProviderProfileId);
|
|
if (profile?.ownerId !== ownerId) {
|
|
throw new Error('AI provider profile not found.');
|
|
}
|
|
patch.aiProviderProfileId = args.aiProviderProfileId;
|
|
}
|
|
if (args.clearAiProviderProfile) {
|
|
patch.aiProviderProfileId = undefined;
|
|
}
|
|
|
|
await ctx.db.patch(settings._id, patch);
|
|
return { success: true };
|
|
},
|
|
});
|