Skip to content
Open
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
10 changes: 10 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"ignorePatterns": ["dist/**", "node_modules/**", "convex/_generated/**"]
}
15 changes: 15 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"unicorn",
"typescript",
"oxc",
"react",
"jsx-a11y",
"react-perf",
"promise",
"node",
"vitest"
],
"ignorePatterns": ["dist/**", "node_modules/**", "convex/_generated/**"]
}
544 changes: 453 additions & 91 deletions bun.lock

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions convex/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const checkInvite = query({
.withIndex("by_code", (q) => q.eq("code", code))
.unique();
if (!invite || !inviteIsUsable(invite)) return { valid: false as const };
const orgId = invite.orgId ?? await getProductOrgId(ctx);
const orgId = invite.orgId ?? (await getProductOrgId(ctx));
const org = await ctx.db.get(orgId);
if (!org) return { valid: false as const };
return { valid: true as const, note: invite.note, orgName: org.name };
Expand All @@ -61,7 +61,7 @@ export const redeemInvite = mutation({
message: "That invite code is not valid anymore.",
});
}
const targetOrgId = invite.orgId ?? await getProductOrgId(ctx);
const targetOrgId = invite.orgId ?? (await getProductOrgId(ctx));
if (viewer.orgId && viewer.orgId !== targetOrgId) {
throw new ConvexError({
code: "FORBIDDEN",
Expand Down Expand Up @@ -117,15 +117,13 @@ export const claimTargetedInvite = mutation({
: await ctx.db
.query("invites")
.withIndex("by_target", (q) =>
q
.eq("targetKind", candidate.kind)
.eq("targetValue", candidate.value),
q.eq("targetKind", candidate.kind).eq("targetValue", candidate.value),
)
.collect();
const invite = invites.find(inviteIsUsable);
if (!invite) continue;

const orgId = invite.orgId ?? await getProductOrgId(ctx);
const orgId = invite.orgId ?? (await getProductOrgId(ctx));
await ctx.db.patch(invite._id, { usedCount: invite.usedCount + 1 });
await ctx.db.patch(viewer._id, { orgId, status: "active" });
await logAudit(ctx, {
Expand Down
36 changes: 8 additions & 28 deletions convex/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@ import { ConvexError, v } from "convex/values";
import { mutation, query } from "./_generated/server";
import type { Doc, Id } from "./_generated/dataModel";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import {
ensureActiveViewerUser,
getViewerFromAuth,
} from "./authUsers";
import { ensureActiveViewerUser, getViewerFromAuth } from "./authUsers";
import { publicUser } from "./users";
import { parseInviteTarget, type InviteTarget } from "./lib/inviteTargets";
import { logInfo } from "./lib/observability";
import { parse, profileTitleSchema } from "./lib/validation";
import { aiGenerationKind } from "./schema";
import {
DEFAULT_OPENROUTER_MODEL,
normalizeModelId,
type AiGenerationKind,
} from "./lib/aiModels";
import { DEFAULT_OPENROUTER_MODEL, normalizeModelId, type AiGenerationKind } from "./lib/aiModels";

/**
* Admin control plane — users, invites, access requests, audit history.
Expand Down Expand Up @@ -137,10 +130,7 @@ export const listUsers = query({
},
});

const AI_GENERATION_KINDS: readonly AiGenerationKind[] = [
"postSummary",
"agentTask",
];
const AI_GENERATION_KINDS: readonly AiGenerationKind[] = ["postSummary", "agentTask"];

export const aiModelSettings = query({
args: {},
Expand Down Expand Up @@ -169,9 +159,7 @@ export const aiModelSettings = query({
effectiveModelId:
setting?.modelId ?? process.env.OPENROUTER_MODEL ?? DEFAULT_OPENROUTER_MODEL,
updatedAt: setting?.updatedAt ?? null,
updatedByName: setting
? (updaterNames.get(setting.updatedById) ?? "unknown")
: null,
updatedByName: setting ? (updaterNames.get(setting.updatedById) ?? "unknown") : null,
};
}),
};
Expand All @@ -185,9 +173,7 @@ export const setAiModelSetting = mutation({
const modelId = normalizeModelId(args.modelId);
const existing = await ctx.db
.query("aiGenerationSettings")
.withIndex("by_org_id_and_kind", (q) =>
q.eq("orgId", admin.orgId).eq("kind", args.kind),
)
.withIndex("by_org_id_and_kind", (q) => q.eq("orgId", admin.orgId).eq("kind", args.kind))
.first();
const now = Date.now();
if (existing) {
Expand Down Expand Up @@ -224,9 +210,7 @@ export const resetAiModelSetting = mutation({
const admin = await requireAdminForWrite(ctx);
const existing = await ctx.db
.query("aiGenerationSettings")
.withIndex("by_org_id_and_kind", (q) =>
q.eq("orgId", admin.orgId).eq("kind", args.kind),
)
.withIndex("by_org_id_and_kind", (q) => q.eq("orgId", admin.orgId).eq("kind", args.kind))
.collect();
await Promise.all(existing.map((setting) => ctx.db.delete(setting._id)));
await logAudit(ctx, {
Expand Down Expand Up @@ -290,9 +274,7 @@ export const listAccessRequests = query({
const admin = await requireAdminForRead(ctx);
const requests = await ctx.db
.query("accessRequests")
.withIndex("by_org_id_and_status_and_created_at", (q) =>
q.eq("orgId", admin.orgId),
)
.withIndex("by_org_id_and_status_and_created_at", (q) => q.eq("orgId", admin.orgId))
.collect();
return requests.sort((a, b) => b.createdAt - a.createdAt);
},
Expand Down Expand Up @@ -372,9 +354,7 @@ export const createInvite = mutation({
});
}
// Targeted invites admit exactly the one person.
const maxUses = target
? 1
: Math.min(Math.max(Math.floor(args.maxUses ?? 1), 0), 1000);
const maxUses = target ? 1 : Math.min(Math.max(Math.floor(args.maxUses ?? 1), 0), 1000);
const note = args.note?.trim().slice(0, 200) || undefined;
const expiresAt =
args.expiresInDays && args.expiresInDays > 0
Expand Down
69 changes: 29 additions & 40 deletions convex/agentTasks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { generateText } from "ai";
import { ConvexError, v } from "convex/values";
import { internalAction, internalMutation, internalQuery, mutation, query } from "./_generated/server";
import {
internalAction,
internalMutation,
internalQuery,
mutation,
query,
} from "./_generated/server";
import { internal } from "./_generated/api";
import type { Doc, Id } from "./_generated/dataModel";
import type { ActionCtx } from "./_generated/server";
Expand Down Expand Up @@ -29,13 +35,10 @@ async function generateAgentResult(args: {
prompt: string;
contextText: string;
}): Promise<AgentResult> {
const openRouterModelId = await args.ctx.runQuery(
internal.ai.getGenerationModelSetting,
{
orgId: args.orgId,
kind: "agentTask",
},
);
const openRouterModelId = await args.ctx.runQuery(internal.ai.getGenerationModelSetting, {
orgId: args.orgId,
kind: "agentTask",
});

// Demo fallback: when no AI provider is configured on the deployment, the
// action returns a disabled signal instead of throwing — so the client shows
Expand All @@ -51,8 +54,7 @@ async function generateAgentResult(args: {

// agentName is interpolated into the system prompt — collapse whitespace and
// cap length so it can't carry injected multi-line instructions.
const agentName =
args.agentName.replace(/\s+/g, " ").trim().slice(0, 60) || "an agent";
const agentName = args.agentName.replace(/\s+/g, " ").trim().slice(0, 60) || "an agent";
const { model, modelId } = resolveModel({ openRouterModelId });
const { text } = await generateText({
model,
Expand Down Expand Up @@ -109,11 +111,8 @@ export const forPost = query({
if (scope.authenticated && !scope.viewer) return [];
const viewer = scope.viewer;
const post = await ctx.db.get(args.postId);
if (
!post ||
post.orgId !== scope.orgId ||
!(await canAccessPost(ctx, post, viewer?._id))
) return [];
if (!post || post.orgId !== scope.orgId || !(await canAccessPost(ctx, post, viewer?._id)))
return [];
const tasks = await ctx.db
.query("agentTasks")
.withIndex("by_org_id_and_post_id", (q) =>
Expand Down Expand Up @@ -149,11 +148,7 @@ export const create = mutation({

if (args.sourceReplyId) {
const sourceReply = await ctx.db.get(args.sourceReplyId);
if (
!sourceReply ||
sourceReply.orgId !== orgId ||
sourceReply.postId !== args.postId
) {
if (!sourceReply || sourceReply.orgId !== orgId || sourceReply.postId !== args.postId) {
notFound("Source reply not found.");
}
}
Expand Down Expand Up @@ -211,10 +206,7 @@ export const getRunnableTask = internalQuery({
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
if (!task) return null;
const [post, agent] = await Promise.all([
ctx.db.get(task.postId),
ctx.db.get(task.agentId),
]);
const [post, agent] = await Promise.all([ctx.db.get(task.postId), ctx.db.get(task.agentId)]);
if (!post || !agent || post.orgId !== task.orgId || agent.orgId !== task.orgId) {
return null;
}
Expand Down Expand Up @@ -247,9 +239,7 @@ export const getRunnableTask = internalQuery({
"",
"REPLIES:",
...(replies.length
? replies.map(
(reply) => `- ${authorNames.get(reply.authorId) ?? "Unknown"}: ${reply.body}`,
)
? replies.map((reply) => `- ${authorNames.get(reply.authorId) ?? "Unknown"}: ${reply.body}`)
: ["(no replies yet)"]),
].join("\n");

Expand Down Expand Up @@ -310,7 +300,11 @@ export const runSimulated = internalAction({
try {
const res = await generateAgentResult({
ctx,
orgId: runnable.task.orgId ?? (() => { throw new Error("Agent task missing orgId"); })(),
orgId:
runnable.task.orgId ??
(() => {
throw new Error("Agent task missing orgId");
})(),
agentName: runnable.agentName,
prompt: runnable.task.prompt,
contextText: runnable.contextText,
Expand All @@ -326,15 +320,12 @@ export const runSimulated = internalAction({
return;
}

const replyId: Id<"replies"> = await ctx.runMutation(
internal.replies.createAsAgent,
{
postId: runnable.task.postId,
parentId: runnable.task.sourceReplyId,
authorId: runnable.task.agentId,
body: res.result,
},
);
const replyId: Id<"replies"> = await ctx.runMutation(internal.replies.createAsAgent, {
postId: runnable.task.postId,
parentId: runnable.task.sourceReplyId,
authorId: runnable.task.agentId,
body: res.result,
});

await ctx.runMutation(internal.agentTasks.setStatus, {
taskId: args.taskId,
Expand All @@ -349,9 +340,7 @@ export const runSimulated = internalAction({
await ctx.runMutation(internal.agentTasks.setStatus, {
taskId: args.taskId,
status: "failed",
error: /API_KEY|not set/i.test(msg)
? "AI is disabled for the time of the demo."
: msg,
error: /API_KEY|not set/i.test(msg) ? "AI is disabled for the time of the demo." : msg,
completedAt: Date.now(),
});
}
Expand Down
41 changes: 15 additions & 26 deletions convex/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ type OpenRouterModel = {
* Pioneer (OpenAI-compatible, https://docs.pioneer.ai):
* PIONEER_API_KEY, PIONEER_MODEL, PIONEER_BASE_URL?
*/
export function resolveModel(
options: ResolveModelOptions = {},
): { model: LanguageModel; modelId: string } {
export function resolveModel(options: ResolveModelOptions = {}): {
model: LanguageModel;
modelId: string;
} {
if (options.openRouterModelId) {
const modelId = normalizeModelId(options.openRouterModelId);
return resolveOpenRouterModel(modelId);
Expand Down Expand Up @@ -140,8 +141,7 @@ export function aiConfigured(options: ResolveModelOptions = {}): boolean {
const provider = (process.env.AI_PROVIDER ?? "openrouter").toLowerCase();
if (provider === "gateway") return !!process.env.AI_GATEWAY_API_KEY;
if (provider === "openrouter") return !!process.env.OPENROUTER_API_KEY;
if (provider === "pioneer")
return !!process.env.PIONEER_API_KEY && !!process.env.PIONEER_MODEL;
if (provider === "pioneer") return !!process.env.PIONEER_API_KEY && !!process.env.PIONEER_MODEL;
if (provider === "openai") return !!process.env.OPENAI_API_KEY;
return false;
}
Expand All @@ -155,9 +155,7 @@ export const getGenerationModelSetting = internalQuery({
const orgId = args.orgId;
const setting = await ctx.db
.query("aiGenerationSettings")
.withIndex("by_org_id_and_kind", (q) =>
q.eq("orgId", orgId).eq("kind", args.kind),
)
.withIndex("by_org_id_and_kind", (q) => q.eq("orgId", orgId).eq("kind", args.kind))
.first();
return setting?.modelId ?? null;
},
Expand Down Expand Up @@ -192,18 +190,10 @@ function isAllowedOpenRouterModel(model: OpenRouterModel): boolean {

const id = model.id.toLowerCase();
const name = model.name?.toLowerCase() ?? "";
if (
id.startsWith("liquid/") ||
id.startsWith("meta-llama/") ||
id.includes("nemotron")
) {
if (id.startsWith("liquid/") || id.startsWith("meta-llama/") || id.includes("nemotron")) {
return false;
}
if (
/channel[-_ ]?rating|content[-_ ]?safety|moderation|guardrail/.test(
`${id} ${name}`,
)
) {
if (/channel[-_ ]?rating|content[-_ ]?safety|moderation|guardrail/.test(`${id} ${name}`)) {
return false;
}

Expand All @@ -219,9 +209,7 @@ function isAllowedOpenRouterModel(model: OpenRouterModel): boolean {

export const listOpenRouterFreeModels = action({
args: {},
handler: async (): Promise<
{ id: string; name: string; contextLength?: number }[]
> => {
handler: async (): Promise<{ id: string; name: string; contextLength?: number }[]> => {
const response = await fetch(
`${DEFAULT_OPENROUTER_BASE_URL}/models?output_modalities=text&sort=pricing-low-to-high`,
);
Expand All @@ -234,10 +222,7 @@ export const listOpenRouterFreeModels = action({
const payload = (await response.json()) as { data?: OpenRouterModel[] };
const models = payload.data ?? [];
const free = models
.filter(
(model) =>
isFreeOpenRouterModel(model) && isAllowedOpenRouterModel(model),
)
.filter((model) => isFreeOpenRouterModel(model) && isAllowedOpenRouterModel(model))
.map((model) => ({
id: model.id,
name: model.name ?? model.id,
Expand Down Expand Up @@ -322,7 +307,11 @@ export const summarizePost = action({
].join("\n");

const openRouterModelId = await ctx.runQuery(internal.ai.getGenerationModelSetting, {
orgId: accessiblePost.orgId ?? (() => { throw new Error("Post missing orgId"); })(),
orgId:
accessiblePost.orgId ??
(() => {
throw new Error("Post missing orgId");
})(),
kind: "postSummary",
});
const { model, modelId } = resolveModel({ openRouterModelId });
Expand Down
Loading