import { defineAction } from "@agent-native/core/action"; import { z } from "zod"; import { ChapterInputSchema, CutRangeSchema, StoredChapterSchema, parseList, saveChapters, } from "./lib/save-chapters.js"; export default defineAction({ mcpApp: { compactCatalog: true }, description: "Replace all chapters on a recording with the given list, sorted by start time. Chapters are {startMs,title} markers on the timeline and in the player. To add, rename or remove one chapter, read the current chapters from get-recording-player-data and send the whole list. Pass that original list as expectedChapters and its mediaUpdatedAt as expectedMediaUpdatedAt. For a regeneration request, regenerate-chapters supplies expectedVersion and expectedMediaUpdatedAt instead. Saves based on changed chapters or media are refused.", schema: z.object({ recordingId: z.string().describe("Recording ID"), chapters: z .union([z.string(), z.array(ChapterInputSchema)]) .describe( "Array of {startMs,title} — either a JSON-encoded string (CLI) or an array (agent).", ), expectedChapters: z .union([z.string(), z.array(StoredChapterSchema)]) .optional() .describe( "The chapters this edit started from (this or expectedVersion is required); if the stored chapters differ, nothing is written and the call fails with errorCode chapters_changed.", ), expectedVersion: z .string() .min(1) .optional() .describe( "The chapters' version token this edit started from (regenerate-chapters gives one); if the stored chapters' version differs, nothing is written and the call fails with errorCode chapters_changed.", ), expectedCuts: z .union([z.string(), z.array(CutRangeSchema)]) .optional() .describe( "Optional. The cut ranges ({startMs,endMs}, original-media ms) the chapter times were mapped through; if the recording's cuts differ, nothing is written and the call fails with errorCode chapters_changed.", ), expectedMediaUpdatedAt: z .string() .optional() .describe( "Required with every chapter replacement: mediaUpdatedAt from the get-recording-player-data response used to create these times.", ), discardUnreadable: z .union([z.boolean(), z.enum(["true", "false"])]) .transform((value) => value === true || value === "true") .optional() .describe( "Only after a chapters_unreadable refusal, and only when the user asked to replace the chapters: drops the stored entries that can't be read. The expected chapters are still checked.", ), }), run: async (args) => { return saveChapters({ recordingId: args.recordingId, chapters: parseList( args.chapters, "chapters", ChapterInputSchema, "invalid_chapters", ), expectedChapters: args.expectedChapters === undefined ? null : parseList( args.expectedChapters, "expectedChapters", StoredChapterSchema, "invalid_chapters", ), expectedVersion: args.expectedVersion ?? null, expectedMediaUpdatedAt: args.expectedMediaUpdatedAt ?? null, discardUnreadable: args.discardUnreadable ?? false, expectedCuts: args.expectedCuts === undefined ? null : parseList( args.expectedCuts, "expectedCuts", CutRangeSchema, "invalid_cuts", ), }); }, });