@messenger-agent/codex-agent 0.24.0-alpha.2 → 0.24.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,218 +1,14 @@
1
- import { HTTPException } from "hono/http-exception";
2
- export function addTokenUsage(left, right) {
3
- const inputTokens = (left?.input_tokens ?? 0) + right.input_tokens;
4
- const outputTokens = (left?.output_tokens ?? 0) + right.output_tokens;
5
- const totalTokens = (left?.total_tokens ?? 0) + right.total_tokens;
6
- if (![inputTokens, outputTokens, totalTokens].every(Number.isSafeInteger))
7
- return undefined;
8
- return { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: totalTokens };
9
- }
10
- export function toUsage(usage) {
11
- const normalizeBreakdown = (breakdown) => {
12
- const inputTokens = breakdown?.input_tokens;
13
- const outputTokens = breakdown?.output_tokens;
14
- const totalTokens = breakdown?.total_tokens;
15
- if (typeof inputTokens !== "number" ||
16
- typeof outputTokens !== "number" ||
17
- typeof totalTokens !== "number" ||
18
- !Number.isSafeInteger(inputTokens) ||
19
- inputTokens < 0 ||
20
- !Number.isSafeInteger(outputTokens) ||
21
- outputTokens < 0 ||
22
- !Number.isSafeInteger(totalTokens) ||
23
- totalTokens < 0) {
24
- return undefined;
25
- }
26
- return { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: totalTokens };
27
- };
28
- const total = normalizeBreakdown(usage?.total);
29
- const last = normalizeBreakdown(usage?.last);
30
- const modelContextWindow = usage?.model_context_window;
31
- if (!total || !last)
32
- return undefined;
33
- return {
34
- total,
35
- last,
36
- ...(typeof modelContextWindow === "number" && Number.isSafeInteger(modelContextWindow) && modelContextWindow > 0
37
- ? { context: { used_tokens: last.total_tokens, window_tokens: modelContextWindow } }
38
- : {}),
39
- };
40
- }
41
- export function textFromContent(content) {
42
- if (typeof content === "string")
43
- return content;
44
- return content
45
- .filter((part) => part.type === "input_text")
46
- .map((part) => part.text ?? "")
47
- .join("\n");
48
- }
49
- export function inputToPrompt(input, instructions, isNewSession, systemReminder) {
50
- const leadingTexts = (isNewSession ? [systemReminder, instructions] : [systemReminder]).filter((text) => Boolean(text));
51
- const prefix = leadingTexts.length > 0 ? `${leadingTexts.join("\n\n")}\n\n` : "";
52
- if (typeof input === "string")
53
- return `${prefix}${input}`;
54
- if (!Array.isArray(input)) {
55
- throw new HTTPException(400, { message: "Invalid input" });
56
- }
57
- const lastUserMsg = [...input]
58
- .reverse()
59
- .find((message) => {
60
- return Boolean(message && typeof message === "object" && "role" in message && message.role === "user");
61
- });
62
- if (!lastUserMsg) {
63
- throw new HTTPException(400, { message: "No user message found" });
64
- }
65
- let systemText = instructions;
66
- if (isNewSession && !systemText) {
67
- const sysMsg = [...input]
68
- .reverse()
69
- .find((message) => {
70
- return (Boolean(message && typeof message === "object" && "role" in message) &&
71
- (message.role === "system" || message.role === "developer"));
72
- });
73
- if (sysMsg) {
74
- systemText = textFromContent(sysMsg.content);
75
- }
76
- }
77
- const arrayLeadingTexts = isNewSession
78
- ? [systemReminder, systemText].filter((text) => Boolean(text))
79
- : [];
80
- return `${arrayLeadingTexts.length > 0 ? `${arrayLeadingTexts.join("\n\n")}\n\n` : ""}${textFromContent(lastUserMsg.content)}`;
81
- }
82
- export function turnOptions(agentMode, model) {
83
- return {
84
- collaborationMode: {
85
- mode: agentMode === "plan" ? "plan" : "default",
86
- settings: { model: model || null, reasoning_effort: "medium", developer_instructions: null },
87
- },
88
- };
89
- }
90
- export function itemText(item) {
91
- if (item.type === "agentMessage") {
92
- return item.text;
93
- }
94
- if (item.type === "reasoning")
95
- return item.summary.join("\n") || item.content.join("\n");
96
- if (item.type === "plan")
97
- return item.text;
98
- return undefined;
99
- }
100
- export function commandExecutionItem(item) {
101
- if (item.type !== "commandExecution") {
102
- return undefined;
103
- }
104
- return item;
105
- }
106
- export function fileChangeItem(item) {
107
- if (item.type !== "fileChange") {
108
- return undefined;
109
- }
110
- return item;
111
- }
112
- export function mcpToolCallItem(item) {
113
- if (item.type !== "mcpToolCall") {
114
- return undefined;
115
- }
116
- return item;
117
- }
118
- export function dynamicToolCallItem(item) {
119
- if (item.type !== "dynamicToolCall") {
120
- return undefined;
121
- }
122
- return item;
123
- }
124
- export function webSearchItem(item) {
125
- if (item.type !== "webSearch") {
126
- return undefined;
127
- }
128
- return item;
129
- }
130
- function textFromMcpContent(content) {
131
- if (!Array.isArray(content))
132
- return undefined;
133
- const text = content
134
- .map((part) => {
135
- if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part) {
136
- return typeof part.text === "string" ? part.text : "";
137
- }
138
- return "";
139
- })
140
- .filter((part) => part.length > 0)
141
- .join("\n");
142
- return text.length > 0 ? text : undefined;
143
- }
144
- export function mcpToolCallOutput(item) {
145
- const structuredContent = item.result?.structuredContent;
146
- if (structuredContent &&
147
- typeof structuredContent === "object" &&
148
- "filePath" in structuredContent &&
149
- typeof structuredContent.filePath === "string") {
150
- return structuredContent.filePath;
151
- }
152
- return textFromMcpContent(item.result?.content) ?? JSON.stringify(item.result ?? null);
153
- }
154
- function parseShellWord(input) {
155
- let index = 0;
156
- let value = "";
157
- while (index < input.length) {
158
- const char = input[index];
159
- if (/\s/.test(char))
160
- break;
161
- if (char === "'") {
162
- index += 1;
163
- while (index < input.length) {
164
- if (input[index] === "'") {
165
- index += 1;
166
- break;
167
- }
168
- value += input[index];
169
- index += 1;
170
- }
171
- continue;
172
- }
173
- if (char === '"') {
174
- index += 1;
175
- while (index < input.length) {
176
- const innerChar = input[index];
177
- if (innerChar === '"') {
178
- index += 1;
179
- break;
180
- }
181
- if (innerChar === "\\" && index + 1 < input.length) {
182
- value += input[index + 1];
183
- index += 2;
184
- continue;
185
- }
186
- value += innerChar;
187
- index += 1;
188
- }
189
- continue;
190
- }
191
- if (char === "\\" && index + 1 < input.length) {
192
- value += input[index + 1];
193
- index += 2;
194
- continue;
195
- }
196
- value += char;
197
- index += 1;
198
- }
199
- return input.slice(index).trim() === "" ? value : undefined;
200
- }
201
- export function displayCommand(command) {
202
- const match = command.match(/^\/bin\/bash\s+-lc\s+([\s\S]+)$/);
203
- if (!match)
204
- return command;
205
- return parseShellWord(match[1].trim()) ?? command;
206
- }
207
- export function dynamicToolCallOutput(item) {
208
- const texts = (item.contentItems ?? [])
209
- .map((content) => (content.type === "inputText" ? content.text : content.imageUrl))
210
- .filter((text) => text.length > 0);
211
- return texts.join("\n");
212
- }
213
- export function webSearchPayload(item) {
214
- return {
215
- query: item.query,
216
- ...(item.action ? { action: item.action } : null),
217
- };
218
- }
1
+ import{HTTPException as a}from"hono/http-exception";function h(t,e){const n=(t?.input_tokens??0)+e.input_tokens,o=(t?.output_tokens??0)+e.output_tokens,r=(t?.total_tokens??0)+e.total_tokens;if([n,o,r].every(Number.isSafeInteger))return{input_tokens:n,output_tokens:o,total_tokens:r}}function k(t){const e=s=>{const l=s?.input_tokens,i=s?.output_tokens,f=s?.total_tokens;if(!(typeof l!="number"||typeof i!="number"||typeof f!="number"||!Number.isSafeInteger(l)||l<0||!Number.isSafeInteger(i)||i<0||!Number.isSafeInteger(f)||f<0))return{input_tokens:l,output_tokens:i,total_tokens:f}},n=e(t?.total),o=e(t?.last),r=t?.model_context_window;if(!(!n||!o))return{total:n,last:o,...typeof r=="number"&&Number.isSafeInteger(r)&&r>0?{context:{used_tokens:o.total_tokens,window_tokens:r}}:{}}}function p(t){return typeof t=="string"?t:t.filter(e=>e.type==="input_text").map(e=>e.text??"").join(`
2
+ `)}function g(t,e,n,o){const r=(n?[o,e]:[o]).filter(u=>!!u),s=r.length>0?`${r.join(`
3
+
4
+ `)}
5
+
6
+ `:"";if(typeof t=="string")return`${s}${t}`;if(!Array.isArray(t))throw new a(400,{message:"Invalid input"});const l=[...t].reverse().find(u=>!!(u&&typeof u=="object"&&"role"in u&&u.role==="user"));if(!l)throw new a(400,{message:"No user message found"});let i=e;if(n&&!i){const u=[...t].reverse().find(c=>!!(c&&typeof c=="object"&&"role"in c)&&(c.role==="system"||c.role==="developer"));u&&(i=p(u.content))}const f=n?[o,i].filter(u=>!!u):[];return`${f.length>0?`${f.join(`
7
+
8
+ `)}
9
+
10
+ `:""}${p(l.content)}`}function m(t,e){return{collaborationMode:{mode:t==="plan"?"plan":"default",settings:{model:e||null,reasoning_effort:"medium",developer_instructions:null}}}}function _(t){if(t.type==="agentMessage")return t.text;if(t.type==="reasoning")return t.summary.join(`
11
+ `)||t.content.join(`
12
+ `);if(t.type==="plan")return t.text}function T(t){if(t.type==="commandExecution")return t}function b(t){if(t.type==="fileChange")return t}function C(t){if(t.type==="mcpToolCall")return t}function w(t){if(t.type==="dynamicToolCall")return t}function I(t){if(t.type==="webSearch")return t}function d(t){if(!Array.isArray(t))return;const e=t.map(n=>n&&typeof n=="object"&&"type"in n&&n.type==="text"&&"text"in n&&typeof n.text=="string"?n.text:"").filter(n=>n.length>0).join(`
13
+ `);return e.length>0?e:void 0}function j(t){const e=t.result?.structuredContent;return e&&typeof e=="object"&&"filePath"in e&&typeof e.filePath=="string"?e.filePath:d(t.result?.content)??JSON.stringify(t.result??null)}function x(t){let e=0,n="";for(;e<t.length;){const o=t[e];if(/\s/.test(o))break;if(o==="'"){for(e+=1;e<t.length;){if(t[e]==="'"){e+=1;break}n+=t[e],e+=1}continue}if(o==='"'){for(e+=1;e<t.length;){const r=t[e];if(r==='"'){e+=1;break}if(r==="\\"&&e+1<t.length){n+=t[e+1],e+=2;continue}n+=r,e+=1}continue}if(o==="\\"&&e+1<t.length){n+=t[e+1],e+=2;continue}n+=o,e+=1}return t.slice(e).trim()===""?n:void 0}function S(t){const e=t.match(/^\/bin\/bash\s+-lc\s+([\s\S]+)$/);return e?x(e[1].trim())??t:t}function v(t){return(t.contentItems??[]).map(n=>n.type==="inputText"?n.text:n.imageUrl).filter(n=>n.length>0).join(`
14
+ `)}function $(t){return{query:t.query,...t.action?{action:t.action}:null}}export{h as addTokenUsage,T as commandExecutionItem,S as displayCommand,w as dynamicToolCallItem,v as dynamicToolCallOutput,b as fileChangeItem,g as inputToPrompt,_ as itemText,C as mcpToolCallItem,j as mcpToolCallOutput,p as textFromContent,k as toUsage,m as turnOptions,I as webSearchItem,$ as webSearchPayload};
package/dist/schemas.js CHANGED
@@ -1,72 +1 @@
1
- import { z } from "zod";
2
- export const ChatTextPartSchema = z.object({
3
- type: z.literal("text"),
4
- text: z.string(),
5
- });
6
- export const ChatFilePartSchema = z.object({
7
- type: z.literal("file"),
8
- mediaType: z.string(),
9
- filename: z.string().optional(),
10
- url: z.string(),
11
- });
12
- export const ChatStepStartPartSchema = z.object({
13
- type: z.literal("step-start"),
14
- });
15
- export const ChatToolPartSchema = z.object({
16
- type: z.literal("tool"),
17
- toolCallId: z.string(),
18
- toolName: z.string(),
19
- title: z.string().optional(),
20
- state: z.string(),
21
- input: z.unknown().optional(),
22
- output: z.unknown().optional(),
23
- approval: z
24
- .object({
25
- id: z.string(),
26
- approved: z.boolean().optional(),
27
- reason: z.string().optional(),
28
- })
29
- .optional(),
30
- });
31
- export const ChatCustomPartSchema = z.looseObject({
32
- type: z.string(),
33
- });
34
- export const CodexChatForkSchema = z.strictObject({
35
- threadId: z.string().min(1),
36
- turnId: z.string().min(1),
37
- });
38
- export const ClaudeChatForkSchema = z.strictObject({
39
- sessionId: z.string().min(1),
40
- messageId: z.string().min(1),
41
- });
42
- export const ChatForkSchema = z.union([CodexChatForkSchema, ClaudeChatForkSchema]);
43
- export const ChatBodySchema = z.object({
44
- conversationId: z.string().optional(),
45
- fork: ChatForkSchema.optional(),
46
- model: z.string(),
47
- message: z.object({
48
- id: z.string().optional(),
49
- role: z.enum(["user", "assistant"]).optional(),
50
- parts: z.array(z.union([
51
- ChatTextPartSchema,
52
- ChatFilePartSchema,
53
- ChatStepStartPartSchema,
54
- ChatToolPartSchema,
55
- ChatCustomPartSchema,
56
- ])),
57
- }),
58
- });
59
- export const ToolRequestUserInputAnswerSchema = z.object({
60
- answers: z.array(z.string()),
61
- });
62
- export const ToolRequestUserInputResponseSchema = z.object({
63
- answers: z.record(z.string(), ToolRequestUserInputAnswerSchema),
64
- });
65
- export const RequestUserInputToolPartSchema = z.object({
66
- type: z.literal("tool"),
67
- toolCallId: z.string(),
68
- toolName: z.literal("request_user_input"),
69
- state: z.literal("output-available"),
70
- sourceTurnId: z.string().min(1).optional(),
71
- output: ToolRequestUserInputResponseSchema,
72
- });
1
+ import{z as t}from"zod";const o=t.object({type:t.literal("text"),text:t.string()}),e=t.object({type:t.literal("file"),mediaType:t.string(),filename:t.string().optional(),url:t.string()}),r=t.object({type:t.literal("step-start")}),n=t.object({type:t.literal("tool"),toolCallId:t.string(),toolName:t.string(),title:t.string().optional(),state:t.string(),input:t.unknown().optional(),output:t.unknown().optional(),approval:t.object({id:t.string(),approved:t.boolean().optional(),reason:t.string().optional()}).optional()}),a=t.looseObject({type:t.string()}),s=t.strictObject({threadId:t.string().min(1),turnId:t.string().min(1)}),i=t.strictObject({sessionId:t.string().min(1),messageId:t.string().min(1)}),l=t.union([s,i]),u=t.object({conversationId:t.string().optional(),fork:l.optional(),model:t.string(),message:t.object({id:t.string().optional(),role:t.enum(["user","assistant"]).optional(),parts:t.array(t.union([o,e,r,n,a]))})}),p=t.object({answers:t.array(t.string())}),c=t.object({answers:t.record(t.string(),p)}),g=t.object({type:t.literal("tool"),toolCallId:t.string(),toolName:t.literal("request_user_input"),state:t.literal("output-available"),sourceTurnId:t.string().min(1).optional(),output:c});export{u as ChatBodySchema,a as ChatCustomPartSchema,e as ChatFilePartSchema,l as ChatForkSchema,r as ChatStepStartPartSchema,o as ChatTextPartSchema,n as ChatToolPartSchema,i as ClaudeChatForkSchema,s as CodexChatForkSchema,g as RequestUserInputToolPartSchema,p as ToolRequestUserInputAnswerSchema,c as ToolRequestUserInputResponseSchema};
@@ -1,232 +1 @@
1
- import { createRequire } from "node:module";
2
- import WebSocket from "ws";
3
- import { ChatBodySchema } from "./schemas.js";
4
- import { appConfig } from "./config.js";
5
- import { cancelCodexConversation, ChatRequestError, handleCodexChatStream } from "./routes/chat.js";
6
- import { logger } from "@messenger-agent/shared/logger";
7
- import { sendHttpTunnelResponse } from "@messenger-agent/shared/tunnel-http";
8
- import { CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE, CODING_AGENT_TUNNEL_PROTOCOL_VERSION, } from "@messenger-agent/shared/tunnel-protocol";
9
- const clientVersion = createRequire(import.meta.url)("../package.json").version;
10
- function delay(ms) {
11
- return new Promise((resolve) => setTimeout(resolve, ms));
12
- }
13
- function parseServerMessage(raw) {
14
- try {
15
- const value = JSON.parse(raw.toString());
16
- if (!value || typeof value !== "object" || !("type" in value))
17
- return undefined;
18
- return value;
19
- }
20
- catch {
21
- return undefined;
22
- }
23
- }
24
- function headersFromRecord(headers) {
25
- const result = new Headers();
26
- for (const [key, value] of Object.entries(headers ?? {})) {
27
- result.set(key, value);
28
- }
29
- return result;
30
- }
31
- function cancelConversationId(message) {
32
- if (message.method !== "POST")
33
- return undefined;
34
- try {
35
- const pathname = new URL(message.path, "http://coding-agent-tunnel.local").pathname;
36
- const match = /^\/v1\/chat\/([^/]+)\/cancel$/.exec(pathname);
37
- return match ? decodeURIComponent(match[1]) : undefined;
38
- }
39
- catch {
40
- return undefined;
41
- }
42
- }
43
- export class CodexTunnelClient {
44
- config;
45
- stopped = false;
46
- socket;
47
- running = new Map();
48
- constructor(config) {
49
- this.config = config;
50
- }
51
- async start() {
52
- if (!this.config.enabled)
53
- return;
54
- if (!this.config.serverUrl || !this.config.tunnelId || !this.config.token) {
55
- throw new Error("Tunnel config requires server_url, tunnel_id, and token when enabled");
56
- }
57
- let backoffMs = this.config.reconnectInitialMs;
58
- while (!this.stopped) {
59
- try {
60
- await this.connectOnce();
61
- backoffMs = this.config.reconnectInitialMs;
62
- }
63
- catch (err) {
64
- if (this.stopped)
65
- break;
66
- logger.warn("[TunnelClient] websocket tunnel connection failed:", err);
67
- }
68
- if (!this.stopped) {
69
- await delay(backoffMs);
70
- backoffMs = Math.min(backoffMs * 2, this.config.reconnectMaxMs);
71
- }
72
- }
73
- }
74
- stop() {
75
- this.stopped = true;
76
- for (const request of this.running.values()) {
77
- request.abortController.abort();
78
- }
79
- this.running.clear();
80
- this.socket?.close();
81
- }
82
- connectOnce() {
83
- return new Promise((resolve, reject) => {
84
- const headers = { Authorization: `Bearer ${this.config.token}` };
85
- const ws = new WebSocket(this.config.serverUrl, { headers });
86
- this.socket = ws;
87
- let heartbeatTimer;
88
- let settled = false;
89
- const cleanup = () => {
90
- if (heartbeatTimer)
91
- clearInterval(heartbeatTimer);
92
- if (this.socket === ws)
93
- this.socket = undefined;
94
- for (const request of this.running.values()) {
95
- request.abortController.abort(new Error("Tunnel connection closed"));
96
- }
97
- this.running.clear();
98
- };
99
- ws.on("open", () => {
100
- this.send({
101
- type: "hello",
102
- protocolVersion: CODING_AGENT_TUNNEL_PROTOCOL_VERSION,
103
- tunnelId: this.config.tunnelId,
104
- agentType: "codex",
105
- clientVersion,
106
- });
107
- heartbeatTimer = setInterval(() => {
108
- this.send({ type: "heartbeat" });
109
- }, this.config.heartbeatIntervalMs);
110
- logger.info(`[TunnelClient] connected to ${this.config.serverUrl} as ${this.config.tunnelId}`);
111
- });
112
- ws.on("message", (raw) => {
113
- const message = parseServerMessage(raw);
114
- if (!message) {
115
- logger.warn("[TunnelClient] received invalid websocket message");
116
- return;
117
- }
118
- if (message.type === "heartbeat")
119
- return;
120
- if (message.type === "cancel") {
121
- this.running.get(message.id)?.abortController.abort();
122
- return;
123
- }
124
- if (message.type === "request") {
125
- this.handleRequest(message).catch((err) => {
126
- logger.error(`[TunnelClient] request ${message.id} failed:`, err);
127
- });
128
- }
129
- });
130
- ws.once("error", (err) => {
131
- if (!settled) {
132
- settled = true;
133
- cleanup();
134
- reject(err);
135
- }
136
- });
137
- ws.once("close", (code, reason) => {
138
- cleanup();
139
- if (code === CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE) {
140
- this.stopped = true;
141
- logger.warn(`[TunnelClient] duplicate websocket tunnel connection rejected; reconnect disabled: ${reason.toString()}`);
142
- }
143
- if (!settled) {
144
- settled = true;
145
- resolve();
146
- }
147
- });
148
- });
149
- }
150
- send(message) {
151
- const socket = this.socket;
152
- if (!socket || socket.readyState !== WebSocket.OPEN)
153
- return;
154
- socket.send(JSON.stringify(message));
155
- }
156
- async handleRequest(message) {
157
- const conversationId = cancelConversationId(message);
158
- if (conversationId) {
159
- let cancelled;
160
- try {
161
- cancelled = await cancelCodexConversation(conversationId);
162
- }
163
- catch (err) {
164
- const errorText = err instanceof Error ? err.message : "Failed to interrupt Codex conversation";
165
- const response = Response.json({ error: { type: "server_error", code: "cancel_failed", message: errorText } }, { status: 500 });
166
- await sendHttpTunnelResponse(message.id, response, (responseMessage) => this.send(responseMessage));
167
- return;
168
- }
169
- const response = Response.json(cancelled
170
- ? { success: true }
171
- : {
172
- error: {
173
- type: "not_found_error",
174
- code: "conversation_not_found",
175
- message: `Conversation not found or already completed: ${conversationId}`,
176
- param: "conversation_id",
177
- },
178
- }, { status: cancelled ? 200 : 404 });
179
- await sendHttpTunnelResponse(message.id, response, (responseMessage) => this.send(responseMessage));
180
- return;
181
- }
182
- if (message.method !== "POST" || message.path !== "/v1/chat/stream") {
183
- this.send({
184
- type: "response.error",
185
- id: message.id,
186
- code: "unsupported_request",
187
- errorText: `Unsupported tunnel request: ${message.method} ${message.path}`,
188
- });
189
- return;
190
- }
191
- const body = ChatBodySchema.safeParse(message.body);
192
- if (!body.success) {
193
- this.send({
194
- type: "response.error",
195
- id: message.id,
196
- code: "invalid_request",
197
- errorText: `Invalid chat request body: ${body.error.message}`,
198
- });
199
- return;
200
- }
201
- const abortController = new AbortController();
202
- this.running.set(message.id, { abortController });
203
- this.send({ type: "response.start", id: message.id, mode: "sse" });
204
- try {
205
- await handleCodexChatStream({
206
- body: body.data,
207
- headers: headersFromRecord(message.headers),
208
- signal: abortController.signal,
209
- }, {
210
- writeData: async (data) => this.send({ type: "response.chunk", id: message.id, data }),
211
- writeDone: async () => this.send({ type: "response.end", id: message.id }),
212
- });
213
- }
214
- catch (err) {
215
- const errorText = err instanceof Error ? err.message : "Tunnel request failed";
216
- const code = err instanceof ChatRequestError ? `http_${err.status}` : "request_failed";
217
- this.send({ type: "response.error", id: message.id, code, errorText });
218
- }
219
- finally {
220
- this.running.delete(message.id);
221
- }
222
- }
223
- }
224
- export function startCodexTunnelClient(config = appConfig.tunnel) {
225
- if (!config.enabled)
226
- return undefined;
227
- const client = new CodexTunnelClient(config);
228
- client.start().catch((err) => {
229
- logger.error("[TunnelClient] stopped unexpectedly:", err);
230
- });
231
- return client;
232
- }
1
+ import{createRequire as p}from"node:module";import h from"ws";import{ChatBodySchema as y}from"./schemas.js";import{appConfig as C}from"./config.js";import{cancelCodexConversation as b,ChatRequestError as g,handleCodexChatStream as m}from"./routes/chat.js";import{logger as l}from"@messenger-agent/shared/logger";import{sendHttpTunnelResponse as f}from"@messenger-agent/shared/tunnel-http";import{CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE as w,CODING_AGENT_TUNNEL_PROTOCOL_VERSION as v}from"@messenger-agent/shared/tunnel-protocol";const T=p(import.meta.url)("../package.json").version;function _(r){return new Promise(e=>setTimeout(e,r))}function k(r){try{const e=JSON.parse(r.toString());return!e||typeof e!="object"||!("type"in e)?void 0:e}catch{return}}function I(r){const e=new Headers;for(const[t,d]of Object.entries(r??{}))e.set(t,d);return e}function O(r){if(r.method==="POST")try{const e=new URL(r.path,"http://coding-agent-tunnel.local").pathname,t=/^\/v1\/chat\/([^/]+)\/cancel$/.exec(e);return t?decodeURIComponent(t[1]):void 0}catch{return}}class q{config;stopped=!1;socket;running=new Map;constructor(e){this.config=e}async start(){if(!this.config.enabled)return;if(!this.config.serverUrl||!this.config.tunnelId||!this.config.token)throw new Error("Tunnel config requires server_url, tunnel_id, and token when enabled");let e=this.config.reconnectInitialMs;for(;!this.stopped;){try{await this.connectOnce(),e=this.config.reconnectInitialMs}catch(t){if(this.stopped)break;l.warn("[TunnelClient] websocket tunnel connection failed:",t)}this.stopped||(await _(e),e=Math.min(e*2,this.config.reconnectMaxMs))}}stop(){this.stopped=!0;for(const e of this.running.values())e.abortController.abort();this.running.clear(),this.socket?.close()}connectOnce(){return new Promise((e,t)=>{const d={Authorization:`Bearer ${this.config.token}`},i=new h(this.config.serverUrl,{headers:d});this.socket=i;let n,a=!1;const s=()=>{n&&clearInterval(n),this.socket===i&&(this.socket=void 0);for(const c of this.running.values())c.abortController.abort(new Error("Tunnel connection closed"));this.running.clear()};i.on("open",()=>{this.send({type:"hello",protocolVersion:v,tunnelId:this.config.tunnelId,agentType:"codex",clientVersion:T}),n=setInterval(()=>{this.send({type:"heartbeat"})},this.config.heartbeatIntervalMs),l.info(`[TunnelClient] connected to ${this.config.serverUrl} as ${this.config.tunnelId}`)}),i.on("message",c=>{const o=k(c);if(!o){l.warn("[TunnelClient] received invalid websocket message");return}if(o.type!=="heartbeat"){if(o.type==="cancel"){this.running.get(o.id)?.abortController.abort();return}o.type==="request"&&this.handleRequest(o).catch(u=>{l.error(`[TunnelClient] request ${o.id} failed:`,u)})}}),i.once("error",c=>{a||(a=!0,s(),t(c))}),i.once("close",(c,o)=>{s(),c===w&&(this.stopped=!0,l.warn(`[TunnelClient] duplicate websocket tunnel connection rejected; reconnect disabled: ${o.toString()}`)),a||(a=!0,e())})})}send(e){const t=this.socket;!t||t.readyState!==h.OPEN||t.send(JSON.stringify(e))}async handleRequest(e){const t=O(e);if(t){let n;try{n=await b(t)}catch(s){const c=s instanceof Error?s.message:"Failed to interrupt Codex conversation",o=Response.json({error:{type:"server_error",code:"cancel_failed",message:c}},{status:500});await f(e.id,o,u=>this.send(u));return}const a=Response.json(n?{success:!0}:{error:{type:"not_found_error",code:"conversation_not_found",message:`Conversation not found or already completed: ${t}`,param:"conversation_id"}},{status:n?200:404});await f(e.id,a,s=>this.send(s));return}if(e.method!=="POST"||e.path!=="/v1/chat/stream"){this.send({type:"response.error",id:e.id,code:"unsupported_request",errorText:`Unsupported tunnel request: ${e.method} ${e.path}`});return}const d=y.safeParse(e.body);if(!d.success){this.send({type:"response.error",id:e.id,code:"invalid_request",errorText:`Invalid chat request body: ${d.error.message}`});return}const i=new AbortController;this.running.set(e.id,{abortController:i}),this.send({type:"response.start",id:e.id,mode:"sse"});try{await m({body:d.data,headers:I(e.headers),signal:i.signal},{writeData:async n=>this.send({type:"response.chunk",id:e.id,data:n}),writeDone:async()=>this.send({type:"response.end",id:e.id})})}catch(n){const a=n instanceof Error?n.message:"Tunnel request failed",s=n instanceof g?`http_${n.status}`:"request_failed";this.send({type:"response.error",id:e.id,code:s,errorText:a})}finally{this.running.delete(e.id)}}}function P(r=C.tunnel){if(!r.enabled)return;const e=new q(r);return e.start().catch(t=>{l.error("[TunnelClient] stopped unexpectedly:",t)}),e}export{q as CodexTunnelClient,P as startCodexTunnelClient};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@messenger-agent/codex-agent",
3
- "version": "0.24.0-alpha.2",
3
+ "version": "0.24.0-alpha.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "ws": "^8.21.0",
23
23
  "yaml": "^2.9.0",
24
24
  "zod": "^4.4.3",
25
- "@messenger-agent/shared": "0.24.0-alpha.2"
25
+ "@messenger-agent/shared": "0.24.0-alpha.3"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/ws": "^8.18.1"