import { COMMENT_ENTITY_TYPE_VALUES, type CommentEntityType } from "@capakraken/shared"; import type { TRPCContext } from "../../trpc.js"; import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../../lib/comment-entity-registry.js"; import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js"; type AssistantToolErrorResult = { error: string }; type CommentRecord = { id: string; body: string; resolved: boolean; createdAt: Date; author: { name: string | null; email: string; }; }; type CommentListRecord = CommentRecord & { replies: CommentRecord[]; }; type CommentsDeps = { createCommentCaller: (ctx: TRPCContext) => { list: (params: { entityType: CommentEntityType; entityId: string; }) => Promise; create: (params: { entityType: CommentEntityType; entityId: string; body: string; }) => Promise; resolve: (params: { id: string; resolved: boolean; }) => Promise; }; createScopedCallerContext: (ctx: ToolContext) => TRPCContext; toAssistantCommentCreationError: ( error: unknown, ) => AssistantToolErrorResult | null; toAssistantCommentResolveError: ( error: unknown, ) => AssistantToolErrorResult | null; }; export const commentReadToolDefinitions: ToolDef[] = withToolAccess([ { type: "function", function: { name: "list_comments", description: `List comments (with replies) for a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required.`, parameters: { type: "object", properties: { entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() }, entityId: { type: "string", description: "Entity ID" }, }, required: ["entityType", "entityId"], }, }, }, ], {}); export const commentMutationToolDefinitions: ToolDef[] = withToolAccess([ { type: "function", function: { name: "create_comment", description: `Add a comment to a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required. Supports @mentions. Always confirm with the user first.`, parameters: { type: "object", properties: { entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() }, entityId: { type: "string", description: "Entity ID" }, body: { type: "string", description: "Comment body text. Use @[Name](userId) for mentions." }, }, required: ["entityType", "entityId", "body"], }, }, }, { type: "function", function: { name: "resolve_comment", description: `Mark a comment as resolved (or unresolve it) on a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required, and only the comment author or an admin can change resolution.`, parameters: { type: "object", properties: { commentId: { type: "string", description: "Comment ID to resolve" }, resolved: { type: "boolean", description: "Set to true to resolve, false to unresolve. Default: true" }, }, required: ["commentId"], }, }, }, ], {}); export function createCommentExecutors( deps: CommentsDeps, ): Record { return { async list_comments( params: { entityType: CommentEntityType; entityId: string }, ctx: ToolContext, ) { const caller = deps.createCommentCaller(deps.createScopedCallerContext(ctx)); const comments = await caller.list({ entityType: params.entityType, entityId: params.entityId, }); return comments.map((comment) => ({ id: comment.id, author: comment.author.name ?? comment.author.email, body: comment.body, resolved: comment.resolved, createdAt: comment.createdAt.toISOString(), replyCount: comment.replies.length, replies: comment.replies.map((reply) => ({ id: reply.id, author: reply.author.name ?? reply.author.email, body: reply.body, resolved: reply.resolved, createdAt: reply.createdAt.toISOString(), })), })); }, async create_comment( params: { entityType: CommentEntityType; entityId: string; body: string }, ctx: ToolContext, ) { if (params.body.length === 0) { return { error: "Comment body is required." }; } if (params.body.length > 10_000) { return { error: "Comment body must be at most 10000 characters." }; } const caller = deps.createCommentCaller(deps.createScopedCallerContext(ctx)); let comment; try { comment = await caller.create({ entityType: params.entityType, entityId: params.entityId, body: params.body, }); } catch (error) { const mapped = deps.toAssistantCommentCreationError(error); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["comment"], id: comment.id, author: comment.author.name ?? comment.author.email, body: comment.body, createdAt: comment.createdAt.toISOString(), }; }, async resolve_comment( params: { commentId: string; resolved?: boolean }, ctx: ToolContext, ) { const caller = deps.createCommentCaller(deps.createScopedCallerContext(ctx)); let updated; try { updated = await caller.resolve({ id: params.commentId, resolved: params.resolved !== false, }); } catch (error) { const mapped = deps.toAssistantCommentResolveError(error); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["comment"], id: updated.id, resolved: updated.resolved, author: updated.author.name ?? updated.author.email, body: updated.body.slice(0, 100), }; }, }; }