@messenger-agent/codex-agent 0.26.0-alpha.5 → 0.26.0-alpha.7

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.
package/dist/app.js CHANGED
@@ -1 +1 @@
1
- import{Hono as e}from"hono";import{logger as i}from"hono/logger";import m from"./routes/chat.js";import{logger as p}from"@messenger-agent/shared/logger";import{agentAuthMiddleware as n}from"@messenger-agent/shared/agent-auth";import{appConfig as a}from"./config.js";import{installAgentActivityResponder as f}from"@messenger-agent/shared/agent-activity";import{codexActivitySnapshot as s}from"./routes/chat.js";const o=new e;f("codex",s),o.use(i((t,...r)=>p.info(t,...r))),o.get("/health",t=>t.json({status:"ok"})),o.use("/*",n(a.authTokens)),o.route("/",m);var k=o;export{k as default};
1
+ import{Hono as e}from"hono";import r from"./routes/chat.js";import{infoRequestLogger as i}from"@messenger-agent/shared/http-logger";import{agentAuthMiddleware as m}from"@messenger-agent/shared/agent-auth";import{appConfig as p}from"./config.js";import{installAgentActivityResponder as a}from"@messenger-agent/shared/agent-activity";import{codexActivitySnapshot as n}from"./routes/chat.js";const o=new e;a("codex",n),o.use(i(["/health"])),o.get("/health",t=>t.json({status:"ok"})),o.use("/*",m(p.authTokens)),o.route("/",r);var l=o;export{l as default};
package/dist/codex.d.ts CHANGED
@@ -27,6 +27,7 @@ export declare class CodexThread {
27
27
  usage?: Usage | null;
28
28
  }>;
29
29
  runStreamed(_prompt: string, options?: TurnRunOptions): Promise<StreamedTurn>;
30
+ private waitForTurnStart;
30
31
  steerStreamed(prompt: string, expectedTurnId: string, options?: {
31
32
  signal?: AbortSignal;
32
33
  }): Promise<StreamedTurn>;
package/dist/codex.js CHANGED
@@ -1,3 +1,3 @@
1
- import{mkdirSync as b,existsSync as _}from"node:fs";import{appServerClient as l}from"./app-server-client.js";import{dynamicToolSpecs as S}from"./dynamic-tools.js";import{loadAllSessions as U,persistSession as I}from"./db.js";import{appConfig as f}from"./config.js";import{logger as E}from"@messenger-agent/shared/logger";import{resolveAgentWorkdir as N}from"@messenger-agent/shared/agent-workspaces";import{codexPlatformInstructions as v}from"./platform-instructions.js";class P extends Error{path;constructor(e){super(`Workdir not found: ${e}`),this.path=e}}class k{id;model;workingDirectory;constructor(e,r,n){this.id=e,this.model=r,this.workingDirectory=n}async run(e,r){const n=await this.runStreamed(e,r),t=new Map,i=new Set;let a=null;for await(const u of n.events)if(u.type==="item.started")i.has(u.item.id)||t.set(u.item.id,u.item);else if(u.type==="item.completed")i.add(u.item.id),t.set(u.item.id,u.item);else if(u.type==="thread.tokenUsage.updated")a=u.usage;else if(u.type==="turn.failed")throw new Error(u.error.message);const d=Array.from(t.values()),o=d.filter(u=>u.type==="agentMessage").map(u=>u.text).join(`
2
- `);return{items:d,finalResponse:o||void 0,usage:a}}async runStreamed(e,r){const n=await l.request("turn/start",this.turnStartParams(e,r)),t=n.turn.id;return{turnId:t,events:this.eventsForTurn(t,n.turn,r?.signal)}}async steerStreamed(e,r,n){const t={threadId:this.id,input:[{type:"text",text:e}],expectedTurnId:r},i=await l.request("turn/steer",t);return{turnId:i.turnId,events:this.eventsForTurn(i.turnId,void 0,n?.signal,void 0,!0)}}async continueWithUserInput(e,r,n){const t=l.pendingUserInputTurnId(this.id,e);return t?{turnId:t,events:this.eventsForTurn(t,void 0,n?.signal,async()=>{if(!await l.respondToUserInput(this.id,e,r))throw new Error(`No pending user input request found for item: ${e}`)})}:(await this.interruptStaleTurn(n?.sourceTurnId),this.runStreamed(C(e,r),n))}async submitPendingUserInput(e,r){const n=l.pendingUserInputTurnId(this.id,e);if(!(!n||!await l.respondToUserInput(this.id,e,r)))return n}async interrupt(e){await l.request("turn/interrupt",{threadId:this.id,turnId:e})}async interruptPendingUserInput(e,r){await this.interrupt(e),l.discardPendingUserInput(this.id,r)}async interruptStaleTurn(e){if(!e)return;(await l.request("thread/read",{threadId:this.id,includeTurns:!0})).thread.turns?.find(t=>t.id===e)?.status==="inProgress"&&await this.interrupt(e)}turnStartParams(e,r){return{threadId:this.id,input:[{type:"text",text:e,text_elements:[]}],cwd:this.workingDirectory,approvalPolicy:"never",model:this.model,...r?.collaborationMode?{collaborationMode:r.collaborationMode}:null}}async*eventsForTurn(e,r,n,t,i=!1){const a=[...r||i?[{type:"thread.started",thread_id:this.id},{type:"turn.started",turn_id:e}]:[],...r?F(r):[]];let d=r?.status==="completed"||r?.status==="failed"||r?.status==="interrupted",o,u=!1,p=()=>{};const c=({method:h,params:w})=>{if(u)return;const y=M(h,w,this.id,e);y&&(a.push(y),(y.type==="turn.completed"||y.type==="turn.failed")&&(d=!0),p())},g=h=>{o=h,d=!0,p()},m=()=>{u=!0,a.length=0,d=!0,p()};l.on("notification",c),l.on("exit",g),n?.addEventListener("abort",m,{once:!0}),n?.aborted&&m();try{for(await t?.();!d||a.length>0;){for(;a.length>0;)yield a.shift();if(d)break;await new Promise(h=>{const w=setTimeout(()=>{p=()=>{},h()},50);p=()=>{clearTimeout(w),p=()=>{},h()}})}o&&(yield{type:"error",message:o.message})}finally{l.off("notification",c),l.off("exit",g),n?.removeEventListener("abort",m)}}}function C(s,e){return[`The user answered a previous request_user_input prompt (${s}), but the original pending tool request is no longer active.`,"Continue the task using this answer:",JSON.stringify(e,null,2)].join(`
3
- `)}class A{sessions=new Map;constructor(){for(const e of U())this.sessions.set(e.sessionId,{workdir:e.workdir,botName:e.botName??"main",llmProxyBinding:e.llmProxyBinding});E.info(`SessionManager: loaded ${this.sessions.size} persisted sessions`)}async getOrCreate(e,r,n,t,i,a,d){const o=this.resolveWorkdir(n,t);if(e){const p=this.sessions.get(e);if(p&&p.workdir===o){const c=p.llmProxyBinding??d,g=await l.request("thread/resume",this.threadResumeParams(e,i,o,a,c));if(!p.llmProxyBinding&&c){const m={...p,llmProxyBinding:c};this.sessions.set(e,m),I({sessionId:e,botName:m.botName,workdir:m.workdir,createdAt:Date.now(),llmProxyBinding:c})}return{thread:new k(g.thread.id,i,o),sessionId:e,isNewSession:!1}}}if(n&&!_(o))throw new P(o);b(o,{recursive:!0});const u=await l.request("thread/start",this.threadStartParams(i,o,a,d));return{thread:new k(u.thread.id,i,o),sessionId:void 0,isNewSession:!0}}async fork(e,r,n,t,i,a,d){const o=this.resolveWorkdir(n,t);if(n&&!_(o))throw new P(o);b(o,{recursive:!0});const p=(await l.request("thread/fork",this.threadForkParams(e.threadId,e.turnId,i,o,a,d))).thread.id;return this.persist(p,r,n,t,d),{thread:new k(p,i,o),sessionId:p,isNewSession:!1}}persist(e,r,n,t,i){const a=r??"main",d=this.resolveWorkdir(n,t);this.sessions.set(e,{botName:a,workdir:d,llmProxyBinding:i}),I({sessionId:e,botName:a,workdir:d,createdAt:Date.now(),llmProxyBinding:i})}bindingFor(e){return e?this.sessions.get(e)?.llmProxyBinding:void 0}workdirFor(e){return e?this.sessions.get(e)?.workdir:void 0}hasSession(e){return!!e&&this.sessions.has(e)}resolveWorkdir(e,r){return N(f,r,e)}threadStartParams(e,r,n,t){return{model:e,modelProvider:f.codex.baseUrl?"custom":null,cwd:r,approvalPolicy:"never",sandbox:n,config:this.codexConfig(t),developerInstructions:v,dynamicTools:S(),experimentalRawEvents:!1,persistExtendedHistory:!1}}threadResumeParams(e,r,n,t,i){return{threadId:e,model:r,modelProvider:f.codex.baseUrl?"custom":null,cwd:n,approvalPolicy:"never",sandbox:t,config:this.codexConfig(i),developerInstructions:v,excludeTurns:!0,persistExtendedHistory:!1}}threadForkParams(e,r,n,t,i,a){return{threadId:e,lastTurnId:r,model:n,modelProvider:f.codex.baseUrl?"custom":null,cwd:t,approvalPolicy:"never",sandbox:i,config:this.codexConfig(a),developerInstructions:v,ephemeral:!1,excludeTurns:!0}}codexConfig(e){return{approval_policy:"never",...f.codex.baseUrl?{model_provider:"custom",model_reasoning_effort:"medium",disable_response_storage:!0,model_providers:{custom:{name:"custom",base_url:f.codex.baseUrl,wire_api:"responses",...f.codex.apiKey?{env_key:"OPENAI_API_KEY"}:null,...e?{http_headers:{"X-Elevo-LLM-Binding":e}}:null}},"features.default_mode_request_user_input":!0}:null}}}f.codex.apiKey&&!process.env.OPENAI_API_KEY&&(process.env.OPENAI_API_KEY=f.codex.apiKey);function F(s){const e=[];for(const r of s.items??[])e.push({type:"item.completed",item:r});return s.status==="completed"?e.push({type:"turn.completed"}):s.status==="failed"&&e.push({type:"turn.failed",error:{message:s.error?.message??"Codex turn failed"}}),e}function M(s,e,r,n){const t=e;if(!t)return;const i=t.threadId,a=t.turnId;if(!(typeof i=="string"&&i!==r)&&!(typeof a=="string"&&a!==n)){if(s==="item/started"&&T(t.item))return{type:"item.started",item:t.item};if(s==="item/completed"&&T(t.item))return{type:"item.completed",item:t.item};if(s==="item/agentMessage/delta")return x(t,"agentMessage");if(s==="item/reasoning/summaryTextDelta"||s==="item/reasoning/textDelta")return x(t,"reasoning");if(s==="item/plan/delta")return x(t,"plan");if(s==="item/tool/requestUserInput")return{type:"item.tool.requestUserInput",params:t};if(s==="turn/diff/updated"&&typeof t.diff=="string")return{type:"turn.diff.updated",diff:t.diff};if(s==="thread/tokenUsage/updated")return{type:"thread.tokenUsage.updated",usage:R(t)};if(s==="turn/completed"){const d=t.turn;return d?.status==="failed"?{type:"turn.failed",error:{message:d.error?.message??"Codex turn failed"}}:{type:"turn.completed"}}if(s==="error")return console.error("Received error notification from app-server:",t),{type:"error",message:q(t)}}}function q(s){const e=s.error,r=e&&typeof e=="object"&&"message"in e?e.message:void 0,n=s.additionalDetails,t=typeof r=="string"&&r.trim()?r:"Codex app-server error";return typeof n=="string"&&n.trim()?`${t}: ${n}`:t}function x(s,e){if(!(typeof s.itemId!="string"||typeof s.delta!="string"))return{type:"item.delta",item_id:s.itemId,item_type:e,delta:s.delta}}function R(s){const e=s.tokenUsage;if(!e||typeof e!="object")return null;const r=e,n=r.total,t=r.last;if(!n||typeof n!="object"||!t||typeof t!="object")return null;const i=d=>{const o=d;return{input_tokens:typeof o.inputTokens=="number"?o.inputTokens:void 0,output_tokens:typeof o.outputTokens=="number"?o.outputTokens:void 0,total_tokens:typeof o.totalTokens=="number"?o.totalTokens:void 0}},a=r.modelContextWindow;return{total:i(n),last:i(t),model_context_window:typeof a=="number"?a:void 0}}function T(s){return!!(s&&typeof s=="object"&&"type"in s&&"id"in s)}const Y=new A;export{k as CodexThread,P as WorkdirNotFoundError,Y as sessionManager};
1
+ import{mkdirSync as k,existsSync as P}from"node:fs";import{appServerClient as l}from"./app-server-client.js";import{dynamicToolSpecs as E}from"./dynamic-tools.js";import{loadAllSessions as U,persistSession as _}from"./db.js";import{appConfig as p}from"./config.js";import{logger as I}from"@messenger-agent/shared/logger";import{resolveAgentWorkdir as N}from"@messenger-agent/shared/agent-workspaces";import{loadCodexPlatformInstructions as C}from"./platform-instructions.js";const v=C(void 0,p.schedule?.enabled===!0);class T extends Error{path;constructor(e){super(`Workdir not found: ${e}`),this.path=e}}class x{id;model;workingDirectory;constructor(e,t,n){this.id=e,this.model=t,this.workingDirectory=n}async run(e,t){const n=await this.runStreamed(e,t),r=new Map,o=new Set;let a=null;for await(const u of n.events)if(u.type==="item.started")o.has(u.item.id)||r.set(u.item.id,u.item);else if(u.type==="item.completed")o.add(u.item.id),r.set(u.item.id,u.item);else if(u.type==="thread.tokenUsage.updated")a=u.usage;else if(u.type==="turn.failed")throw new Error(u.error.message);const d=Array.from(r.values()),i=d.filter(u=>u.type==="agentMessage").map(u=>u.text).join(`
2
+ `);return{items:d,finalResponse:i||void 0,usage:a}}async runStreamed(e,t){const n=l.request("turn/start",this.turnStartParams(e,t)),r=await this.waitForTurnStart(n,t?.signal),o=r.turn.id;return{turnId:o,events:this.eventsForTurn(o,r.turn,t?.signal)}}async waitForTurnStart(e,t){if(!t)return e;let n,r;const o=new Promise((d,i)=>{r=i}),a=()=>{n=t.reason??new DOMException("The operation was aborted","AbortError"),r(n)};t.addEventListener("abort",a,{once:!0}),t.aborted&&a();try{return await Promise.race([e,o])}catch(d){throw d===n&&e.then(i=>this.interrupt(i.turn.id)).catch(i=>I.error("Failed to clean up aborted Codex turn start:",i)),d}finally{t.removeEventListener("abort",a)}}async steerStreamed(e,t,n){const r={threadId:this.id,input:[{type:"text",text:e}],expectedTurnId:t},o=await l.request("turn/steer",r);return{turnId:o.turnId,events:this.eventsForTurn(o.turnId,void 0,n?.signal,void 0,!0)}}async continueWithUserInput(e,t,n){const r=l.pendingUserInputTurnId(this.id,e);return r?{turnId:r,events:this.eventsForTurn(r,void 0,n?.signal,async()=>{if(!await l.respondToUserInput(this.id,e,t))throw new Error(`No pending user input request found for item: ${e}`)})}:(await this.interruptStaleTurn(n?.sourceTurnId),this.runStreamed(A(e,t),n))}async submitPendingUserInput(e,t){const n=l.pendingUserInputTurnId(this.id,e);if(!(!n||!await l.respondToUserInput(this.id,e,t)))return n}async interrupt(e){await l.request("turn/interrupt",{threadId:this.id,turnId:e})}async interruptPendingUserInput(e,t){await this.interrupt(e),l.discardPendingUserInput(this.id,t)}async interruptStaleTurn(e){if(!e)return;(await l.request("thread/read",{threadId:this.id,includeTurns:!0})).thread.turns?.find(r=>r.id===e)?.status==="inProgress"&&await this.interrupt(e)}turnStartParams(e,t){return{threadId:this.id,input:[{type:"text",text:e,text_elements:[]}],cwd:this.workingDirectory,approvalPolicy:"never",model:this.model,...t?.collaborationMode?{collaborationMode:t.collaborationMode}:null}}async*eventsForTurn(e,t,n,r,o=!1){const a=[...t||o?[{type:"thread.started",thread_id:this.id},{type:"turn.started",turn_id:e}]:[],...t?M(t):[]];let d=t?.status==="completed"||t?.status==="failed"||t?.status==="interrupted",i,u=!1,c=()=>{};const f=({method:h,params:w})=>{if(u)return;const g=R(h,w,this.id,e);g&&(a.push(g),(g.type==="turn.completed"||g.type==="turn.failed")&&(d=!0),c())},y=h=>{i=h,d=!0,c()},m=()=>{u=!0,a.length=0,d=!0,c()};l.on("notification",f),l.on("exit",y),n?.addEventListener("abort",m,{once:!0}),n?.aborted&&m();try{for(await r?.();!d||a.length>0;){for(;a.length>0;)yield a.shift();if(d)break;await new Promise(h=>{const w=setTimeout(()=>{c=()=>{},h()},50);c=()=>{clearTimeout(w),c=()=>{},h()}})}i&&(yield{type:"error",message:i.message})}finally{l.off("notification",f),l.off("exit",y),n?.removeEventListener("abort",m)}}}function A(s,e){return[`The user answered a previous request_user_input prompt (${s}), but the original pending tool request is no longer active.`,"Continue the task using this answer:",JSON.stringify(e,null,2)].join(`
3
+ `)}class F{sessions=new Map;constructor(){for(const e of U())this.sessions.set(e.sessionId,{workdir:e.workdir,botName:e.botName??"main",llmProxyBinding:e.llmProxyBinding});I.info(`SessionManager: loaded ${this.sessions.size} persisted sessions`)}async getOrCreate(e,t,n,r,o,a,d){const i=this.resolveWorkdir(n,r);if(e){const c=this.sessions.get(e);if(c&&c.workdir===i){const f=c.llmProxyBinding??d,y=await l.request("thread/resume",this.threadResumeParams(e,o,i,a,f));if(!c.llmProxyBinding&&f){const m={...c,llmProxyBinding:f};this.sessions.set(e,m),_({sessionId:e,botName:m.botName,workdir:m.workdir,createdAt:Date.now(),llmProxyBinding:f})}return{thread:new x(y.thread.id,o,i),sessionId:e,isNewSession:!1}}}if(n&&!P(i))throw new T(i);k(i,{recursive:!0});const u=await l.request("thread/start",this.threadStartParams(o,i,a,d));return{thread:new x(u.thread.id,o,i),sessionId:void 0,isNewSession:!0}}async fork(e,t,n,r,o,a,d){const i=this.resolveWorkdir(n,r);if(n&&!P(i))throw new T(i);k(i,{recursive:!0});const c=(await l.request("thread/fork",this.threadForkParams(e.threadId,e.turnId,o,i,a,d))).thread.id;return this.persist(c,t,n,r,d),{thread:new x(c,o,i),sessionId:c,isNewSession:!1}}persist(e,t,n,r,o){const a=t??"main",d=this.resolveWorkdir(n,r);this.sessions.set(e,{botName:a,workdir:d,llmProxyBinding:o}),_({sessionId:e,botName:a,workdir:d,createdAt:Date.now(),llmProxyBinding:o})}bindingFor(e){return e?this.sessions.get(e)?.llmProxyBinding:void 0}workdirFor(e){return e?this.sessions.get(e)?.workdir:void 0}hasSession(e){return!!e&&this.sessions.has(e)}resolveWorkdir(e,t){return N(p,t,e)}threadStartParams(e,t,n,r){return{model:e,modelProvider:p.codex.baseUrl?"custom":null,cwd:t,approvalPolicy:"never",sandbox:n,config:this.codexConfig(r),developerInstructions:v,dynamicTools:E(p.schedule?.enabled===!0),experimentalRawEvents:!1,persistExtendedHistory:!1}}threadResumeParams(e,t,n,r,o){return{threadId:e,model:t,modelProvider:p.codex.baseUrl?"custom":null,cwd:n,approvalPolicy:"never",sandbox:r,config:this.codexConfig(o),developerInstructions:v,excludeTurns:!0,persistExtendedHistory:!1}}threadForkParams(e,t,n,r,o,a){return{threadId:e,lastTurnId:t,model:n,modelProvider:p.codex.baseUrl?"custom":null,cwd:r,approvalPolicy:"never",sandbox:o,config:this.codexConfig(a),developerInstructions:v,ephemeral:!1,excludeTurns:!0}}codexConfig(e){return{approval_policy:"never",...p.codex.baseUrl?{model_provider:"custom",model_reasoning_effort:"medium",disable_response_storage:!0,model_providers:{custom:{name:"custom",base_url:p.codex.baseUrl,wire_api:"responses",...p.codex.apiKey?{env_key:"OPENAI_API_KEY"}:null,...e?{http_headers:{"X-Elevo-LLM-Binding":e}}:null}},"features.default_mode_request_user_input":!0}:null}}}p.codex.apiKey&&!process.env.OPENAI_API_KEY&&(process.env.OPENAI_API_KEY=p.codex.apiKey);function M(s){const e=[];for(const t of s.items??[])e.push({type:"item.completed",item:t});return s.status==="completed"?e.push({type:"turn.completed"}):s.status==="failed"&&e.push({type:"turn.failed",error:{message:s.error?.message??"Codex turn failed"}}),e}function R(s,e,t,n){const r=e;if(!r)return;const o=r.threadId,a=r.turnId;if(!(typeof o=="string"&&o!==t)&&!(typeof a=="string"&&a!==n)){if(s==="item/started"&&S(r.item))return{type:"item.started",item:r.item};if(s==="item/completed"&&S(r.item))return{type:"item.completed",item:r.item};if(s==="item/agentMessage/delta")return b(r,"agentMessage");if(s==="item/reasoning/summaryTextDelta"||s==="item/reasoning/textDelta")return b(r,"reasoning");if(s==="item/plan/delta")return b(r,"plan");if(s==="item/tool/requestUserInput")return{type:"item.tool.requestUserInput",params:r};if(s==="turn/diff/updated"&&typeof r.diff=="string")return{type:"turn.diff.updated",diff:r.diff};if(s==="thread/tokenUsage/updated")return{type:"thread.tokenUsage.updated",usage:D(r)};if(s==="turn/completed"){const d=r.turn;return d?.status==="failed"?{type:"turn.failed",error:{message:d.error?.message??"Codex turn failed"}}:{type:"turn.completed"}}if(s==="error")return console.error("Received error notification from app-server:",r),{type:"error",message:q(r)}}}function q(s){const e=s.error,t=e&&typeof e=="object"&&"message"in e?e.message:void 0,n=s.additionalDetails,r=typeof t=="string"&&t.trim()?t:"Codex app-server error";return typeof n=="string"&&n.trim()?`${r}: ${n}`:r}function b(s,e){if(!(typeof s.itemId!="string"||typeof s.delta!="string"))return{type:"item.delta",item_id:s.itemId,item_type:e,delta:s.delta}}function D(s){const e=s.tokenUsage;if(!e||typeof e!="object")return null;const t=e,n=t.total,r=t.last;if(!n||typeof n!="object"||!r||typeof r!="object")return null;const o=d=>{const i=d;return{input_tokens:typeof i.inputTokens=="number"?i.inputTokens:void 0,output_tokens:typeof i.outputTokens=="number"?i.outputTokens:void 0,total_tokens:typeof i.totalTokens=="number"?i.totalTokens:void 0}},a=t.modelContextWindow;return{total:o(n),last:o(r),model_context_window:typeof a=="number"?a:void 0}}function S(s){return!!(s&&typeof s=="object"&&"type"in s&&"id"in s)}const H=new F;export{x as CodexThread,T as WorkdirNotFoundError,H as sessionManager};
package/dist/config.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type AgentAuthConfig, type AgentWorkspacesConfig, type GitInitConfig } from "@messenger-agent/shared/agent-config";
2
2
  import { type LogLevel } from "@messenger-agent/shared/logger";
3
+ import { type ScheduleServiceConfig } from "@messenger-agent/shared/schedule-tools";
3
4
  export type AppConfig = {
4
5
  configPath: string;
5
6
  logLevel: LogLevel;
@@ -19,6 +20,9 @@ export type AppConfig = {
19
20
  baseUrl?: string;
20
21
  imageModel: string;
21
22
  };
23
+ schedule: ScheduleServiceConfig & {
24
+ enabled: boolean;
25
+ };
22
26
  tunnel: {
23
27
  enabled: boolean;
24
28
  serverUrl?: string;
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import{existsSync as j,readFileSync as M}from"node:fs";import{tmpdir as f}from"node:os";import{join as a}from"node:path";import{parse as C}from"yaml";import{z as e}from"zod";import{normalizeOptional as n,parseAgentAuthConfig as _,parseAgentWorkspacesConfig as b,parseGitInitConfig as v}from"@messenger-agent/shared/agent-config";import{logger as s,normalizeLogLevel as I,setLogFile as x,setLogLevel as h}from"@messenger-agent/shared/logger";const L="wss://m.elevo.vip/agent-bridge/tunnel",y="https://m.elevo.vip/agent-bridge/llm/v1",U=e.object({log_level:e.string().optional(),port:e.coerce.number().int().positive().optional(),auth_tokens:e.record(e.string(),e.string()).optional(),workspaces:e.array(e.object({id:e.string().optional(),name:e.string().optional(),path:e.string().optional()})).optional(),file_uploads:e.object({temp_dir:e.string().optional()}).optional(),data_dir:e.string().optional(),codex:e.object({port:e.coerce.number().int().positive().optional(),data_dir:e.string().optional(),file_uploads:e.object({temp_dir:e.string().optional()}).optional(),api_key:e.string().optional(),base_url:e.string().optional()}).optional(),openai:e.object({api_key:e.string().optional(),base_url:e.string().optional(),image_model:e.string().optional()}).optional(),gitlab:e.object({ca_cert_path:e.string().optional(),host:e.string().optional()}).optional(),git:e.object({co_authors:e.array(e.string()).optional()}).optional(),tunnel:e.object({enabled:e.boolean().optional(),server_url:e.string().optional(),tunnel_id:e.string().optional(),token:e.string().optional(),reconnect_initial_ms:e.number().int().positive().optional(),reconnect_max_ms:e.number().int().positive().optional(),heartbeat_interval_ms:e.number().int().positive().optional()}).optional()}).loose();function D(){const t=process.env.AGENT_CONFIG_PATH??"./agent-config.yaml";if(!j(t)){const r="./data",u=a(r,"logs","codex-agent.log");return h("info"),x(u),s.warn(`Config file not found at ${t}`),{configPath:t,logLevel:"info",logFile:u,port:3e3,fileUploads:{tempDir:a(f(),"codex-agent-uploads")},dataDir:r,sessionsDbPath:a(r,"sessions.db"),codex:{},openai:{imageModel:"gpt-image-2"},tunnel:{enabled:!1,reconnectInitialMs:1e3,reconnectMaxMs:3e4,heartbeatIntervalMs:3e4},..._({}),...b({}),...v({})}}let p;try{p=C(M(t,"utf-8"))}catch(r){s.error(`Failed to parse config file at ${t}:`,r),p={}}const l=U.safeParse(p??{});l.success||s.error(`Invalid config format at ${t}:`,l.error.issues);const o=l.success?l.data:{},g=I(o.log_level),c=n(o.codex?.data_dir)??n(o.data_dir)??"./data",m=a(c,"logs","codex-agent.log"),i=o.tunnel?.enabled??!1,d=n(o.tunnel?.token);h(g),x(m);const k={configPath:t,logLevel:g,logFile:m,port:o.codex?.port??o.port??3e3,fileUploads:{tempDir:n(o.codex?.file_uploads?.temp_dir)??n(o.file_uploads?.temp_dir)??a(f(),"codex-agent-uploads")},dataDir:c,sessionsDbPath:a(c,"sessions.db"),codex:{apiKey:n(o.codex?.api_key)??(i?d:void 0),baseUrl:n(o.codex?.base_url)??(i?y:void 0)},openai:{apiKey:n(o.openai?.api_key)??(i?d:void 0),baseUrl:n(o.openai?.base_url)??(i?y:void 0),imageModel:n(o.openai?.image_model)??"gpt-image-2"},tunnel:{enabled:i,serverUrl:n(o.tunnel?.server_url)??(i?L:void 0),tunnelId:n(o.tunnel?.tunnel_id),token:d,reconnectInitialMs:o.tunnel?.reconnect_initial_ms??1e3,reconnectMaxMs:o.tunnel?.reconnect_max_ms??3e4,heartbeatIntervalMs:o.tunnel?.heartbeat_interval_ms??3e4},..._(o),...b(o),...v(o)};return s.info(`Loaded agent config from ${t}`),k}const z=D();export{z as appConfig};
1
+ import{existsSync as M,readFileSync as U}from"node:fs";import{tmpdir as b}from"node:os";import{join as a}from"node:path";import{parse as I}from"yaml";import{z as e}from"zod";import{normalizeOptional as n,parseAgentAuthConfig as v,parseAgentWorkspacesConfig as h,parseGitInitConfig as x}from"@messenger-agent/shared/agent-config";import{logger as s,normalizeLogLevel as L,setLogFile as y,setLogLevel as k}from"@messenger-agent/shared/logger";import{deriveScheduleServiceConfig as A}from"@messenger-agent/shared/schedule-tools";const D="wss://m.elevo.vip/agent-bridge/tunnel",j="https://m.elevo.vip/agent-bridge/llm/v1",F=e.object({log_level:e.string().optional(),port:e.coerce.number().int().positive().optional(),auth_tokens:e.record(e.string(),e.string()).optional(),workspaces:e.array(e.object({id:e.string().optional(),name:e.string().optional(),path:e.string().optional()})).optional(),file_uploads:e.object({temp_dir:e.string().optional()}).optional(),data_dir:e.string().optional(),codex:e.object({port:e.coerce.number().int().positive().optional(),data_dir:e.string().optional(),file_uploads:e.object({temp_dir:e.string().optional()}).optional(),api_key:e.string().optional(),base_url:e.string().optional()}).optional(),openai:e.object({api_key:e.string().optional(),base_url:e.string().optional(),image_model:e.string().optional()}).optional(),schedule:e.object({enabled:e.boolean().optional(),base_url:e.string().optional(),api_key:e.string().optional()}).optional(),gitlab:e.object({ca_cert_path:e.string().optional(),host:e.string().optional()}).optional(),git:e.object({co_authors:e.array(e.string()).optional()}).optional(),tunnel:e.object({enabled:e.boolean().optional(),server_url:e.string().optional(),tunnel_id:e.string().optional(),token:e.string().optional(),reconnect_initial_ms:e.number().int().positive().optional(),reconnect_max_ms:e.number().int().positive().optional(),heartbeat_interval_ms:e.number().int().positive().optional()}).optional()}).loose();function S(){const t=process.env.AGENT_CONFIG_PATH??"./agent-config.yaml";if(!M(t)){const l="./data",_=a(l,"logs","codex-agent.log");return k("info"),y(_),s.warn(`Config file not found at ${t}`),{configPath:t,logLevel:"info",logFile:_,port:3e3,fileUploads:{tempDir:a(b(),"codex-agent-uploads")},dataDir:l,sessionsDbPath:a(l,"sessions.db"),codex:{},openai:{imageModel:"gpt-image-2"},schedule:{enabled:!1},tunnel:{enabled:!1,reconnectInitialMs:1e3,reconnectMaxMs:3e4,heartbeatIntervalMs:3e4},...v({}),...h({}),...x({})}}let p;try{p=I(U(t,"utf-8"))}catch(l){s.error(`Failed to parse config file at ${t}:`,l),p={}}const r=F.safeParse(p??{});r.success||s.error(`Invalid config format at ${t}:`,r.error.issues);const o=r.success?r.data:{},g=L(o.log_level),c=n(o.codex?.data_dir)??n(o.data_dir)??"./data",u=a(c,"logs","codex-agent.log"),i=o.tunnel?.enabled??!1,d=n(o.tunnel?.token),m=n(o.openai?.api_key)??(i?d:void 0),f=n(o.openai?.base_url)??(i?j:void 0);k(g),y(u);const C={configPath:t,logLevel:g,logFile:u,port:o.codex?.port??o.port??3e3,fileUploads:{tempDir:n(o.codex?.file_uploads?.temp_dir)??n(o.file_uploads?.temp_dir)??a(b(),"codex-agent-uploads")},dataDir:c,sessionsDbPath:a(c,"sessions.db"),codex:{apiKey:n(o.codex?.api_key)??(i?d:void 0),baseUrl:n(o.codex?.base_url)??(i?j:void 0)},openai:{apiKey:m,baseUrl:f,imageModel:n(o.openai?.image_model)??"gpt-image-2"},schedule:{enabled:o.schedule?.enabled??!1,...A({baseUrl:n(o.schedule?.base_url),apiKey:n(o.schedule?.api_key)},f,m)},tunnel:{enabled:i,serverUrl:n(o.tunnel?.server_url)??(i?D:void 0),tunnelId:n(o.tunnel?.tunnel_id),token:d,reconnectInitialMs:o.tunnel?.reconnect_initial_ms??1e3,reconnectMaxMs:o.tunnel?.reconnect_max_ms??3e4,heartbeatIntervalMs:o.tunnel?.heartbeat_interval_ms??3e4},...v(o),...h(o),...x(o)};return s.info(`Loaded agent config from ${t}`),C}const E=S();export{E as appConfig};
@@ -9,6 +9,6 @@ type ManagedTaskToolContext = {
9
9
  export declare function setManagedTaskToolContext(threadId: string, context: ManagedTaskToolContext): void;
10
10
  export declare function inheritManagedTaskToolContext(parentThreadId: string, childThreadIds: string[]): void;
11
11
  export declare function inheritManagedTaskToolContextFromNotification(method: string, params: unknown): void;
12
- export declare function dynamicToolSpecs(): DynamicToolSpec[];
12
+ export declare function dynamicToolSpecs(scheduleEnabled?: boolean): DynamicToolSpec[];
13
13
  export declare function handleDynamicToolCall(params: DynamicToolCallParams): Promise<DynamicToolCallResponse>;
14
14
  export {};
@@ -1 +1 @@
1
- import c from"node:path";import{mkdir as h,writeFile as u}from"node:fs/promises";import{generateImage as f}from"ai";import{createOpenAI as y}from"@ai-sdk/openai";import{Agent as T,fetch as w}from"undici";import{z as r}from"zod";import{executeManagedTaskTool as x,isManagedTaskToolName as I,MANAGED_TASK_TOOL_DEFINITIONS as A}from"@messenger-agent/shared/managed-task-tools";import{AgentFileError as b,resolveAgentFilePath as k}from"@messenger-agent/shared/agent-files";const p=1200*1e3,S=new T({headersTimeout:p,bodyTimeout:p}),v=(t,e)=>w(t,{...e,dispatcher:S}),l=r.object({prompt:r.string().describe("The text prompt describing the image to generate"),store_path:r.string().describe("Absolute file path (including filename) to save the image. If omitted, saves to the current working directory with an auto-generated name.").optional(),size:r.enum(["1024x1024","1536x1024","1024x1536"]).describe("Image size. Leave it undefined to use the auto size.").optional()}),m=r.object({path:r.string().describe("File to send. Absolute paths use the system path; relative paths resolve from the active workspace.")}),M=r.toJSONSchema(l),E=r.toJSONSchema(m);function _(t){return{contentItems:[{type:"inputText",text:JSON.stringify(t)}],success:!0}}const s=new Map;function R(t,e){const o=e.threadRef?.trim();s.set(t,{workspaceRoot:e.workspaceRoot,...o?{threadRef:o}:null,...e.matrixEvent?{matrixEvent:e.matrixEvent}:null,...e.llmProxyBinding?{llmProxyBinding:e.llmProxyBinding}:null})}function d(t,e){const o=s.get(t);if(o)for(const n of e)n&&n!==t&&R(n,o)}function K(t,e){if(t!=="item/started"&&t!=="item/completed"||!e||typeof e!="object")return;const o=e,n=o.item;if(!n||typeof n!="object")return;const i=n;if(i.type==="subAgentActivity"&&i.kind==="started"&&typeof o.threadId=="string"&&typeof i.agentThreadId=="string"){d(o.threadId,[i.agentThreadId]);return}i.type==="collabAgentToolCall"&&i.tool==="spawnAgent"&&typeof i.senderThreadId=="string"&&Array.isArray(i.receiverThreadIds)&&d(i.senderThreadId,i.receiverThreadIds.filter(a=>typeof a=="string"))}async function g(t){const e=s.get(t);if(e)return e.workspaceRoot;const{sessionManager:o}=await import("./codex.js"),n=o.workdirFor(t);if(!n)throw new Error(`No workspace context for thread ${t}; the session may not exist in this process`);return n}async function F(t){const{appConfig:e}=await import("./config.js");if(!e.openai.apiKey)throw new Error("Missing openai.api_key in LLM config");return{apiKey:e.openai.apiKey,baseURL:e.openai.baseUrl,imageModel:e.openai.imageModel,llmProxyBinding:s.get(t)?.llmProxyBinding}}function U(){return[{name:"generate_image",description:"Generate an image from a text prompt. The image is saved to the specified path (absolute path including filename) or the current working directory if not specified. Returns the saved file path.",inputSchema:M},{name:"send_file",description:"Send any readable file to the client. Absolute paths use the system path; relative paths resolve from the active workspace. The agent runtime captures this tool call and performs the actual send.",inputSchema:E},...A.map(t=>({name:t.name,description:t.description,inputSchema:t.inputSchema}))]}async function j(t){try{if(t.tool==="generate_image"){const e=l.parse(t.arguments),o=await F(t.threadId),n=y({apiKey:o.apiKey,baseURL:o.baseURL,fetch:v,...o.llmProxyBinding?{headers:{"X-Elevo-LLM-Binding":o.llmProxyBinding}}:null}),{image:i}=await f({model:n.image(o.imageModel),prompt:e.prompt,size:e.size,maxRetries:0,abortSignal:AbortSignal.timeout(p)}),a=e.store_path?c.resolve(e.store_path):c.join(process.cwd(),`generated_${Date.now()}.png`);return await h(c.dirname(a),{recursive:!0}),await u(a,Buffer.from(i.uint8Array)),{contentItems:[{type:"inputText",text:a}],success:!0}}if(t.tool==="send_file"){const e=m.parse(t.arguments),o=await k(e.path,await g(t.threadId));return{contentItems:[{type:"inputText",text:JSON.stringify({path:o})}],success:!0}}if(I(t.tool)){const e=await x(await g(t.threadId),t.tool,t.arguments,s.get(t.threadId));return _(e)}throw new Error(`Unknown dynamic tool: ${t.tool}`)}catch(e){return{contentItems:[{type:"inputText",text:e instanceof b&&e.code==="INVALID_PATH"||e instanceof Error?e.message:String(e)}],success:!1}}}export{U as dynamicToolSpecs,j as handleDynamicToolCall,d as inheritManagedTaskToolContext,K as inheritManagedTaskToolContextFromNotification,R as setManagedTaskToolContext};
1
+ import l from"node:path";import{mkdir as f,writeFile as y}from"node:fs/promises";import{generateImage as T}from"ai";import{createOpenAI as w}from"@ai-sdk/openai";import{Agent as I,fetch as x}from"undici";import{z as r}from"zod";import{executeManagedTaskTool as S,isManagedTaskToolName as b,MANAGED_TASK_TOOL_DEFINITIONS as A}from"@messenger-agent/shared/managed-task-tools";import{AgentFileError as k,resolveAgentFilePath as v}from"@messenger-agent/shared/agent-files";import{executeScheduleTool as E,SCHEDULE_TOOL_DEFINITION as c,ScheduleToolError as _}from"@messenger-agent/shared/schedule-tools";const p=1200*1e3,M=new I({headersTimeout:p,bodyTimeout:p}),R=(t,e)=>x(t,{...e,dispatcher:M}),d=r.object({prompt:r.string().describe("The text prompt describing the image to generate"),store_path:r.string().describe("Absolute file path (including filename) to save the image. If omitted, saves to the current working directory with an auto-generated name.").optional(),size:r.enum(["1024x1024","1536x1024","1024x1536"]).describe("Image size. Leave it undefined to use the auto size.").optional()}),m=r.object({path:r.string().describe("File to send. Absolute paths use the system path; relative paths resolve from the active workspace.")}),N=r.toJSONSchema(d),O=r.toJSONSchema(m);function g(t){return{contentItems:[{type:"inputText",text:JSON.stringify(t)}],success:!0}}const a=new Map;function F(t,e){const o=e.threadRef?.trim();a.set(t,{workspaceRoot:e.workspaceRoot,...o?{threadRef:o}:null,...e.matrixEvent?{matrixEvent:e.matrixEvent}:null,...e.llmProxyBinding?{llmProxyBinding:e.llmProxyBinding}:null})}function u(t,e){const o=a.get(t);if(o)for(const n of e)n&&n!==t&&F(n,o)}function $(t,e){if(t!=="item/started"&&t!=="item/completed"||!e||typeof e!="object")return;const o=e,n=o.item;if(!n||typeof n!="object")return;const i=n;if(i.type==="subAgentActivity"&&i.kind==="started"&&typeof o.threadId=="string"&&typeof i.agentThreadId=="string"){u(o.threadId,[i.agentThreadId]);return}i.type==="collabAgentToolCall"&&i.tool==="spawnAgent"&&typeof i.senderThreadId=="string"&&Array.isArray(i.receiverThreadIds)&&u(i.senderThreadId,i.receiverThreadIds.filter(s=>typeof s=="string"))}async function h(t){const e=a.get(t);if(e)return e.workspaceRoot;const{sessionManager:o}=await import("./codex.js"),n=o.workdirFor(t);if(!n)throw new Error(`No workspace context for thread ${t}; the session may not exist in this process`);return n}async function L(t){const{appConfig:e}=await import("./config.js");if(!e.openai.apiKey)throw new Error("Missing openai.api_key in LLM config");return{apiKey:e.openai.apiKey,baseURL:e.openai.baseUrl,imageModel:e.openai.imageModel,llmProxyBinding:a.get(t)?.llmProxyBinding}}async function C(){const{appConfig:t}=await import("./config.js");return t.schedule}function H(t=!1){const e=[{name:"generate_image",description:"Generate an image from a text prompt. The image is saved to the specified path (absolute path including filename) or the current working directory if not specified. Returns the saved file path.",inputSchema:N},{name:"send_file",description:"Send any readable file to the client. Absolute paths use the system path; relative paths resolve from the active workspace. The agent runtime captures this tool call and performs the actual send.",inputSchema:O},...A.map(o=>({name:o.name,description:o.description,inputSchema:o.inputSchema}))];return t&&e.push({name:c.name,description:c.description,inputSchema:c.inputSchema}),e}async function V(t){try{if(t.tool==="generate_image"){const e=d.parse(t.arguments),o=await L(t.threadId),n=w({apiKey:o.apiKey,baseURL:o.baseURL,fetch:R,...o.llmProxyBinding?{headers:{"X-Elevo-LLM-Binding":o.llmProxyBinding}}:null}),{image:i}=await T({model:n.image(o.imageModel),prompt:e.prompt,size:e.size,maxRetries:0,abortSignal:AbortSignal.timeout(p)}),s=e.store_path?l.resolve(e.store_path):l.join(process.cwd(),`generated_${Date.now()}.png`);return await f(l.dirname(s),{recursive:!0}),await y(s,Buffer.from(i.uint8Array)),{contentItems:[{type:"inputText",text:s}],success:!0}}if(t.tool==="send_file"){const e=m.parse(t.arguments),o=await v(e.path,await h(t.threadId));return{contentItems:[{type:"inputText",text:JSON.stringify({path:o})}],success:!0}}if(b(t.tool)){const e=await S(await h(t.threadId),t.tool,t.arguments,a.get(t.threadId));return g(e)}if(t.tool===c.name)return g(await E(await C(),a.get(t.threadId),t.arguments));throw new Error(`Unknown dynamic tool: ${t.tool}`)}catch(e){const o=e instanceof k&&e.code==="INVALID_PATH"||e instanceof Error?e.message:String(e),n=e instanceof _?{error:{code:e.code,message:e.message,retryable:e.retryable,currentRevision:e.currentRevision}}:o;return{contentItems:[{type:"inputText",text:typeof n=="string"?n:JSON.stringify(n)}],success:!1}}}export{H as dynamicToolSpecs,V as handleDynamicToolCall,u as inheritManagedTaskToolContext,$ as inheritManagedTaskToolContextFromNotification,F as setManagedTaskToolContext};
@@ -1,2 +1 @@
1
- export declare function loadCodexPlatformInstructions(templatePath?: string): string;
2
- export declare const codexPlatformInstructions: string;
1
+ export declare function loadCodexPlatformInstructions(templatePath?: string, scheduleEnabled?: boolean): string;
@@ -1,3 +1,4 @@
1
- import{existsSync as s,readFileSync as r}from"node:fs";import{fileURLToPath as a}from"node:url";import{logger as m}from"@messenger-agent/shared/logger";const d=[a(new URL("./assets/codex-home/AGENTS.md",import.meta.url)),a(new URL("../../../assets/codex-home/AGENTS.md",import.meta.url)),"/app/assets/codex-home/AGENTS.md"],o="## Codex Managed Task Tools\n\nWhen using managed task tools in Codex, call these tool names directly: `search_managed_tasks`, `get_managed_task`, `create_managed_task`, `update_managed_task`, `apply_managed_task_patch`, and `delete_managed_task`.\n";function _(n){const e=[n??process.env.CODEX_AGENTS_TEMPLATE_PATH,...d].find(t=>!!(t&&s(t)));return e?`${r(e,"utf-8").trimEnd()}
1
+ import{existsSync as c,readFileSync as d}from"node:fs";import{fileURLToPath as o}from"node:url";import{logger as i}from"@messenger-agent/shared/logger";const m=[o(new URL("./assets/codex-home/AGENTS.md",import.meta.url)),o(new URL("../../../assets/codex-home/AGENTS.md",import.meta.url)),"/app/assets/codex-home/AGENTS.md"],s="## Codex Managed Task Tools\n\nWhen using managed task tools in Codex, call these tool names directly: `search_managed_tasks`, `get_managed_task`, `create_managed_task`, `update_managed_task`, `apply_managed_task_patch`, and `delete_managed_task`.\n",l="## Schedule Tool\n\nUse the single `schedule` tool to create, list, inspect, update, pause, resume, or cancel schedules in the current trusted Matrix context. The action is selected inside `input.action`; never ask for or invent a Room ID, actor ID, source event, or service credential. After create or update succeeds, repeat the service's normalized rule, absolute next run time, IANA timezone, message, and mention MXIDs to the user so relative times and recurrence are unambiguous.\n";function h(n,r=!1){const e=r?`
2
+ ${l}`:"",t=[n??process.env.CODEX_AGENTS_TEMPLATE_PATH,...m].find(a=>!!(a&&c(a)));return t?`${d(t,"utf-8").trimEnd()}
2
3
 
3
- ${o}`:(m.warn("Codex platform instructions template not found"),o)}const p=_();export{p as codexPlatformInstructions,_ as loadCodexPlatformInstructions};
4
+ ${s}${e}`:(i.warn("Codex platform instructions template not found"),`${s}${e}`)}export{h as loadCodexPlatformInstructions};
@@ -1,2 +1,2 @@
1
- import{Hono as Ht}from"hono";import{HTTPException as D}from"hono/http-exception";import{zValidator as Jt}from"@hono/zod-validator";import{streamSSE as Kt}from"hono/streaming";import{randomUUID as Qt}from"node:crypto";import{sessionManager as j,WorkdirNotFoundError as Vt}from"../codex.js";import{logger as rt}from"@messenger-agent/shared/logger";import{ChatBodySchema as Gt,RequestUserInputToolPartSchema as Yt}from"../schemas.js";import{appConfig as xt}from"../config.js";import{setManagedTaskToolContext as Zt}from"../dynamic-tools.js";import{resolveAgentWorkspaceRoot as te}from"@messenger-agent/shared/agent-workspaces";import{saveUploadedFilePart as ee}from"@messenger-agent/shared/uploaded-files";import{commandExecutionItem as bt,addTokenUsage as ne,displayCommand as ae,dynamicToolCallItem as Tt,dynamicToolCallOutput as re,fileChangeItem as St,itemText as Et,mcpToolCallItem as Rt,mcpToolCallOutput as oe,toUsage as ie,turnOptions as se,webSearchItem as kt,webSearchPayload as Ot}from"./shared.js";const B=new Ht,f=new Map,z=new Set,ot=new Map,k=new Map;function Le(){const t=new Set([...k.values()].map(e=>e.conversationId).filter(e=>!!e));return{active:z.size,waiting:t.size}}function de(t){return{type:"not_found_error",code:"conversation_not_found",message:`Conversation not found or already completed: ${t}`,param:"conversation_id"}}function ue(t){const e=v(t,"X-Matrix-Room-Id")?.trim(),r=v(t,"X-Matrix-Thread-ID")?.trim();return e&&r?`${e}/${r}`:void 0}function le(t){const e=v(t,"X-Matrix-Room-Id")?.trim(),r=v(t,"X-Matrix-Event-ID")?.trim(),o=v(t,"X-Matrix-Thread-ID")?.trim();if(!(!e||!r))return{roomId:e,eventId:r,...o?{threadRootEventId:o}:null}}class q extends Error{status;constructor(e,r){super(r),this.status=e,this.name="ChatRequestError"}}function v(t,e){return t?.get(e)??void 0}async function _t(t){const e=f.get(t);if(!e)return!1;for(e.cancelRequested=!0;;){const r=e.transition;if(await r,r===e.transition)break}return e.thread&&e.turnId&&(e.interrupt||(e.interrupt=e.thread.interrupt(e.turnId).finally(()=>{e.interrupt=void 0})),await e.interrupt),e.owner?.abortController.abort(),e.resolveOwnerReady?.(),f.get(t)===e&&f.delete(t),!0}function Mt(t){let e=f.get(t);e||(e={conversationId:t,generation:0,cancelRequested:!1,transition:Promise.resolve()},f.set(t,e));const r=++e.generation,o=new AbortController,i=!e.owner;return i&&(e.owner={generation:r,abortController:o},e.ownerReady=new Promise(d=>{e.resolveOwnerReady=d})),{state:e,generation:r,abortController:o,ownsOutput:i}}function N(t){return f.get(t.state.conversationId)===t.state&&t.state.owner?.generation===t.generation}async function H(t,e){const r=t.transition;let o;const i=new Promise(d=>{o=d});t.transition=r.then(()=>i),await r;try{return await e()}finally{o()}}async function ce(t,e,r,o){!t.ownsOutput&&!t.state.turnId&&await t.state.ownerReady;try{return await H(t.state,async()=>{const{state:i}=t;if(f.get(i.conversationId)!==i||i.cancelRequested)throw new DOMException("Chat stream was cancelled before the turn started","AbortError");i.thread=e;const d=i.turnId?await e.steerStreamed(r,i.turnId,{signal:t.abortController.signal}):await e.runStreamed(r,{...o,signal:t.abortController.signal});return i.turnId=d.turnId,d})}finally{t.ownsOutput&&t.state.resolveOwnerReady?.()}}async function pe(t,e,r){return H(t.state,async()=>{const{state:o}=t;if(!N(t)||o.cancelRequested)throw new DOMException("Chat stream was cancelled before user input was submitted","AbortError");o.thread=e;const i=await r();return o.turnId=i.turnId,o.resolveOwnerReady?.(),i})}async function fe(t,e,r,o){return H(t.state,async()=>{const{state:i}=t;if(f.get(i.conversationId)!==i||i.cancelRequested)throw new DOMException("Chat stream was cancelled before user input was submitted","AbortError");i.thread=e;const d=await e.submitPendingUserInput(r,o);return d&&(i.turnId=d),d})}async function me(t,e,r,o,i){return H(t.state,async()=>{const{state:d}=t;if(f.get(d.conversationId)!==d||d.cancelRequested)throw new DOMException("Chat stream was cancelled before the turn started","AbortError");d.thread=e,await e.interruptPendingUserInput(r.turnId,r.toolCallId),d.turnId=void 0;const T=await e.runStreamed(o,{...i,signal:t.abortController.signal});return d.turnId=T.turnId,T})}function Pt(t,e,r){t.state.thread=e,t.state.turnId=r}function Dt(t){const{state:e}=t;N(t)&&(e.owner=void 0,e.resolveOwnerReady?.(),e.turnId=void 0,f.get(e.conversationId)===e&&f.delete(e.conversationId))}function qt(t){N(t)&&(t.state.owner=void 0,t.state.resolveOwnerReady?.(),!t.state.turnId&&f.get(t.state.conversationId)===t.state&&f.delete(t.state.conversationId))}function we(t){N(t)&&(qt(t),t.state.resolveOwnerReady?.(),!t.state.turnId&&f.get(t.state.conversationId)===t.state&&f.delete(t.state.conversationId))}function J(t,e){return`${t??"unknown"}:${e}`}function ye(t){if(t)return[...k.values()].find(e=>e.conversationId===t)}function Nt(t,e){return t??e}function ge(t){return t.type==="file"&&"mediaType"in t&&typeof t.mediaType=="string"&&"url"in t&&typeof t.url=="string"&&(!("filename"in t)||t.filename===void 0||typeof t.filename=="string")}async function Ie(t){if(t.type==="text"&&typeof t.text=="string")return t.text;if(ge(t))return ee(t,xt.fileUploads.tempDir);throw new D(400,{message:`Unsupported message part type: ${t.type}`})}async function he(t){const r=(await Promise.all(t.map(Ie))).filter(o=>!!(o&&o.trim().length>0)).join(`
1
+ import{Hono as Ht}from"hono";import{HTTPException as D}from"hono/http-exception";import{zValidator as Jt}from"@hono/zod-validator";import{streamSSE as Kt}from"hono/streaming";import{randomUUID as Qt}from"node:crypto";import{sessionManager as j,WorkdirNotFoundError as Vt}from"../codex.js";import{logger as rt}from"@messenger-agent/shared/logger";import{ChatBodySchema as Gt,RequestUserInputToolPartSchema as Yt}from"../schemas.js";import{appConfig as xt}from"../config.js";import{setManagedTaskToolContext as Zt}from"../dynamic-tools.js";import{resolveAgentWorkspaceRoot as te}from"@messenger-agent/shared/agent-workspaces";import{saveUploadedFilePart as ee}from"@messenger-agent/shared/uploaded-files";import{commandExecutionItem as bt,addTokenUsage as ne,displayCommand as ae,dynamicToolCallItem as Tt,dynamicToolCallOutput as re,fileChangeItem as St,itemText as Et,mcpToolCallItem as Rt,mcpToolCallOutput as oe,toUsage as ie,turnOptions as se,webSearchItem as kt,webSearchPayload as Ot}from"./shared.js";const B=new Ht,f=new Map,z=new Set,ot=new Map,k=new Map;function Le(){const t=new Set([...k.values()].map(e=>e.conversationId).filter(e=>!!e));return{active:z.size,waiting:t.size}}function de(t){return{type:"not_found_error",code:"conversation_not_found",message:`Conversation not found or already completed: ${t}`,param:"conversation_id"}}function ue(t){const e=v(t,"X-Matrix-Room-Id")?.trim(),r=v(t,"X-Matrix-Thread-ID")?.trim();return e&&r?`${e}/${r}`:void 0}function le(t){const e=v(t,"X-Matrix-Room-Id")?.trim(),r=v(t,"X-Matrix-Event-ID")?.trim(),o=v(t,"X-Matrix-Thread-ID")?.trim();if(!(!e||!r))return{roomId:e,eventId:r,...o?{threadRootEventId:o}:null}}class q extends Error{status;constructor(e,r){super(r),this.status=e,this.name="ChatRequestError"}}function v(t,e){return t?.get(e)??void 0}async function _t(t){const e=f.get(t);if(!e)return!1;for(e.cancelRequested=!0,e.turnId||e.owner?.abortController.abort();;){const r=e.transition;if(await r,r===e.transition)break}return e.thread&&e.turnId&&(e.interrupt||(e.interrupt=e.thread.interrupt(e.turnId).finally(()=>{e.interrupt=void 0})),await e.interrupt),e.owner?.abortController.abort(),e.resolveOwnerReady?.(),f.get(t)===e&&f.delete(t),!0}function Mt(t){let e=f.get(t);e||(e={conversationId:t,generation:0,cancelRequested:!1,transition:Promise.resolve()},f.set(t,e));const r=++e.generation,o=new AbortController,i=!e.owner;return i&&(e.owner={generation:r,abortController:o},e.ownerReady=new Promise(d=>{e.resolveOwnerReady=d})),{state:e,generation:r,abortController:o,ownsOutput:i}}function N(t){return f.get(t.state.conversationId)===t.state&&t.state.owner?.generation===t.generation}async function H(t,e){const r=t.transition;let o;const i=new Promise(d=>{o=d});t.transition=r.then(()=>i),await r;try{return await e()}finally{o()}}async function ce(t,e,r,o){!t.ownsOutput&&!t.state.turnId&&await t.state.ownerReady;try{return await H(t.state,async()=>{const{state:i}=t;if(f.get(i.conversationId)!==i||i.cancelRequested)throw new DOMException("Chat stream was cancelled before the turn started","AbortError");i.thread=e;const d=i.turnId?await e.steerStreamed(r,i.turnId,{signal:t.abortController.signal}):await e.runStreamed(r,{...o,signal:t.abortController.signal});return i.turnId=d.turnId,d})}finally{t.ownsOutput&&t.state.resolveOwnerReady?.()}}async function pe(t,e,r){return H(t.state,async()=>{const{state:o}=t;if(!N(t)||o.cancelRequested)throw new DOMException("Chat stream was cancelled before user input was submitted","AbortError");o.thread=e;const i=await r();return o.turnId=i.turnId,o.resolveOwnerReady?.(),i})}async function fe(t,e,r,o){return H(t.state,async()=>{const{state:i}=t;if(f.get(i.conversationId)!==i||i.cancelRequested)throw new DOMException("Chat stream was cancelled before user input was submitted","AbortError");i.thread=e;const d=await e.submitPendingUserInput(r,o);return d&&(i.turnId=d),d})}async function me(t,e,r,o,i){return H(t.state,async()=>{const{state:d}=t;if(f.get(d.conversationId)!==d||d.cancelRequested)throw new DOMException("Chat stream was cancelled before the turn started","AbortError");d.thread=e,await e.interruptPendingUserInput(r.turnId,r.toolCallId),d.turnId=void 0;const T=await e.runStreamed(o,{...i,signal:t.abortController.signal});return d.turnId=T.turnId,T})}function Pt(t,e,r){t.state.thread=e,t.state.turnId=r}function Dt(t){const{state:e}=t;N(t)&&(e.owner=void 0,e.resolveOwnerReady?.(),e.turnId=void 0,f.get(e.conversationId)===e&&f.delete(e.conversationId))}function qt(t){N(t)&&(t.state.owner=void 0,t.state.resolveOwnerReady?.(),!t.state.turnId&&f.get(t.state.conversationId)===t.state&&f.delete(t.state.conversationId))}function we(t){N(t)&&(qt(t),t.state.resolveOwnerReady?.(),!t.state.turnId&&f.get(t.state.conversationId)===t.state&&f.delete(t.state.conversationId))}function J(t,e){return`${t??"unknown"}:${e}`}function ye(t){if(t)return[...k.values()].find(e=>e.conversationId===t)}function Nt(t,e){return t??e}function ge(t){return t.type==="file"&&"mediaType"in t&&typeof t.mediaType=="string"&&"url"in t&&typeof t.url=="string"&&(!("filename"in t)||t.filename===void 0||typeof t.filename=="string")}async function Ie(t){if(t.type==="text"&&typeof t.text=="string")return t.text;if(ge(t))return ee(t,xt.fileUploads.tempDir);throw new D(400,{message:`Unsupported message part type: ${t.type}`})}async function he(t){const r=(await Promise.all(t.map(Ie))).filter(o=>!!(o&&o.trim().length>0)).join(`
2
2
  `).trim();if(!r)throw new D(400,{message:"No supported message content found"});return r}function Ce(t){if(!t.some(o=>o.type==="tool"))return;if(t.length!==1)throw new D(400,{message:"When a tool part is present, the message must contain exactly one part"});const r=Yt.safeParse(t[0]);if(!r.success)throw new D(400,{message:`Invalid requestUserInput tool part: ${r.error.message}`});return{itemId:r.data.toolCallId,response:r.data.output,sourceTurnId:r.data.sourceTurnId}}function m(t,e,r,o){const i=[e,r,o].filter(Boolean).join(":"),d=t.get(i);if(d)return d;const T=Qt();return t.set(i,T),T}function ve(t,e){return t&&t.length>0?t:e}function xe(t){rt.debug("Received chat stream event:",t)}function be(t){if(t instanceof Vt||t&&typeof t=="object"&&"path"in t&&typeof t.path=="string")return t.path;if(t instanceof Error&&t.message.startsWith("Workdir not found: "))return t.message.slice(19)}B.post("/v1/chat/:conversation_id/cancel",async t=>{const e=t.req.param("conversation_id");try{return await _t(e)?t.json({success:!0}):t.json({error:de(e)},404)}catch(r){const o=r instanceof Error?r.message:"Failed to interrupt Codex conversation";return rt.error(`Failed to cancel Codex conversation ${e}:`,r),t.json({error:{type:"server_error",code:"cancel_failed",message:o}},500)}}),B.get("/admin/status",t=>t.json({activeTaskCount:z.size,waitingClientToolCount:k.size,clientToolWaits:[...k.values()].map(({conversationId:e,toolCallId:r,toolName:o,since:i})=>({conversationId:e,toolCallId:r,toolName:o,since:i}))}));async function Ut({body:t,headers:e}){const{conversationId:r,fork:o,model:i,effort:d,message:T}=t,K=v(e,"X-Matrix-Bot-Name"),Q=v(e,"X-Matrix-Workdir"),U=v(e,"X-Matrix-Workspace-ID"),Lt=v(e,"X-Matrix-Agent-Mode"),V=v(e,"X-Elevo-LLM-Binding")?.trim()||void 0,G=o?void 0:j.bindingFor(r);if(V&&G&&V!==G)throw new q(409,"LLM proxy binding does not match the existing conversation");const L=G??V,At=ue(e),Wt=le(e),Y=Ce(T.parts),Z=Y?void 0:await he(T.parts),it=Y?void 0:ye(r),c=Y,O=Z&&it?it:void 0;let A=!o&&r?Mt(r):void 0;const Ft=(()=>{try{return te(xt,U)}catch(S){throw new q(400,S instanceof Error?S.message:"Invalid workspace")}})(),{thread:y,isNewSession:Xt}=await(async()=>{try{if(o){if(!("threadId"in o))throw new q(400,"Codex fork requires threadId and turnId");return await j.fork(o,K,Q,U,i,"danger-full-access",L)}return await j.getOrCreate(r,K,Q,U,i,"danger-full-access",L)}catch(S){A&&we(A);const _=be(S);throw _?new q(400,`Workdir not found: ${_}`):S}})();A??=Mt(y.id),Zt(y.id,{workspaceRoot:Ft,threadRef:At,matrixEvent:Wt,llmProxyBinding:L});const st=se(Lt,i,d);return{run:async(S,_)=>{const w=A,W=w.abortController;z.add(W);const tt=()=>{N(w)&&_t(w.state.conversationId).catch(a=>rt.error("Failed to cancel disconnected Codex stream:",a))};_?.aborted&&tt(),_?.addEventListener("abort",tt,{once:!0});let x=!!c,dt=!!c,ut=!1,l,I=y.id,h,F=!1,P;const lt=new Set,ct=new Set,X=new Set,p=new Map,pt=new Set,$t=new Set,ft=new Set;let mt,et,wt;const yt=()=>{P=setTimeout(()=>{s({type:"heartbeat"}).catch(()=>{})},6e4)},gt=async a=>{P&&clearTimeout(P),a.data==="[DONE]"?await S.writeDone():await S.writeData(JSON.parse(String(a.data))),yt()},s=async a=>{await gt({data:JSON.stringify(a)})},$=async()=>{await gt({data:"[DONE]"})},jt=async()=>{const a=Nt(l,y.id),n=ot.get(a);n&&(ot.delete(a),await s({type:"data",kind:"diff",data:{diff:n}}))},It=async(a,n)=>{if(a.type!=="contextCompaction")return!1;if(n==="end"&&!pt.has(a.id))return!0;const u=n==="start"?pt:$t;return u.has(a.id)||(u.add(a.id),await s({type:"data",kind:"compact",data:n==="start"?{phase:n,activityId:a.id}:{phase:n,activityId:a.id,status:"completed"}})),!0},nt=async a=>{if(a.type!=="subAgentActivity")return!1;if(ft.has(a.id))return!0;ft.add(a.id);const n=m(p,l,a.id),u={activity:a.kind,agentThreadId:a.agentThreadId,...a.agentPath?{agentPath:a.agentPath}:null};return await s({type:"tool-input-available",toolCallId:n,toolName:`subagent_${a.kind}`,...a.agentPath?{title:a.agentPath}:null,input:u}),await s(a.kind==="interrupted"?{type:"tool-output-error",toolCallId:n,errorText:"Subagent interrupted"}:{type:"tool-output-available",toolCallId:n,output:u}),!0};yt();const ht=a=>{l!==a&&(l=a,Xt&&j.persist(a,K,Q,U,L))},E=async(a,n)=>{x||(x=!0,ht(a),await s({type:"start",messageMetadata:{conversationId:a,threadId:a,...n?{turnId:n}:null}}),await s({type:"start-step"}),dt=!0)},at=async a=>{ut||(ut=!0,dt&&(await jt(),a==="stop"&&et&&await s({type:"data",kind:"token_usage",data:et}),await s({type:"finish-step"})),await s({type:"finish",finishReason:a}),await $())},Ct=async a=>{if(lt.has(a.id))return;lt.add(a.id);const n=a.type==="plan"?{type:"plan"}:void 0;await s({type:"text-start",id:a.id,...n?{textMetadata:n}:{}})},vt=async a=>{ct.has(a.id)||(ct.add(a.id),await s({type:"reasoning-start",id:a.id}))},Bt=async a=>{const n=bt(a);if(n)return await s({type:"tool-input-available",toolCallId:m(p,l,a.id),toolName:"bash",input:ae(n.command)}),!0;const u=St(a);if(u){for(const[b,C]of u.changes.entries())await s({type:"tool-input-available",toolCallId:m(p,l,a.id,String(b)),toolName:C.kind.type==="update"?"Edit":C.kind.type==="add"?"Add":"Delete",title:C.path,input:C});return!0}const g=Rt(a);if(g)return await s({type:"tool-input-available",toolCallId:m(p,l,a.id),toolName:g.tool,input:g.arguments}),!0;const R=Tt(a);if(R)return await s({type:"tool-input-available",toolCallId:m(p,l,a.id),toolName:R.tool,input:R.arguments}),!0;const M=kt(a);return M?(await s({type:"tool-input-available",toolCallId:m(p,l,a.id),toolName:"web_search",title:M.query,input:Ot(M)}),!0):!1},zt=async a=>{const n=bt(a);if(n){const b=n.status==="failed"||n.status==="declined";return await s(b?{type:"tool-output-error",toolCallId:m(p,l,a.id),errorText:ve(n.aggregatedOutput,"Command execution failed")}:{type:"tool-output-available",toolCallId:m(p,l,a.id),output:n.aggregatedOutput??""}),!0}const u=St(a);if(u){const b=u.status==="failed"||u.status==="declined";for(const[C]of u.changes.entries())await s(b?{type:"tool-output-error",toolCallId:m(p,l,a.id,String(C)),errorText:u.status}:{type:"tool-output-available",toolCallId:m(p,l,a.id,String(C)),output:u.status,preliminary:!1});return!0}const g=Rt(a);if(g){const b=g.status==="failed";return await s(b?{type:"tool-output-error",toolCallId:m(p,l,a.id),errorText:g.error?.message??"MCP tool call failed"}:{type:"tool-output-available",toolCallId:m(p,l,a.id),output:oe(g)}),!0}const R=Tt(a);if(R){const b=R.status==="failed"||R.success===!1,C=re(R);return await s(b?{type:"tool-output-error",toolCallId:m(p,l,a.id),errorText:C||"Dynamic tool call failed"}:{type:"tool-output-available",toolCallId:m(p,l,a.id),output:C}),!0}const M=kt(a);return M?(await s({type:"tool-output-available",toolCallId:m(p,l,a.id),output:Ot(M)}),!0):!1};try{if(c&&!w.ownsOutput){await fe(w,y,c.itemId,c.response),k.delete(J(r,c.itemId)),await $();return}const a=O?await me(w,y,O,Z,st):c?await pe(w,y,()=>y.continueWithUserInput(c.itemId,c.response,{signal:W.signal,sourceTurnId:c.sourceTurnId})):await ce(w,y,Z,st);if(c?k.delete(J(r,c.itemId)):O&&k.delete(J(r,O.toolCallId)),Pt(w,y,a.turnId),!w.ownsOutput){await $();return}c?(await s({type:"data",kind:"agent_session",data:{agentType:"codex",threadId:y.id,turnId:a.turnId}}),await s({type:"tool-output-available",toolCallId:c.itemId,toolName:"request_user_input",output:c.response})):O&&await s({type:"tool-output-error",toolCallId:O.toolCallId,toolName:"request_user_input",errorText:"Question interrupted by text response"});for await(const n of a.events)if(n.type!=="item.delta"&&xe(n),n.type==="thread.started")I=n.thread_id,ht(I);else if(n.type==="turn.started")h=n.turn_id,Pt(w,y,h),await E(I,h);else if(n.type==="item.started"){if(x||await E(I,h),await It(n.item,"start")||await nt(n.item)||await Bt(n.item))continue;n.item.type==="agentMessage"||n.item.type==="plan"?await Ct(n.item):n.item.type==="reasoning"&&await vt(n.item)}else if(n.type==="item.delta")x||await E(I,h),n.item_type==="agentMessage"||n.item_type==="plan"?(X.add(n.item_id),await s({type:"text-delta",id:n.item_id,delta:n.delta})):n.item_type==="reasoning"&&(X.add(n.item_id),await s({type:"reasoning-delta",id:n.item_id,delta:n.delta}));else if(n.type==="item.updated"){if(await nt(n.item))continue}else if(n.type==="item.completed"){if(x||await E(I,h),await It(n.item,"end")||await nt(n.item)||await zt(n.item))continue;if(n.item.type==="agentMessage"||n.item.type==="plan"){await Ct(n.item);const u=Et(n.item)??"";u&&!X.has(n.item.id)&&await s({type:"text-delta",id:n.item.id,delta:u}),await s({type:"text-end",id:n.item.id})}else if(n.item.type==="reasoning"){await vt(n.item);const u=Et(n.item)??"";u&&!X.has(n.item.id)&&await s({type:"reasoning-delta",id:n.item.id,delta:u}),await s({type:"reasoning-end",id:n.item.id})}}else if(n.type==="item.tool.requestUserInput"){x||await E(I,h),k.set(J(l,n.params.itemId),{conversationId:l,toolCallId:n.params.itemId,toolName:"request_user_input",turnId:n.params.turnId,since:new Date().toISOString()}),await s({type:"tool-input-available",toolCallId:n.params.itemId,toolName:"request_user_input",input:n.params}),await $();return}else if(n.type==="turn.diff.updated")x||await E(I,h),ot.set(Nt(l,y.id),n.diff);else if(n.type==="thread.tokenUsage.updated"){const u=ie(n.usage);if(u){const g=ne(wt,u.last);g&&(wt=g,et={...u,last:g})}}else if(n.type==="turn.failed"){mt!==n.error.message&&await s({type:"error",errorText:n.error.message}),await at("error"),F=!0,Dt(w);return}else n.type==="error"&&(mt=n.message,await s({type:"error",errorText:n.message}));F=!W.signal.aborted,F&&Dt(w),x||await E(I,h),await at("stop")}catch(a){const n=a instanceof Error?a.message:"Chat stream failed";x||await E(I,h),await s({type:"error",errorText:n}),await at("error")}finally{P&&clearTimeout(P),_?.removeEventListener("abort",tt),z.delete(W),F||qt(w)}}}}async function Ae({body:t,headers:e,signal:r},o){await(await Ut({body:t,headers:e})).run(o,r)}B.post("/v1/chat/stream",Jt("json",Gt),async t=>{const e=t.req.valid("json"),r=await(async()=>{try{return await Ut({body:e,headers:t.req.raw.headers})}catch(o){throw o instanceof q?new D(o.status,{message:o.message}):o}})();return Kt(t,async o=>{const i=async d=>{await o.writeSSE(d)};await r.run({writeData:async d=>{await i({data:JSON.stringify(d)})},writeDone:async()=>{await i({data:"[DONE]"})}},t.req.raw.signal)})});var We=B;export{q as ChatRequestError,_t as cancelCodexConversation,Le as codexActivitySnapshot,We as default,Ae as handleCodexChatStream,Ut as prepareCodexChatStream};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@messenger-agent/codex-agent",
3
- "version": "0.26.0-alpha.5",
3
+ "version": "0.26.0-alpha.7",
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.26.0-alpha.5"
25
+ "@messenger-agent/shared": "0.26.0-alpha.7"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/ws": "^8.18.1"