1import * as Schema from "effect/Schema";
2
3import {
4 CommandId,
5 IsoDateTime,
6 NonNegativeInt,
7 PositiveInt,
8 ThreadId,
9 TrimmedNonEmptyString,
10} from "./baseSchemas.ts";
11import { ThreadLinkedPullRequest } from "./threadPullRequest.ts";
12import { ThreadTitleRegeneration } from "./threadTitle.ts";
13
14function 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
30function 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
39const ThreadMetadataTitle = TrimmedNonEmptyString.check(Schema.isMaxLength(512)).annotate({
40 description: "New concise display title. Required only when action is rename.",
41});
42
43const 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
53const 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
61export 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});
70export type ThreadMetadataMcpAction = typeof ThreadMetadataMcpAction.Type;
71
72export 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});
79export type ThreadMetadataMcpPullRequest = typeof ThreadMetadataMcpPullRequest.Type;
80
81export 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);
110export type ThreadMetadataMcpUpdateInput = typeof ThreadMetadataMcpUpdateInput.Type;
111
112export 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});
122export type ThreadMetadataMcpUpdateResult = typeof ThreadMetadataMcpUpdateResult.Type;