1import * as NodeOS from "node:os";
2
3let nextServerRequestId = 10_000;
4let pendingSkillsListRequestId: number | string | null = null;
5let pendingUserInputRequestId: number | null = null;
6
7const writeMessage = (message: unknown) => {
8 process.stdout.write(`${JSON.stringify(message)}\n`);
9};
10
11const respond = (id: number | string, result: unknown) => {
12 writeMessage({ id, result });
13};
14
15const respondError = (id: number | string, code: number, message: string) => {
16 writeMessage({
17 id,
18 error: {
19 code,
20 message,
21 },
22 });
23};
24
25const sendRequest = (method: string, params: unknown) => {
26 const id = nextServerRequestId++;
27 writeMessage({ id, method, params });
28 return id;
29};
30
31const handleMethod = (message: Record<string, unknown>) => {
32 const method = message.method;
33 if (typeof method !== "string") {
34 return;
35 }
36
37 switch (method) {
38 case "initialize": {
39 // oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone mock peer process has no Effect runtime.
40 const platform = NodeOS.platform();
41 const stderrBytes = Number(process.env.CODEX_APP_SERVER_TEST_STDERR_BYTES ?? 0);
42 if (Number.isFinite(stderrBytes) && stderrBytes > 0) {
43 process.stderr.write("x".repeat(stderrBytes), () => {
44 respond(message.id as number | string, {
45 userAgent: "mock-codex-app-server",
46 codexHome: process.cwd(),
47 platformFamily: platform === "win32" ? "windows" : "unix",
48 platformOs: platform === "darwin" ? "macos" : platform,
49 });
50 });
51 return;
52 }
53 respond(message.id as number | string, {
54 userAgent: "mock-codex-app-server",
55 codexHome: process.cwd(),
56 platformFamily: platform === "win32" ? "windows" : "unix",
57 platformOs: platform === "darwin" ? "macos" : platform,
58 });
59 return;
60 }
61 case "initialized": {
62 writeMessage({
63 method: "item/agentMessage/delta",
64 params: {
65 delta: "Mock server is ready.",
66 itemId: "item-1",
67 threadId: "thread-1",
68 turnId: "turn-1",
69 },
70 });
71 return;
72 }
73 case "account/read": {
74 respond(message.id as number | string, {
75 account: {
76 type: "chatgpt",
77 email: "mock@example.com",
78 planType: "plus",
79 },
80 requiresOpenaiAuth: false,
81 });
82 return;
83 }
84 case "skills/list": {
85 pendingSkillsListRequestId = message.id as number | string;
86 pendingUserInputRequestId = sendRequest("item/tool/requestUserInput", {
87 isBlocking: true,
88 itemId: "item-approval-1",
89 threadId: "thread-1",
90 turnId: "turn-1",
91 questions: [
92 {
93 id: "approved",
94 header: "Approve",
95 question: "Continue with the mock skills request?",
96 options: [
97 {
98 label: "yes",
99 description: "Approve the request",
100 },
101 ],
102 },
103 ],
104 });
105 return;
106 }
107 default: {
108 if (message.id !== undefined) {
109 respondError(message.id as number | string, -32601, `Unhandled request: ${method}`);
110 }
111 }
112 }
113};
114
115const handleResponse = (message: Record<string, unknown>) => {
116 if (message.id !== pendingUserInputRequestId) {
117 return;
118 }
119
120 pendingUserInputRequestId = null;
121
122 respond(pendingSkillsListRequestId!, {
123 data: [
124 {
125 cwd: process.cwd(),
126 errors: [],
127 skills: [],
128 },
129 ],
130 });
131 pendingSkillsListRequestId = null;
132};
133
134let remainder = "";
135
136process.stdin.setEncoding("utf8");
137process.stdin.on("data", (chunk) => {
138 remainder += chunk;
139 const lines = remainder.split("\n");
140 remainder = lines.pop() ?? "";
141
142 for (const line of lines) {
143 const trimmed = line.trim();
144 if (trimmed.length === 0) {
145 continue;
146 }
147
148 const message = JSON.parse(trimmed) as Record<string, unknown>;
149 if ("method" in message) {
150 handleMethod(message);
151 continue;
152 }
153 if ("id" in message) {
154 handleResponse(message);
155 }
156 }
157});
158
159process.stdin.on("end", () => {
160 process.exit(0);
161});