| 1 | import * as Schema from "effect/Schema"; |
| 2 | |
| 3 | import { |
| 4 | CommandId, |
| 5 | IsoDateTime, |
| 6 | NonNegativeInt, |
| 7 | PositiveInt, |
| 8 | ThreadId, |
| 9 | TrimmedNonEmptyString, |
| 10 | } from "./baseSchemas.ts"; |
| 11 | import { ThreadLinkedPullRequest } from "./threadPullRequest.ts"; |
| 12 | import { ThreadTitleRegeneration } from "./threadTitle.ts"; |
| 13 | |
| 14 | function hasOnlyPairedSurrogates(value: string): boolean { |
| 15 | for (let index = 0; index < value.length; index += 1) { |
| 16 | const codeUnit = value.charCodeAt(index); |
| 17 | if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) { |
| 18 | const nextCodeUnit = value.charCodeAt(index + 1); |
| 19 | if (index + 1 >= value.length || nextCodeUnit < 0xdc00 || nextCodeUnit > 0xdfff) { |
| 20 | return false; |
| 21 | } |
| 22 | index += 1; |
| 23 | } else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) { |
| 24 | return false; |
| 25 | } |
| 26 | } |
| 27 | return true; |
| 28 | } |
| 29 | |
| 30 | function isHttpUrl(value: string): boolean { |
| 31 | try { |
| 32 | const url = new URL(value); |
| 33 | return url.protocol === "http:" || url.protocol === "https:"; |
| 34 | } catch { |
| 35 | return false; |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | const ThreadMetadataTitle = TrimmedNonEmptyString.check(Schema.isMaxLength(512)).annotate({ |
| 40 | description: "New concise display title. Required only when action is rename.", |
| 41 | }); |
| 42 | |
| 43 | const ThreadMetadataClientRequestId = TrimmedNonEmptyString.check(Schema.isMaxLength(256)) |
| 44 | .check( |
| 45 | Schema.makeFilter((value) => |
| 46 | hasOnlyPairedSurrogates(value) |
| 47 | ? true |
| 48 | : "clientRequestId must contain valid paired Unicode surrogates.", |
| 49 | ), |
| 50 | ) |
| 51 | .annotate({ description: "Stable idempotency key to reuse when retrying this mutation." }); |
| 52 | |
| 53 | const ThreadMetadataMcpPullRequestUrl = TrimmedNonEmptyString.check( |
| 54 | Schema.makeFilter((value) => |
| 55 | isHttpUrl(value) ? true : "Pull request URL must be a well-formed HTTP(S) URL.", |
| 56 | ), |
| 57 | ).annotate({ |
| 58 | description: "Canonical HTTP(S) pull request URL, including self-hosted repository URLs.", |
| 59 | }); |
| 60 | |
| 61 | export const ThreadMetadataMcpAction = Schema.Literals([ |
| 62 | "rename", |
| 63 | "regenerate_title", |
| 64 | "link_pull_request", |
| 65 | "unlink_pull_request", |
| 66 | ]).annotate({ |
| 67 | description: |
| 68 | "Metadata mutation: rename, regenerate_title, link_pull_request, or unlink_pull_request.", |
| 69 | }); |
| 70 | export type ThreadMetadataMcpAction = typeof ThreadMetadataMcpAction.Type; |
| 71 | |
| 72 | export const ThreadMetadataMcpPullRequest = Schema.Struct({ |
| 73 | repository: TrimmedNonEmptyString.annotate({ |
| 74 | description: "Repository name as owner/name.", |
| 75 | }), |
| 76 | number: PositiveInt.annotate({ description: "Pull request number." }), |
| 77 | url: ThreadMetadataMcpPullRequestUrl, |
| 78 | }); |
| 79 | export type ThreadMetadataMcpPullRequest = typeof ThreadMetadataMcpPullRequest.Type; |
| 80 | |
| 81 | export const ThreadMetadataMcpUpdateInput = Schema.Struct({ |
| 82 | threadId: Schema.optional(ThreadId).annotate({ |
| 83 | description: "Thread to update. Omit to update the calling thread.", |
| 84 | }), |
| 85 | action: ThreadMetadataMcpAction, |
| 86 | title: Schema.optional(ThreadMetadataTitle), |
| 87 | pullRequest: Schema.optional(ThreadMetadataMcpPullRequest).annotate({ |
| 88 | description: "Pull request to link. Required only when action is link_pull_request.", |
| 89 | }), |
| 90 | clientRequestId: Schema.optional(ThreadMetadataClientRequestId), |
| 91 | }).check( |
| 92 | Schema.makeFilter((input) => { |
| 93 | switch (input.action) { |
| 94 | case "rename": |
| 95 | return input.title !== undefined && input.pullRequest === undefined |
| 96 | ? true |
| 97 | : "rename requires title and does not accept pullRequest."; |
| 98 | case "link_pull_request": |
| 99 | return input.pullRequest !== undefined && input.title === undefined |
| 100 | ? true |
| 101 | : "link_pull_request requires pullRequest and does not accept title."; |
| 102 | case "regenerate_title": |
| 103 | case "unlink_pull_request": |
| 104 | return input.title === undefined && input.pullRequest === undefined |
| 105 | ? true |
| 106 | : `${input.action} does not accept title or pullRequest.`; |
| 107 | } |
| 108 | }), |
| 109 | ); |
| 110 | export type ThreadMetadataMcpUpdateInput = typeof ThreadMetadataMcpUpdateInput.Type; |
| 111 | |
| 112 | export const ThreadMetadataMcpUpdateResult = Schema.Struct({ |
| 113 | threadId: ThreadId, |
| 114 | action: ThreadMetadataMcpAction, |
| 115 | commandId: CommandId, |
| 116 | sequence: NonNegativeInt, |
| 117 | title: Schema.String, |
| 118 | titleRegeneration: Schema.NullOr(ThreadTitleRegeneration), |
| 119 | linkedPullRequest: Schema.NullOr(ThreadLinkedPullRequest), |
| 120 | updatedAt: IsoDateTime, |
| 121 | }); |
| 122 | export type ThreadMetadataMcpUpdateResult = typeof ThreadMetadataMcpUpdateResult.Type; |