import * as Sentry from "@sentry/node"; // Use raw fs (not ctx.fs) so these operations run asynchronously and don't // interfere with the file-watcher used by `convex dev`. // eslint-disable-next-line no-restricted-imports import { promises as fs } from "fs"; import { z } from "zod"; import { aiDirForConvexDir, aiFilesStatePathForConvexDir } from "./paths.js"; import { attemptReadFile, exhaustiveCheck } from "./utils.js"; export const aiFilesStateSchema = z.object({ guidelinesHash: z.string().nullable(), agentsMdSectionHash: z.string().nullable(), claudeMdHash: z.string().nullable(), // Commit SHA from get-convex/agent-skills that was current when skills were // last installed. Used to detect when newer skills are available. agentSkillsSha: z.string().nullable(), }); export type AiFilesState = z.infer; const DEFAULT_AI_STATE: AiFilesState = { guidelinesHash: null, agentsMdSectionHash: null, claudeMdHash: null, agentSkillsSha: null, }; export type AttemptReadAiStateResult = | { kind: "no-file" } | { kind: "ok"; state: AiFilesState } | { kind: "parse-error"; error: unknown }; export async function attemptReadAiState( convexDir: string, ): Promise { const result = await attemptReadFile(aiFilesStatePathForConvexDir(convexDir)); if (result.kind === "not-found" || result.kind === "empty") return { kind: "no-file" }; try { const state = aiFilesStateSchema.parse(JSON.parse(result.content)); return { kind: "ok", state }; } catch (error) { Sentry.captureException(error); return { kind: "parse-error", error }; } } export async function readAiStateOrDefault( convexDir: string, ): Promise { const result = await attemptReadAiState(convexDir); if (result.kind === "ok") return result.state; if (result.kind === "no-file") return { ...DEFAULT_AI_STATE }; if (result.kind === "parse-error") return { ...DEFAULT_AI_STATE }; return exhaustiveCheck(result); } export async function hasAiState(convexDir: string): Promise { const result = await attemptReadAiState(convexDir); return result.kind === "ok"; } export async function writeAiState({ state, convexDir, }: { state: AiFilesState; convexDir: string; }): Promise { const validated = aiFilesStateSchema.parse(state); await fs.mkdir(aiDirForConvexDir(convexDir), { recursive: true }); await fs.writeFile( aiFilesStatePathForConvexDir(convexDir), JSON.stringify(validated, null, 2) + "\n", "utf8", ); }