import { z } from "zod"; import { addIssueCommentSchema, checkoutIssueSchema, createApprovalSchema, createIssueSchema, updateIssueSchema, upsertIssueDocumentSchema, linkIssueApprovalSchema, } from "@paperclipai/shared"; import { PaperclipApiClient } from "./client.js"; import { formatErrorResponse, formatTextResponse } from "./format.js"; export interface ToolDefinition { name: string; description: string; schema: z.AnyZodObject; execute: (input: Record) => Promise<{ content: Array<{ type: "text"; text: string }>; }>; } function makeTool( name: string, description: string, schema: z.ZodObject, execute: (input: z.infer) => Promise, ): ToolDefinition { return { name, description, schema, execute: async (input) => { try { const parsed = schema.parse(input); return formatTextResponse(await execute(parsed)); } catch (error) { return formatErrorResponse(error); } }, }; } function parseOptionalJson(raw: string | undefined | null): unknown { if (!raw || raw.trim().length === 0) return undefined; return JSON.parse(raw); } const companyIdOptional = z.string().uuid().optional().nullable(); const agentIdOptional = z.string().uuid().optional().nullable(); const issueIdSchema = z.string().min(1); const projectIdSchema = z.string().min(1); const goalIdSchema = z.string().uuid(); const approvalIdSchema = z.string().uuid(); const documentKeySchema = z.string().trim().min(1).max(64); const listIssuesSchema = z.object({ companyId: companyIdOptional, status: z.string().optional(), projectId: z.string().uuid().optional(), assigneeAgentId: z.string().uuid().optional(), participantAgentId: z.string().uuid().optional(), assigneeUserId: z.string().optional(), touchedByUserId: z.string().optional(), inboxArchivedByUserId: z.string().optional(), unreadForUserId: z.string().optional(), labelId: z.string().uuid().optional(), executionWorkspaceId: z.string().uuid().optional(), originKind: z.string().optional(), originId: z.string().optional(), includeRoutineExecutions: z.boolean().optional(), q: z.string().optional(), }); const listCommentsSchema = z.object({ issueId: issueIdSchema, after: z.string().uuid().optional(), order: z.enum(["asc", "desc"]).optional(), limit: z.number().int().positive().max(500).optional(), }); const upsertDocumentToolSchema = z.object({ issueId: issueIdSchema, key: documentKeySchema, title: z.string().trim().max(200).nullable().optional(), format: z.enum(["markdown"]).default("markdown"), body: z.string().max(524288), changeSummary: z.string().trim().max(500).nullable().optional(), baseRevisionId: z.string().uuid().nullable().optional(), }); const createIssueToolSchema = z.object({ companyId: companyIdOptional, }).merge(createIssueSchema); const updateIssueToolSchema = z.object({ issueId: issueIdSchema, }).merge(updateIssueSchema); const checkoutIssueToolSchema = z.object({ issueId: issueIdSchema, agentId: agentIdOptional, expectedStatuses: checkoutIssueSchema.shape.expectedStatuses.optional(), }); const addCommentToolSchema = z.object({ issueId: issueIdSchema, }).merge(addIssueCommentSchema); const approvalDecisionSchema = z.object({ approvalId: approvalIdSchema, action: z.enum(["approve", "reject", "requestRevision", "resubmit"]), decisionNote: z.string().optional(), payloadJson: z.string().optional(), }); const createApprovalToolSchema = z.object({ companyId: companyIdOptional, }).merge(createApprovalSchema); const apiRequestSchema = z.object({ method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]), path: z.string().min(1), jsonBody: z.string().optional(), }); export function createToolDefinitions(client: PaperclipApiClient): ToolDefinition[] { return [ makeTool( "paperclipMe", "Get the current authenticated Paperclip actor details", z.object({}), async () => client.requestJson("GET", "/agents/me"), ), makeTool( "paperclipInboxLite", "Get the current authenticated agent inbox-lite assignment list", z.object({}), async () => client.requestJson("GET", "/agents/me/inbox-lite"), ), makeTool( "paperclipListAgents", "List agents in a company", z.object({ companyId: companyIdOptional }), async ({ companyId }) => client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/agents`), ), makeTool( "paperclipGetAgent", "Get a single agent by id", z.object({ agentId: z.string().min(1), companyId: companyIdOptional }), async ({ agentId, companyId }) => { const qs = companyId ? `?companyId=${encodeURIComponent(companyId)}` : ""; return client.requestJson("GET", `/agents/${encodeURIComponent(agentId)}${qs}`); }, ), makeTool( "paperclipListIssues", "List issues for a company with optional filters", listIssuesSchema, async (input) => { const companyId = client.resolveCompanyId(input.companyId); const params = new URLSearchParams(); for (const [key, value] of Object.entries(input)) { if (key === "companyId" || value === undefined || value === null) continue; params.set(key, String(value)); } const qs = params.toString(); return client.requestJson("GET", `/companies/${companyId}/issues${qs ? `?${qs}` : ""}`); }, ), makeTool( "paperclipGetIssue", "Get a single issue by UUID or identifier", z.object({ issueId: issueIdSchema }), async ({ issueId }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}`), ), makeTool( "paperclipGetHeartbeatContext", "Get compact heartbeat context for an issue", z.object({ issueId: issueIdSchema, wakeCommentId: z.string().uuid().optional() }), async ({ issueId, wakeCommentId }) => { const qs = wakeCommentId ? `?wakeCommentId=${encodeURIComponent(wakeCommentId)}` : ""; return client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/heartbeat-context${qs}`); }, ), makeTool( "paperclipListComments", "List issue comments with incremental options", listCommentsSchema, async ({ issueId, after, order, limit }) => { const params = new URLSearchParams(); if (after) params.set("after", after); if (order) params.set("order", order); if (limit) params.set("limit", String(limit)); const qs = params.toString(); return client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/comments${qs ? `?${qs}` : ""}`); }, ), makeTool( "paperclipGetComment", "Get a specific issue comment by id", z.object({ issueId: issueIdSchema, commentId: z.string().uuid() }), async ({ issueId, commentId }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/comments/${encodeURIComponent(commentId)}`), ), makeTool( "paperclipListIssueApprovals", "List approvals linked to an issue", z.object({ issueId: issueIdSchema }), async ({ issueId }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/approvals`), ), makeTool( "paperclipListDocuments", "List issue documents", z.object({ issueId: issueIdSchema }), async ({ issueId }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/documents`), ), makeTool( "paperclipGetDocument", "Get one issue document by key", z.object({ issueId: issueIdSchema, key: documentKeySchema }), async ({ issueId, key }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}`), ), makeTool( "paperclipListDocumentRevisions", "List revisions for an issue document", z.object({ issueId: issueIdSchema, key: documentKeySchema }), async ({ issueId, key }) => client.requestJson( "GET", `/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}/revisions`, ), ), makeTool( "paperclipListProjects", "List projects in a company", z.object({ companyId: companyIdOptional }), async ({ companyId }) => client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/projects`), ), makeTool( "paperclipGetProject", "Get a project by id or company-scoped short reference", z.object({ projectId: projectIdSchema, companyId: companyIdOptional }), async ({ projectId, companyId }) => { const qs = companyId ? `?companyId=${encodeURIComponent(companyId)}` : ""; return client.requestJson("GET", `/projects/${encodeURIComponent(projectId)}${qs}`); }, ), makeTool( "paperclipListGoals", "List goals in a company", z.object({ companyId: companyIdOptional }), async ({ companyId }) => client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/goals`), ), makeTool( "paperclipGetGoal", "Get a goal by id", z.object({ goalId: goalIdSchema }), async ({ goalId }) => client.requestJson("GET", `/goals/${encodeURIComponent(goalId)}`), ), makeTool( "paperclipListApprovals", "List approvals in a company", z.object({ companyId: companyIdOptional, status: z.string().optional() }), async ({ companyId, status }) => { const qs = status ? `?status=${encodeURIComponent(status)}` : ""; return client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/approvals${qs}`); }, ), makeTool( "paperclipCreateApproval", "Create a board approval request, optionally linked to one or more issues", createApprovalToolSchema, async ({ companyId, ...body }) => client.requestJson("POST", `/companies/${client.resolveCompanyId(companyId)}/approvals`, { body, }), ), makeTool( "paperclipGetApproval", "Get an approval by id", z.object({ approvalId: approvalIdSchema }), async ({ approvalId }) => client.requestJson("GET", `/approvals/${encodeURIComponent(approvalId)}`), ), makeTool( "paperclipGetApprovalIssues", "List issues linked to an approval", z.object({ approvalId: approvalIdSchema }), async ({ approvalId }) => client.requestJson("GET", `/approvals/${encodeURIComponent(approvalId)}/issues`), ), makeTool( "paperclipListApprovalComments", "List comments for an approval", z.object({ approvalId: approvalIdSchema }), async ({ approvalId }) => client.requestJson("GET", `/approvals/${encodeURIComponent(approvalId)}/comments`), ), makeTool( "paperclipCreateIssue", "Create a new issue", createIssueToolSchema, async ({ companyId, ...body }) => client.requestJson("POST", `/companies/${client.resolveCompanyId(companyId)}/issues`, { body }), ), makeTool( "paperclipUpdateIssue", "Patch an issue, optionally including a comment", updateIssueToolSchema, async ({ issueId, ...body }) => client.requestJson("PATCH", `/issues/${encodeURIComponent(issueId)}`, { body }), ), makeTool( "paperclipCheckoutIssue", "Checkout an issue for an agent", checkoutIssueToolSchema, async ({ issueId, agentId, expectedStatuses }) => client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/checkout`, { body: { agentId: client.resolveAgentId(agentId), expectedStatuses: expectedStatuses ?? ["todo", "backlog", "blocked"], }, }), ), makeTool( "paperclipReleaseIssue", "Release an issue checkout", z.object({ issueId: issueIdSchema }), async ({ issueId }) => client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/release`, { body: {} }), ), makeTool( "paperclipAddComment", "Add a comment to an issue", addCommentToolSchema, async ({ issueId, ...body }) => client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/comments`, { body }), ), makeTool( "paperclipUpsertIssueDocument", "Create or update an issue document", upsertDocumentToolSchema, async ({ issueId, key, ...body }) => client.requestJson( "PUT", `/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}`, { body }, ), ), makeTool( "paperclipRestoreIssueDocumentRevision", "Restore a prior revision of an issue document", z.object({ issueId: issueIdSchema, key: documentKeySchema, revisionId: z.string().uuid(), }), async ({ issueId, key, revisionId }) => client.requestJson( "POST", `/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}/revisions/${encodeURIComponent(revisionId)}/restore`, { body: {} }, ), ), makeTool( "paperclipLinkIssueApproval", "Link an approval to an issue", z.object({ issueId: issueIdSchema }).merge(linkIssueApprovalSchema), async ({ issueId, approvalId }) => client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/approvals`, { body: { approvalId }, }), ), makeTool( "paperclipUnlinkIssueApproval", "Unlink an approval from an issue", z.object({ issueId: issueIdSchema, approvalId: approvalIdSchema }), async ({ issueId, approvalId }) => client.requestJson( "DELETE", `/issues/${encodeURIComponent(issueId)}/approvals/${encodeURIComponent(approvalId)}`, ), ), makeTool( "paperclipApprovalDecision", "Approve, reject, request revision, or resubmit an approval", approvalDecisionSchema, async ({ approvalId, action, decisionNote, payloadJson }) => { const path = action === "approve" ? `/approvals/${encodeURIComponent(approvalId)}/approve` : action === "reject" ? `/approvals/${encodeURIComponent(approvalId)}/reject` : action === "requestRevision" ? `/approvals/${encodeURIComponent(approvalId)}/request-revision` : `/approvals/${encodeURIComponent(approvalId)}/resubmit`; const body = action === "resubmit" ? { payload: parseOptionalJson(payloadJson) ?? {} } : { decisionNote }; return client.requestJson("POST", path, { body }); }, ), makeTool( "paperclipAddApprovalComment", "Add a comment to an approval", z.object({ approvalId: approvalIdSchema, body: z.string().min(1) }), async ({ approvalId, body }) => client.requestJson("POST", `/approvals/${encodeURIComponent(approvalId)}/comments`, { body: { body }, }), ), makeTool( "paperclipApiRequest", "Make a JSON request to an existing Paperclip /api endpoint for unsupported operations", apiRequestSchema, async ({ method, path, jsonBody }) => { if (!path.startsWith("/") || path.includes("..")) { throw new Error("path must start with / and be relative to /api, and must not contain '..'"); } return client.requestJson(method, path, { body: parseOptionalJson(jsonBody), }); }, ), ]; }