UNPKG

@ankimcp/anki-mcp-server

Version:

Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards

173 lines • 9.08 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var SetDueDateTool_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.SetDueDateTool = exports.setDueDateInputSchema = void 0; const common_1 = require("@nestjs/common"); const microservices_1 = require("@nestjs/microservices"); const mcp_nest_1 = require("@rekog/mcp-nest"); const zod_1 = require("zod"); const anki_connect_client_1 = require("../../../clients/anki-connect.client.js"); const anki_utils_1 = require("../../../utils/anki.utils.js"); const card_validation_utils_1 = require("../../../utils/card-validation.utils.js"); const DAYS_SPEC = /^(\d+)(?:-(\d+))?(!?)$/; exports.setDueDateInputSchema = zod_1.z.object({ cards: zod_1.z .array(zod_1.z.number().int().positive()) .min(1) .max(100) .describe("Array of card IDs to reschedule (max 100). Card IDs (not note IDs) — use get_cards, " + "get_due_cards, or notesInfo to obtain them."), days: zod_1.z .string() .describe("How many days from now the cards become due. " + '"0" = today, "1" = tomorrow, "3-7" = a random day in that inclusive range ' + "(spreads a batch out instead of stacking it on one day). " + 'Append "!" — e.g. "1!" — to also overwrite the card\'s interval with that value, ' + "rather than leaving the existing interval in place."), }); let SetDueDateTool = SetDueDateTool_1 = class SetDueDateTool { ankiClient; logger = new common_1.Logger(SetDueDateTool_1.name); constructor(ankiClient) { this.ankiClient = ankiClient; } async execute(params) { const cards = Array.isArray(params?.cards) ? [...new Set(params.cards)] : []; const rawDays = params?.days; try { this.logger.log(`Executing setDueDate: ${cards.length} card(s) -> "${rawDays}"`); if (cards.length === 0) { throw new Error("cards array is required for setDueDate action"); } if (!rawDays || rawDays.trim() === "") { throw new Error("days is required for setDueDate action"); } const days = rawDays.trim(); const match = DAYS_SPEC.exec(days); if (!match) { throw new Error(`Invalid days spec "${days}". Expected a day count ("0", "5"), an ` + 'inclusive range ("3-7"), optionally suffixed with "!" to also ' + 'overwrite the interval ("1!").'); } const [, startStr, endStr] = match; if (endStr !== undefined && Number(endStr) < Number(startStr)) { throw new Error(`Invalid days range "${days}": the end of the range must not be ` + `earlier than the start.`); } await (0, card_validation_utils_1.assertCardIdsExist)(cards, this.ankiClient, "No cards were rescheduled."); await this.ankiClient.invoke("setDueDate", { cards, days }); let scheduled = []; let readBackFailed = false; try { const updated = await this.ankiClient.invoke("cardsInfo", { cards }); if (!Array.isArray(updated) || updated.length !== cards.length) { throw new Error(`cardsInfo read-back returned ${Array.isArray(updated) ? updated.length : typeof updated} entries for ${cards.length} card(s)`); } const missingIds = (0, card_validation_utils_1.findMissingIds)(cards, updated); if (missingIds.length > 0) { throw new Error(`cardsInfo read-back is missing ${missingIds.length} of ` + `${cards.length} card(s): [${missingIds.join(", ")}]`); } scheduled = cards.map((cardId, index) => ({ cardId, intervalDays: updated[index]?.interval ?? 0, due: updated[index]?.due ?? 0, })); } catch (readBackError) { readBackFailed = true; this.logger.warn(`Rescheduled ${cards.length} card(s) to "${days}" but could not read ` + `the new scheduling back`, readBackError); } if (!readBackFailed) { this.logger.log(`Rescheduled ${cards.length} card(s) to "${days}"`); } return { success: true, message: readBackFailed ? `Successfully rescheduled ${cards.length} card(s) to "${days}", but reading ` + `the new scheduling back failed. The reschedule was applied, and a retry would reschedule the cards again.` : `Successfully rescheduled ${cards.length} card(s) to "${days}"`, cardsAffected: cards.length, days, intervalOverwritten: days.endsWith("!"), scheduled, }; } catch (error) { this.logger.error("Failed to execute setDueDate", error); return (0, anki_utils_1.createErrorResponse)(error, { action: "setDueDate", cardIds: cards, days: rawDays, hint: 'This can happen when Anki is not running, the card IDs are invalid (note IDs are not card IDs), or days is not in a form like "0", "5", "3-7", or "1!"', }); } } }; exports.SetDueDateTool = SetDueDateTool; __decorate([ (0, mcp_nest_1.Tool)({ name: "setDueDate", description: "Reschedule cards to become due in a given number of days, without recording a review. " + "Use this to bring a card forward (or push it back) when its current interval doesn't match how well " + "the user actually knows it — rating the card instead would log a fake review and skew its ease factor. " + "Unlike forgetCards this keeps the card's existing interval and ease unless you ask otherwise, so it is " + "the gentler option when the card's history is still worth keeping. " + "Note that applying this to a new card turns it into a review card.", parameters: exports.setDueDateInputSchema, outputSchema: zod_1.z.object({ success: zod_1.z.boolean(), message: zod_1.z.string(), cardsAffected: zod_1.z.number(), days: zod_1.z.string().describe("The due-date spec that was applied"), intervalOverwritten: zod_1.z .boolean() .describe('True when the spec ended in "!", meaning the interval was reset to the day count'), scheduled: zod_1.z .array(zod_1.z.object({ cardId: zod_1.z.number(), intervalDays: zod_1.z .number() .describe("The card's interval after rescheduling"), due: zod_1.z .number() .describe("Anki's raw due value: days since collection creation for review cards, " + "a queue position for new cards, a unix timestamp for learning cards"), })) .describe("Per-card scheduling state read back after the change. Empty when the " + "read-back failed — the reschedule itself still succeeded, so check " + "`message` rather than treating an empty array as a no-op."), }), annotations: { title: "Set Card Due Date", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, }, }), __param(0, (0, microservices_1.Payload)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], SetDueDateTool.prototype, "execute", null); exports.SetDueDateTool = SetDueDateTool = SetDueDateTool_1 = __decorate([ (0, mcp_nest_1.McpController)(), __metadata("design:paramtypes", [anki_connect_client_1.AnkiConnectClient]) ], SetDueDateTool); //# sourceMappingURL=set-due-date.tool.js.map