@sleep2agi/agent-network 0.0.14 → 0.0.16

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/README.md CHANGED
@@ -64,11 +64,11 @@ anet init profile <id> [options] # → .anet/profiles/<id>.json
64
64
 
65
65
  ```
66
66
  ~/.anet/config.json 全局(hub URL,一次性)
67
- {workpath}/.anet/server.ts Channel 插件
67
+ {workpath}/.anet/node-server.ts Channel 插件
68
68
  {workpath}/.anet/package.json 依赖声明
69
69
  {workpath}/.anet/.env COMMHUB_URL
70
70
  {workpath}/.anet/profiles/cmd.json 启动 profile
71
- {workpath}/.mcp.json MCP 配置(commhub → .anet/server.ts)
71
+ {workpath}/.mcp.json MCP 配置(commhub → .anet/node-server.ts)
72
72
  ```
73
73
 
74
74
  全部在项目目录内,不碰全局 `~/.claude/`。
@@ -92,10 +92,10 @@ anet init
92
92
  ```bash
93
93
  cd ~/my-project
94
94
  anet init project
95
- # ✅ .anet/server.ts
95
+ # ✅ .anet/node-server.ts
96
96
  # ✅ Dependencies installed
97
97
  # CommHub URL: http://YOUR_IP:9200
98
- # .mcp.json: commhub → .anet/server.ts
98
+ # .mcp.json: commhub → .anet/node-server.ts
99
99
  ```
100
100
 
101
101
  #### anet init profile
@@ -206,7 +206,11 @@ const { CommHub } = require('@sleep2agi/agent-network');
206
206
 
207
207
  | 版本 | 变更 |
208
208
  |------|------|
209
- | 0.0.12 | README 同步 + 文档更新 |
209
+ | 0.0.16 | server.ts node-server.ts,从 npm 包内 copy(不依赖 GitHub 下载) |
210
+ | 0.0.15 | 自动去掉 hub URL 结尾斜杠 |
211
+ | 0.0.14 | init project 自动生成 CLAUDE.md |
212
+ | 0.0.13 | init 交互输入后不再卡住 |
213
+ | 0.0.12 | README 同步 |
210
214
  | 0.0.11 | node config 加 anet_version 字段 |
211
215
  | 0.0.10 | resumeAlias 字段,resume 按名字搜索 |
212
216
  | 0.0.9 | start/resume 分离 |
package/dist/bin/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import{createRequire as n}from"node:module";var m=Object.defineProperty;var c=(z)=>z;function p(z,B){this[z]=c.bind(null,B)}var d=(z,B)=>{for(var Q in B)m(z,Q,{get:B[Q],enumerable:!0,configurable:!0,set:p.bind(B,Q)})};var l=(z,B)=>()=>(z&&(B=z(z=0)),B);var t=n(import.meta.url);var C={};d(C,{default:()=>o,CommHub:()=>H});import{EventEmitter as r}from"events";import{hostname as w}from"os";var H,o;var j=l(()=>{H=class H extends r{url;alias;token;agent;resumeId;heartbeatInterval;reconnectDelay;heartbeatTimer;sseAbort;running=!1;constructor(z){super();if(this.url=z.url.replace(/\/$/,""),this.alias=z.alias,this.token=z.token,this.agent=z.agent||"sdk",this.resumeId=`sdk-${z.alias}-${Date.now().toString(36)}`,this.heartbeatInterval=z.heartbeatInterval??180000,this.reconnectDelay=z.reconnectDelay??3000,z.autoConnect!==!1)this.connect()}log(z){console.log(`[${new Date().toTimeString().slice(0,8)}] [commhub:${this.alias}] ${z}`)}async call(z,B){let Q={"Content-Type":"application/json"};if(this.token)Q.Authorization=`Bearer ${this.token}`;let X=await(await fetch(`${this.url}/mcp`,{method:"POST",headers:Q,body:JSON.stringify({jsonrpc:"2.0",id:Date.now(),method:"tools/call",params:{name:z,arguments:B}})})).json(),Z=X?.result?.content?.[0]?.text;return Z?JSON.parse(Z):X}async connect(){if(this.running)return;this.running=!0,await this.status("idle"),this.log("registered"),this.heartbeatTimer=setInterval(()=>{this.status("idle").catch((z)=>this.log(`heartbeat failed: ${z.message}`))},this.heartbeatInterval),this.connectSSE()}async disconnect(){if(this.running=!1,this.sseAbort?.abort(),this.heartbeatTimer)clearInterval(this.heartbeatTimer);await this.status("offline").catch(()=>{}),this.log("disconnected")}async send(z,B,Q="normal"){return this.call("send_task",{alias:z,task:B,priority:Q,from_session:this.alias})}async message(z,B){return this.call("send_message",{alias:z,message:B,from_session:this.alias})}async reply(z,B,Q="completed"){return this.call("reply",{task_id:z,text:B,status:Q})}async status(z,B){return this.call("report_status",{resume_id:this.resumeId,alias:this.alias,status:z,server:w(),hostname:w(),agent:this.agent,project_dir:process.cwd(),...B})}async getAllStatus(){return this.call("get_all_status",{})}async broadcast(z,B){return this.call("broadcast",{message:z,filter_server:B?.server,filter_status:B?.status})}async connectSSE(){let z=encodeURIComponent(this.alias),B=`${this.url}/events/${z}`,Q=this.reconnectDelay;while(this.running){try{this.sseAbort=new AbortController;let V={Accept:"text/event-stream"};if(this.token)V.Authorization=`Bearer ${this.token}`;let X=await fetch(B,{headers:V,signal:this.sseAbort.signal});if(!X.ok||!X.body){this.log(`SSE failed: ${X.status}`),await this.sleep(Q),Q=Math.min(Q*1.5,60000);continue}Q=this.reconnectDelay;let Z=X.body.getReader(),U=new TextDecoder,W="";while(this.running){let{done:Y,value:$}=await Z.read();if(Y)break;W+=U.decode($,{stream:!0});let R=W.split(`
3
- `);W=R.pop()||"";for(let q of R){if(!q.startsWith("data: "))continue;try{let T=JSON.parse(q.slice(6));if(T.type==="connected"){this.log("SSE connected"),this.emit("connected");continue}if(T.type==="new_task"||T.type==="new_message"||T.type==="broadcast")await this.processInbox()}catch{}}}}catch(V){if(V.name==="AbortError")break;this.emit("error",V),this.log(`SSE error: ${V.message}`)}if(this.running)this.emit("disconnected"),this.log(`SSE reconnecting in ${Q/1000}s...`),await this.sleep(Q),Q=Math.min(Q*1.5,60000)}}async processInbox(){try{let B=(await this.call("get_inbox",{alias:this.alias,limit:10}))?.messages||[];for(let Q of B)await this.call("ack_inbox",{alias:this.alias,message_id:Q.id}),this.log(`← ${Q.from_session}: ${Q.content.slice(0,60)}`),this.emit("task",Q),this.emit("message",Q)}catch(z){this.log(`inbox error: ${z.message}`)}}sleep(z){return new Promise((B)=>setTimeout(B,z))}};o=H});import{readFileSync as E,writeFileSync as K,existsSync as _,mkdirSync as G,readdirSync as b}from"fs";import{join as N}from"path";import{spawn as a}from"child_process";var L=process.argv.slice(2),A=L[0],O=process.env.HOME||process.env.USERPROFILE||"~";function S(){return N(O,".anet","config.json")}function k(){return N(process.cwd(),".anet","profiles")}function I(){let z=S();if(_(z))try{return JSON.parse(E(z,"utf-8"))}catch{}return{}}function i(z){let B=N(O,".anet");G(B,{recursive:!0}),K(N(B,"config.json"),JSON.stringify(z,null,2)+`
4
- `)}function M(z){let B=N(k(),`${z}.json`);if(_(B))try{return JSON.parse(E(B,"utf-8"))}catch{}return null}function e(z,B){let Q=k();G(Q,{recursive:!0}),K(N(Q,`${z}.json`),JSON.stringify(B,null,2)+`
5
- `)}function y(){let z=k();if(!_(z))return[];return b(z).filter((B)=>B.endsWith(".json")).map((B)=>B.replace(/\.json$/,""))}function F(){let z={_channels:[],_envs:[]};for(let B=0;B<L.length;B++){if(L[B]==="--channel"&&L[B+1]){z._channels.push(L[++B]);continue}if(L[B]==="--env"&&L[B+1]){z._envs.push(L[++B]);continue}if(L[B].startsWith("--")&&L[B+1]&&!L[B+1].startsWith("--"))z[L[B].slice(2)]=L[++B]}return z}function P(){console.log(`
2
+ import{createRequire as t}from"node:module";var p=Object.defineProperty;var c=(z)=>z;function d(z,B){this[z]=c.bind(null,B)}var l=(z,B)=>{for(var Q in B)p(z,Q,{get:B[Q],enumerable:!0,configurable:!0,set:d.bind(B,Q)})};var n=(z,B)=>()=>(z&&(B=z(z=0)),B);var w=t(import.meta.url);var j={};l(j,{default:()=>o,CommHub:()=>H});import{EventEmitter as r}from"events";import{hostname as C}from"os";var H,o;var P=n(()=>{H=class H extends r{url;alias;token;agent;resumeId;heartbeatInterval;reconnectDelay;heartbeatTimer;sseAbort;running=!1;constructor(z){super();if(this.url=z.url.replace(/\/$/,""),this.alias=z.alias,this.token=z.token,this.agent=z.agent||"sdk",this.resumeId=`sdk-${z.alias}-${Date.now().toString(36)}`,this.heartbeatInterval=z.heartbeatInterval??180000,this.reconnectDelay=z.reconnectDelay??3000,z.autoConnect!==!1)this.connect()}log(z){console.log(`[${new Date().toTimeString().slice(0,8)}] [commhub:${this.alias}] ${z}`)}async call(z,B){let Q={"Content-Type":"application/json"};if(this.token)Q.Authorization=`Bearer ${this.token}`;let X=await(await fetch(`${this.url}/mcp`,{method:"POST",headers:Q,body:JSON.stringify({jsonrpc:"2.0",id:Date.now(),method:"tools/call",params:{name:z,arguments:B}})})).json(),Z=X?.result?.content?.[0]?.text;return Z?JSON.parse(Z):X}async connect(){if(this.running)return;this.running=!0,await this.status("idle"),this.log("registered"),this.heartbeatTimer=setInterval(()=>{this.status("idle").catch((z)=>this.log(`heartbeat failed: ${z.message}`))},this.heartbeatInterval),this.connectSSE()}async disconnect(){if(this.running=!1,this.sseAbort?.abort(),this.heartbeatTimer)clearInterval(this.heartbeatTimer);await this.status("offline").catch(()=>{}),this.log("disconnected")}async send(z,B,Q="normal"){return this.call("send_task",{alias:z,task:B,priority:Q,from_session:this.alias})}async message(z,B){return this.call("send_message",{alias:z,message:B,from_session:this.alias})}async reply(z,B,Q="completed"){return this.call("reply",{task_id:z,text:B,status:Q})}async status(z,B){return this.call("report_status",{resume_id:this.resumeId,alias:this.alias,status:z,server:C(),hostname:C(),agent:this.agent,project_dir:process.cwd(),...B})}async getAllStatus(){return this.call("get_all_status",{})}async broadcast(z,B){return this.call("broadcast",{message:z,filter_server:B?.server,filter_status:B?.status})}async connectSSE(){let z=encodeURIComponent(this.alias),B=`${this.url}/events/${z}`,Q=this.reconnectDelay;while(this.running){try{this.sseAbort=new AbortController;let V={Accept:"text/event-stream"};if(this.token)V.Authorization=`Bearer ${this.token}`;let X=await fetch(B,{headers:V,signal:this.sseAbort.signal});if(!X.ok||!X.body){this.log(`SSE failed: ${X.status}`),await this.sleep(Q),Q=Math.min(Q*1.5,60000);continue}Q=this.reconnectDelay;let Z=X.body.getReader(),N=new TextDecoder,W="";while(this.running){let{done:Y,value:U}=await Z.read();if(Y)break;W+=N.decode(U,{stream:!0});let R=W.split(`
3
+ `);W=R.pop()||"";for(let q of R){if(!q.startsWith("data: "))continue;try{let O=JSON.parse(q.slice(6));if(O.type==="connected"){this.log("SSE connected"),this.emit("connected");continue}if(O.type==="new_task"||O.type==="new_message"||O.type==="broadcast")await this.processInbox()}catch{}}}}catch(V){if(V.name==="AbortError")break;this.emit("error",V),this.log(`SSE error: ${V.message}`)}if(this.running)this.emit("disconnected"),this.log(`SSE reconnecting in ${Q/1000}s...`),await this.sleep(Q),Q=Math.min(Q*1.5,60000)}}async processInbox(){try{let B=(await this.call("get_inbox",{alias:this.alias,limit:10}))?.messages||[];for(let Q of B)await this.call("ack_inbox",{alias:this.alias,message_id:Q.id}),this.log(`← ${Q.from_session}: ${Q.content.slice(0,60)}`),this.emit("task",Q),this.emit("message",Q)}catch(z){this.log(`inbox error: ${z.message}`)}}sleep(z){return new Promise((B)=>setTimeout(B,z))}};o=H});import{readFileSync as E,writeFileSync as K,existsSync as _,mkdirSync as G,readdirSync as S}from"fs";import{join as L}from"path";import{spawn as a}from"child_process";var __dirname="/home/vansin/agent-orchestra/agent-network/bin",$=process.argv.slice(2),A=$[0],T=process.env.HOME||process.env.USERPROFILE||"~";function y(){return L(T,".anet","config.json")}function k(){return L(process.cwd(),".anet","profiles")}function I(){let z=y();if(_(z))try{return JSON.parse(E(z,"utf-8"))}catch{}return{}}function i(z){let B=L(T,".anet");G(B,{recursive:!0}),K(L(B,"config.json"),JSON.stringify(z,null,2)+`
4
+ `)}function M(z){let B=L(k(),`${z}.json`);if(_(B))try{return JSON.parse(E(B,"utf-8"))}catch{}return null}function e(z,B){let Q=k();G(Q,{recursive:!0}),K(L(Q,`${z}.json`),JSON.stringify(B,null,2)+`
5
+ `)}function u(){let z=k();if(!_(z))return[];return S(z).filter((B)=>B.endsWith(".json")).map((B)=>B.replace(/\.json$/,""))}function D(){let z={_channels:[],_envs:[]};for(let B=0;B<$.length;B++){if($[B]==="--channel"&&$[B+1]){z._channels.push($[++B]);continue}if($[B]==="--env"&&$[B+1]){z._envs.push($[++B]);continue}if($[B].startsWith("--")&&$[B+1]&&!$[B+1].startsWith("--"))z[$[B].slice(2)]=$[++B]}return z}function v(){console.log(`
6
6
  anet — AI Agent Network CLI
7
7
 
8
8
  anet init Configure hub URL (global, once)
@@ -20,10 +20,11 @@ Quick start:
20
20
  anet init profile 指挥室 --alias 指挥室 --channel server:commhub
21
21
  anet start 指挥室 # 新建
22
22
  anet resume 指挥室 # 下次恢复
23
- `)}async function s(){let z=F(),B=z.hub;if(!B)process.stdout.write("CommHub URL (e.g. http://YOUR_IP:9200): "),B=await new Promise((V)=>{process.stdin.setEncoding("utf-8"),process.stdin.once("data",(X)=>{process.stdin.unref(),V(X.toString().trim())})});if(!B)console.error("Error: hub URL required"),process.exit(1);try{let X=await(await fetch(`${B}/health`)).json();console.log(`✅ CommHub v${X.version} — ${X.sessions} sessions, ${X.sse_connections} SSE`)}catch(V){console.error(`❌ Cannot reach ${B}: ${V.message}`),process.exit(1)}let Q=I();if(Q.hub=B,z.token)Q.token=z.token;i(Q),console.log(`
24
- Saved to ${S()}`),console.log("Next: anet init project")}async function zz(){let B=I().hub;if(!B)console.error("Run 'anet init' first to configure hub URL"),process.exit(1);let Q=N(process.cwd(),".anet");G(Q,{recursive:!0});let V=N(Q,"server.ts");if(!_(V)){console.log("Downloading Channel plugin...");try{let $=await fetch("https://raw.githubusercontent.com/sleep2agi/agent-comm-hub/main/channel/server.ts");if($.ok)K(V,await $.text()),console.log(" ✅ .anet/server.ts")}catch($){console.log(`Failed: ${$.message}`),console.log(" Manual: curl -sL https://raw.githubusercontent.com/sleep2agi/agent-comm-hub/main/channel/server.ts -o .anet/server.ts")}}else console.log("Channel plugin: exists");let X=N(Q,"package.json");if(!_(X))try{let $=await fetch("https://raw.githubusercontent.com/sleep2agi/agent-comm-hub/main/channel/package.json");if($.ok){K(X,await $.text());try{let{execSync:R}=await import("child_process");R("bun install",{cwd:Q,stdio:"pipe"}),console.log(" ✅ Dependencies installed")}catch{console.log(" ⚠️ Run: cd .anet && bun install")}}}catch{}let Z=N(Q,".env");K(Z,`COMMHUB_URL=${B}
25
- `),console.log(`CommHub URL: ${B}`);let U=N(process.cwd(),".mcp.json"),W={};if(_(U))try{W=JSON.parse(E(U,"utf-8"))}catch{}if(!W.mcpServers?.commhub)W.mcpServers=W.mcpServers||{},W.mcpServers.commhub={type:"stdio",command:"bun",args:[".anet/server.ts"]},K(U,JSON.stringify(W,null,2)+`
26
- `),console.log(".mcp.json: commhub → .anet/server.ts");else console.log(".mcp.json: commhub already set");let Y=N(process.cwd(),"CLAUDE.md");if(!_(Y))K(Y,`# Agent Network (CommHub)
23
+ `)}async function s(){let z=D(),B=z.hub;if(!B)process.stdout.write("CommHub URL (e.g. http://YOUR_IP:9200): "),B=await new Promise((V)=>{process.stdin.setEncoding("utf-8"),process.stdin.once("data",(X)=>{process.stdin.unref(),V(X.toString().trim())})});if(!B)console.error("Error: hub URL required"),process.exit(1);B=B.replace(/\/+$/,"");try{let X=await(await fetch(`${B}/health`)).json();console.log(`✅ CommHub v${X.version} — ${X.sessions} sessions, ${X.sse_connections} SSE`)}catch(V){console.error(`❌ Cannot reach ${B}: ${V.message}`),process.exit(1)}let Q=I();if(Q.hub=B,z.token)Q.token=z.token;i(Q),console.log(`
24
+ Saved to ${y()}`),console.log("Next: anet init project")}async function zz(){let B=I().hub;if(!B)console.error("Run 'anet init' first to configure hub URL"),process.exit(1);let Q=L(process.cwd(),".anet");G(Q,{recursive:!0});let V=L(Q,"node-server.ts");if(!_(V)){let U=L(__dirname,"..","src","node-server.ts");if(_(U)){let{copyFileSync:R}=await import("fs");R(U,V),console.log(" .anet/node-server.ts (from npm package)")}else try{let R=await fetch("https://raw.githubusercontent.com/sleep2agi/agent-comm-hub/main/channel/commhub-channel.ts");if(R.ok)K(V,await R.text()),console.log(" ✅ .anet/node-server.ts (downloaded)");else console.log("Cannot find channel plugin. Copy manually from agent-comm-hub/channel/commhub-channel.ts")}catch{console.log(" Cannot find channel plugin. Copy manually from agent-comm-hub/channel/commhub-channel.ts")}}else console.log(" Channel plugin: exists");let X=L(Q,"package.json");if(!_(X)){K(X,JSON.stringify({private:!0,dependencies:{"@modelcontextprotocol/sdk":"^1.12.0"}},null,2)+`
25
+ `);try{let{execSync:U}=await import("child_process");U("bun install",{cwd:Q,stdio:"pipe"}),console.log(" ✅ Dependencies installed")}catch{console.log(" ⚠️ Run: cd .anet && bun install")}}let Z=L(Q,".env");K(Z,`COMMHUB_URL=${B}
26
+ `),console.log(`CommHub URL: ${B}`);let N=L(process.cwd(),".mcp.json"),W={};if(_(N))try{W=JSON.parse(E(N,"utf-8"))}catch{}if(!W.mcpServers?.commhub)W.mcpServers=W.mcpServers||{},W.mcpServers.commhub={type:"stdio",command:"bun",args:[".anet/node-server.ts"]},K(N,JSON.stringify(W,null,2)+`
27
+ `),console.log(".mcp.json: commhub → .anet/node-server.ts");else console.log(".mcp.json: commhub already set");let Y=L(process.cwd(),"CLAUDE.md");if(!_(Y))K(Y,`# Agent Network (CommHub)
27
28
 
28
29
  ## 通信方式
29
30
 
@@ -62,16 +63,16 @@ commhub_get_all_status()
62
63
  - 回复指挥室用 commhub_send_task(不是 commhub_reply,reply 不推送)
63
64
  - 不要猜 alias,用 get_all_status 查
64
65
  `),console.log("CLAUDE.md: created");else console.log("CLAUDE.md: already exists");console.log(`
65
- ✅ Project ready. Next: anet init profile <id> --alias <名字> --channel server:commhub`)}function Bz(){let z=L[2];if(!z)console.error("Usage: anet init profile <id> --alias <名字> [--channel ...] [--env ...]"),process.exit(1);let B=I(),Q=F(),V=Q.alias||z,X=Q.hub||B.hub;if(!X)console.error("Run 'anet init' first to configure hub URL"),process.exit(1);let Z={};for(let R of Q._envs){let q=R.indexOf("=");if(q>0)Z[R.slice(0,q)]=R.slice(q+1)}let U={anet_version:"0.0.11",...Q.name?{name:Q.name}:{},alias:V,hub:X,channels:Q._channels.length>0?Q._channels:["server:commhub"],env:Z,flags:{dangerouslySkipPermissions:!0,...Q["teammate-mode"]?{teammateMode:Q["teammate-mode"]}:{}},...Q.resume?{resume:Q.resume}:{},...Q["resume-alias"]?{resumeAlias:Q["resume-alias"]}:{}},W=N(O,".claude","channels","commhub"),Y=process.cwd().replace(/\//g,"-"),$=N(W,Y);if(G($,{recursive:!0}),K(N($,".env"),`COMMHUB_ALIAS=${V}
66
- `),e(z,U),console.log(`
67
- ✅ Profile "${z}" saved`),console.log(` alias: ${V}`),console.log(` channels: ${U.channels.join(", ")}`),Object.keys(Z).length)console.log(` env: ${Object.keys(Z).join(", ")}`);console.log(`
68
- Start: anet start ${z}`)}function u(z,B){let Q=M(z);if(!Q)console.error(`Profile "${z}" not found. Run: anet ls`),process.exit(1);let V={...process.env,COMMHUB_ALIAS:Q.alias};for(let[W,Y]of Object.entries(Q.env))V[W]=Y.replace(/^~/,O);let X=[];if(Q.flags.dangerouslySkipPermissions)X.push("--dangerously-skip-permissions");for(let W of Q.channels)if(W.startsWith("server:"))X.push("--dangerously-load-development-channels",W);else X.push("--channels",W);if(Q.flags.teammateMode)X.push("--teammate-mode",Q.flags.teammateMode);if(B==="resume"){let W=Q.resumeAlias||Q.name||Q.alias;X.push("--resume",W)}X.push("-n",Q.name||Q.alias),console.log(`[anet] ${B==="start"?"Starting new":"Resuming"} "${z}" (${Q.alias})...
69
- `),a("claude",X,{env:V,stdio:"inherit",shell:!0}).on("exit",(W)=>process.exit(W||0))}function v(){let z=L[1];if(!z){h("start");return}u(z,"start")}function Qz(){let z=L[1];if(!z){h("resume");return}u(z,"resume")}function h(z){let B=y();if(B.length===0){console.log("No profiles. Run: anet init profile <id> --alias <名字>");return}console.log(`
66
+ ✅ Project ready. Next: anet init profile <id> --alias <名字> --channel server:commhub`)}function Bz(){let z=$[2];if(!z)console.error("Usage: anet init profile <id> --alias <名字> [--channel ...] [--env ...]"),process.exit(1);let B=I(),Q=D(),V=Q.alias||z,X=Q.hub||B.hub;if(!X)console.error("Run 'anet init' first to configure hub URL"),process.exit(1);let Z={};for(let R of Q._envs){let q=R.indexOf("=");if(q>0)Z[R.slice(0,q)]=R.slice(q+1)}let N={anet_version:"0.0.11",...Q.name?{name:Q.name}:{},alias:V,hub:X,channels:Q._channels.length>0?Q._channels:["server:commhub"],env:Z,flags:{dangerouslySkipPermissions:!0,...Q["teammate-mode"]?{teammateMode:Q["teammate-mode"]}:{}},...Q.resume?{resume:Q.resume}:{},...Q["resume-alias"]?{resumeAlias:Q["resume-alias"]}:{}},W=L(T,".claude","channels","commhub"),Y=process.cwd().replace(/\//g,"-"),U=L(W,Y);if(G(U,{recursive:!0}),K(L(U,".env"),`COMMHUB_ALIAS=${V}
67
+ `),e(z,N),console.log(`
68
+ ✅ Profile "${z}" saved`),console.log(` alias: ${V}`),console.log(` channels: ${N.channels.join(", ")}`),Object.keys(Z).length)console.log(` env: ${Object.keys(Z).join(", ")}`);console.log(`
69
+ Start: anet start ${z}`)}function f(z,B){let Q=M(z);if(!Q)console.error(`Profile "${z}" not found. Run: anet ls`),process.exit(1);let V={...process.env,COMMHUB_ALIAS:Q.alias};for(let[W,Y]of Object.entries(Q.env))V[W]=Y.replace(/^~/,T);let X=[];if(Q.flags.dangerouslySkipPermissions)X.push("--dangerously-skip-permissions");for(let W of Q.channels)if(W.startsWith("server:"))X.push("--dangerously-load-development-channels",W);else X.push("--channels",W);if(Q.flags.teammateMode)X.push("--teammate-mode",Q.flags.teammateMode);if(B==="resume"){let W=Q.resumeAlias||Q.name||Q.alias;X.push("--resume",W)}X.push("-n",Q.name||Q.alias),console.log(`[anet] ${B==="start"?"Starting new":"Resuming"} "${z}" (${Q.alias})...
70
+ `),a("claude",X,{env:V,stdio:"inherit",shell:!0}).on("exit",(W)=>process.exit(W||0))}function b(){let z=$[1];if(!z){h("start");return}f(z,"start")}function Qz(){let z=$[1];if(!z){h("resume");return}f(z,"resume")}function h(z){let B=u();if(B.length===0){console.log("No profiles. Run: anet init profile <id> --alias <名字>");return}console.log(`
70
71
  Profiles:
71
72
  `);for(let Q of B){let V=M(Q);console.log(` ${Q}${V?.name?` (${V.name})`:""} → ${V?.alias} [${V?.channels.join(", ")}]`)}console.log(`
72
73
  anet ${z} <id>
73
- `)}async function Vz(){let z=y();if(z.length>0){console.log(`
74
+ `)}async function Vz(){let z=u();if(z.length>0){console.log(`
74
75
  Profiles:
75
- `);for(let W of z){let Y=M(W);console.log(` ${W}${Y?.name?` (${Y.name})`:""} → ${Y?.alias} [${Y?.channels.join(", ")}]`)}console.log()}let B=process.cwd(),Q=N(O,".claude","sessions"),V=[];if(_(Q))for(let W of b(Q).filter((Y)=>Y.endsWith(".json")))try{let Y=JSON.parse(E(N(Q,W),"utf-8"));if(Y.cwd===B)V.push(Y)}catch{}if(V.length===0&&z.length===0){console.log("No sessions or profiles in this directory."),console.log(`Get started: anet init
76
- `);return}let X=I(),Z=[],U={};if(X.hub)try{let[W,Y]=await Promise.all([fetch(`${X.hub}/api/status`).then(($)=>$.json()),fetch(`${X.hub}/health`).then(($)=>$.json())]);Z=W.sessions||[],U=Y.sse_sessions||{}}catch{}if(V.length>0){console.log(`Sessions (${B}):
77
- `),console.log(" SESSION PID NETWORK"),console.log(" ──────────────────── ─────── ─────────────────────");for(let W of V){let Y=W.sessionId.slice(0,18),$=!1;try{process.kill(W.pid,0),$=!0}catch{}let R="(not in network)",q=B.replace(/\//g,"-"),T=N(O,".claude","channels","commhub",q,".env");if(_(T)){let D=E(T,"utf-8").match(/COMMHUB_ALIAS=(.+)/);if(D){let J=D[1].trim(),x=Z.find((g)=>g.alias===J),f=U[J]?"●":"○";R=x?`${J} ${x.status} ${f}`:`${J} (not registered)`}}console.log(` ${Y} ${($?`${W.pid}`:`${W.pid}✕`).padEnd(7)} ${R}`)}console.log()}}async function Wz(){let z=I(),B=F(),Q=process.env.COMMHUB_URL||B.hub||z.hub||"http://127.0.0.1:9200",V=process.env.COMMHUB_ALIAS||B.alias;if(!V)console.error("Error: --alias required"),process.exit(1);let{CommHub:X}=await Promise.resolve().then(() => (j(),C)),Z=new X({url:Q,alias:V});Z.on("task",async(U)=>{console.log(`[${V}] ← ${U.from_session}: ${U.content.slice(0,100)}`),await Z.send(U.from_session,`[${V}] 收到: ${U.content.slice(0,200)}`)}),Z.on("connected",()=>console.log(`[${V}] Connected`)),Z.on("disconnected",()=>console.log(`[${V}] Reconnecting...`)),process.on("SIGINT",()=>Z.disconnect().then(()=>process.exit(0))),console.log(`[${V}] Listening on ${Q}`)}switch(A){case"init":if(L[1]==="project")zz();else if(L[1]==="profile")Bz();else s();break;case"start":v();break;case"resume":Qz();break;case"ls":case"list":Vz();break;case"run":Wz();break;case"--help":case"-h":case void 0:P();break;default:if(M(A))L.unshift("start"),v();else console.error(`Unknown: ${A}`),P(),process.exit(1)}
76
+ `);for(let W of z){let Y=M(W);console.log(` ${W}${Y?.name?` (${Y.name})`:""} → ${Y?.alias} [${Y?.channels.join(", ")}]`)}console.log()}let B=process.cwd(),Q=L(T,".claude","sessions"),V=[];if(_(Q))for(let W of S(Q).filter((Y)=>Y.endsWith(".json")))try{let Y=JSON.parse(E(L(Q,W),"utf-8"));if(Y.cwd===B)V.push(Y)}catch{}if(V.length===0&&z.length===0){console.log("No sessions or profiles in this directory."),console.log(`Get started: anet init
77
+ `);return}let X=I(),Z=[],N={};if(X.hub)try{let[W,Y]=await Promise.all([fetch(`${X.hub}/api/status`).then((U)=>U.json()),fetch(`${X.hub}/health`).then((U)=>U.json())]);Z=W.sessions||[],N=Y.sse_sessions||{}}catch{}if(V.length>0){console.log(`Sessions (${B}):
78
+ `),console.log(" SESSION PID NETWORK"),console.log(" ──────────────────── ─────── ─────────────────────");for(let W of V){let Y=W.sessionId.slice(0,18),U=!1;try{process.kill(W.pid,0),U=!0}catch{}let R="(not in network)",q=B.replace(/\//g,"-"),O=L(T,".claude","channels","commhub",q,".env");if(_(O)){let F=E(O,"utf-8").match(/COMMHUB_ALIAS=(.+)/);if(F){let J=F[1].trim(),x=Z.find((m)=>m.alias===J),g=N[J]?"●":"○";R=x?`${J} ${x.status} ${g}`:`${J} (not registered)`}}console.log(` ${Y} ${(U?`${W.pid}`:`${W.pid}✕`).padEnd(7)} ${R}`)}console.log()}}async function Wz(){let z=I(),B=D(),Q=process.env.COMMHUB_URL||B.hub||z.hub||"http://127.0.0.1:9200",V=process.env.COMMHUB_ALIAS||B.alias;if(!V)console.error("Error: --alias required"),process.exit(1);let{CommHub:X}=await Promise.resolve().then(() => (P(),j)),Z=new X({url:Q,alias:V});Z.on("task",async(N)=>{console.log(`[${V}] ← ${N.from_session}: ${N.content.slice(0,100)}`),await Z.send(N.from_session,`[${V}] 收到: ${N.content.slice(0,200)}`)}),Z.on("connected",()=>console.log(`[${V}] Connected`)),Z.on("disconnected",()=>console.log(`[${V}] Reconnecting...`)),process.on("SIGINT",()=>Z.disconnect().then(()=>process.exit(0))),console.log(`[${V}] Listening on ${Q}`)}switch(A){case"init":if($[1]==="project")zz();else if($[1]==="profile")Bz();else s();break;case"start":b();break;case"resume":Qz();break;case"ls":case"list":Vz();break;case"run":Wz();break;case"--help":case"-h":case void 0:v();break;default:if(M(A))$.unshift("start"),b();else console.error(`Unknown: ${A}`),v(),process.exit(1)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "AI Agent Network — Server + Client + Setup in one package. SSE real-time communication for multi-agent orchestration.",
5
5
  "type": "module",
6
6
  "main": "dist/src/client.js",
@@ -19,7 +19,8 @@
19
19
  },
20
20
  "files": [
21
21
  "dist",
22
- "src/server.ts"
22
+ "src/server.ts",
23
+ "src/node-server.ts"
23
24
  ],
24
25
  "scripts": {
25
26
  "build": "bun build src/client.ts --outdir dist/src --target node --minify && bun build bin/cli.ts --outdir dist/bin --target node --minify --external @sleep2agi/commhub-server --external bun:sqlite --external '../../server/*' && tsc --emitDeclarationOnly --declaration --outDir dist",
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * CommHub Channel Plugin for Claude Code
4
+ *
5
+ * Alias resolution (priority order):
6
+ * 1. COMMHUB_ALIAS env var
7
+ * 2. Project .env: ~/.claude/channels/commhub/{project-path}/.env
8
+ * 3. tmux session name
9
+ * 4. hostname
10
+ *
11
+ * Shared config from: ~/.claude/channels/commhub/.env
12
+ * COMMHUB_URL, COMMHUB_TOKEN
13
+ */
14
+
15
+ import { readFileSync, existsSync } from "fs";
16
+ import { join } from "path";
17
+ import { hostname } from "os";
18
+ import { execSync } from "child_process";
19
+
20
+ // ── .env loader helper ────────────────────────────────
21
+ function loadEnvFile(path: string): void {
22
+ if (!existsSync(path)) return;
23
+ for (const line of readFileSync(path, "utf-8").split("\n")) {
24
+ const trimmed = line.trim();
25
+ if (!trimmed || trimmed.startsWith("#")) continue;
26
+ const eq = trimmed.indexOf("=");
27
+ if (eq < 0) continue;
28
+ const key = trimmed.slice(0, eq).trim();
29
+ const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
30
+ if (!process.env[key]) process.env[key] = val;
31
+ }
32
+ }
33
+
34
+ // ── Load shared config ────────────────────────────────
35
+ const HOME = process.env.HOME || "~";
36
+ const COMMHUB_DIR = join(HOME, ".claude/channels/commhub");
37
+ loadEnvFile(join(COMMHUB_DIR, ".env"));
38
+
39
+ // ── Load project-specific config ──────────────────────
40
+ // /home/vansin/vincent → -home-vansin-vincent
41
+ const projectPath = process.cwd().replace(/\//g, "-");
42
+ loadEnvFile(join(COMMHUB_DIR, projectPath, ".env"));
43
+
44
+ // ── Get tmux session name ─────────────────────────────
45
+ function getTmuxSessionName(): string {
46
+ try {
47
+ return execSync("tmux display-message -p '#S'", { encoding: "utf-8", timeout: 2000 }).trim();
48
+ } catch {
49
+ return "";
50
+ }
51
+ }
52
+
53
+ // ── Resolve config ────────────────────────────────────
54
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
55
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
56
+ import {
57
+ ListToolsRequestSchema,
58
+ CallToolRequestSchema,
59
+ } from "@modelcontextprotocol/sdk/types.js";
60
+
61
+ const COMMHUB_URL = process.env.COMMHUB_URL || "http://127.0.0.1:9200";
62
+ const TMUX_NAME = process.env.COMMHUB_TMUX || getTmuxSessionName();
63
+ const ALIAS = process.env.COMMHUB_ALIAS || TMUX_NAME || hostname();
64
+ const RESUME_ID = process.env.COMMHUB_RESUME_ID || process.env.CLAUDE_RESUME_ID || crypto.randomUUID();
65
+ const AUTH_TOKEN = process.env.COMMHUB_TOKEN || "";
66
+
67
+ function log(msg: string) {
68
+ const ts = new Date().toTimeString().slice(0, 8);
69
+ process.stderr.write(`[${ts}] [commhub] ${msg}\n`);
70
+ }
71
+
72
+ function sleep(ms: number): Promise<void> {
73
+ return new Promise((r) => setTimeout(r, ms));
74
+ }
75
+
76
+ log(`ENV: URL=${COMMHUB_URL} ALIAS=${ALIAS} RESUME_ID=${RESUME_ID.slice(0, 8)}... TMUX=${TMUX_NAME || "none"} CWD=${process.cwd()} PROJECT_ENV=${projectPath}`);
77
+
78
+ // ── MCP Server with Channel capability ──────────────
79
+ const mcp = new Server(
80
+ {
81
+ name: "commhub-channel",
82
+ version: "0.3.0",
83
+ },
84
+ {
85
+ capabilities: {
86
+ experimental: { "claude/channel": {} },
87
+ tools: {},
88
+ },
89
+ instructions: [
90
+ `Messages from CommHub arrive as <channel source="commhub" task_id="..." priority="..." from="...">`,
91
+ `These are tasks dispatched by the hub or other sessions via the CommHub Server.`,
92
+ `Reply using the commhub_reply tool to report status or results back.`,
93
+ `You can also use commhub_report_status to update your session status.`,
94
+ `Session alias: ${ALIAS}`,
95
+ ].join("\n"),
96
+ }
97
+ );
98
+
99
+ // ── Tools ───────────────────────────────────────────
100
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
101
+ tools: [
102
+ {
103
+ name: "commhub_reply",
104
+ description: "Reply to a CommHub task — report completion or send a message back to the hub.",
105
+ inputSchema: {
106
+ type: "object" as const,
107
+ properties: {
108
+ task_id: { type: "string", description: "The task_id from the channel message (or 'hub' for general)" },
109
+ text: { type: "string", description: "Reply text / result summary" },
110
+ status: {
111
+ type: "string",
112
+ enum: ["completed", "blocked", "error", "in_progress"],
113
+ description: "Task status",
114
+ },
115
+ },
116
+ required: ["text"],
117
+ },
118
+ },
119
+ {
120
+ name: "commhub_report_status",
121
+ description: "Update this session's status in CommHub (working/idle/blocked/error). Returns inbox_count.",
122
+ inputSchema: {
123
+ type: "object" as const,
124
+ properties: {
125
+ status: {
126
+ type: "string",
127
+ enum: ["working", "idle", "blocked", "error"],
128
+ },
129
+ task: { type: "string", description: "Current task description" },
130
+ progress: { type: "number", description: "Progress 0-100" },
131
+ },
132
+ required: ["status"],
133
+ },
134
+ },
135
+ {
136
+ name: "commhub_send_task",
137
+ description: "Send a task to another session via CommHub.",
138
+ inputSchema: {
139
+ type: "object" as const,
140
+ properties: {
141
+ alias: { type: "string", description: "Target session alias" },
142
+ task: { type: "string", description: "Task content" },
143
+ priority: { type: "string", enum: ["high", "normal", "low"], description: "Priority (default: normal)" },
144
+ },
145
+ required: ["alias", "task"],
146
+ },
147
+ },
148
+ {
149
+ name: "commhub_send_message",
150
+ description: "Send a message to another session (no task lifecycle, just chat). Use for replies and status updates.",
151
+ inputSchema: {
152
+ type: "object" as const,
153
+ properties: {
154
+ alias: { type: "string", description: "Target session alias" },
155
+ message: { type: "string", description: "Message content" },
156
+ },
157
+ required: ["alias", "message"],
158
+ },
159
+ },
160
+ {
161
+ name: "commhub_get_all_status",
162
+ description: "Get status of all sessions from CommHub.",
163
+ inputSchema: {
164
+ type: "object" as const,
165
+ properties: {},
166
+ },
167
+ },
168
+ ],
169
+ }));
170
+
171
+ // Helper: call CommHub MCP endpoint
172
+ async function callCommHub(toolName: string, args: Record<string, unknown>): Promise<any> {
173
+ const initRes = await fetch(`${COMMHUB_URL}/mcp`, {
174
+ method: "POST",
175
+ headers: {
176
+ "Content-Type": "application/json",
177
+ Accept: "application/json, text/event-stream",
178
+ ...(AUTH_TOKEN ? { Authorization: `Bearer ${AUTH_TOKEN}` } : {}),
179
+ },
180
+ body: JSON.stringify({
181
+ jsonrpc: "2.0",
182
+ id: 1,
183
+ method: "initialize",
184
+ params: {
185
+ protocolVersion: "2025-03-26",
186
+ capabilities: {},
187
+ clientInfo: { name: "commhub-channel", version: "0.3.0" },
188
+ },
189
+ }),
190
+ });
191
+ if (!initRes.ok) {
192
+ const errText = await initRes.text();
193
+ log(`CommHub init failed: ${initRes.status} ${errText.slice(0, 100)}`);
194
+ return { ok: false, error: `init failed: ${initRes.status}` };
195
+ }
196
+ await initRes.text();
197
+
198
+ const res = await fetch(`${COMMHUB_URL}/mcp`, {
199
+ method: "POST",
200
+ headers: {
201
+ "Content-Type": "application/json",
202
+ Accept: "application/json, text/event-stream",
203
+ ...(AUTH_TOKEN ? { Authorization: `Bearer ${AUTH_TOKEN}` } : {}),
204
+ },
205
+ body: JSON.stringify({
206
+ jsonrpc: "2.0",
207
+ id: 2,
208
+ method: "tools/call",
209
+ params: { name: toolName, arguments: args },
210
+ }),
211
+ });
212
+
213
+ const text = await res.text();
214
+ const dataLine = text.split("\n").find((l) => l.startsWith("data: "));
215
+ if (dataLine) {
216
+ const json = JSON.parse(dataLine.slice(6));
217
+ return json?.result?.content?.[0]?.text ? JSON.parse(json.result.content[0].text) : json;
218
+ }
219
+ return { ok: false, error: "no response" };
220
+ }
221
+
222
+ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
223
+ const { name, arguments: args } = req.params;
224
+
225
+ if (name === "commhub_reply") {
226
+ const { task_id, text, status } = args as any;
227
+ if (status === "completed") {
228
+ const result = await callCommHub("report_completion", {
229
+ alias: ALIAS,
230
+ task: task_id || "task",
231
+ result: text,
232
+ });
233
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
234
+ }
235
+ const result = await callCommHub("report_status", {
236
+ resume_id: RESUME_ID,
237
+ alias: ALIAS,
238
+ status: status === "blocked" ? "blocked" : status === "error" ? "error" : "working",
239
+ task: text.slice(0, 200),
240
+ output: text,
241
+ });
242
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
243
+ }
244
+
245
+ if (name === "commhub_report_status") {
246
+ const { status, task, progress } = args as any;
247
+ const result = await callCommHub("report_status", {
248
+ resume_id: RESUME_ID,
249
+ alias: ALIAS,
250
+ status,
251
+ task,
252
+ progress,
253
+ });
254
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
255
+ }
256
+
257
+ if (name === "commhub_send_task") {
258
+ const { alias, task, priority } = args as any;
259
+ const result = await callCommHub("send_task", {
260
+ alias,
261
+ task,
262
+ priority: priority || "normal",
263
+ from_session: ALIAS,
264
+ });
265
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
266
+ }
267
+
268
+ if (name === "commhub_send_message") {
269
+ const { alias, message } = args as any;
270
+ const result = await callCommHub("send_message", {
271
+ alias,
272
+ message,
273
+ from_session: ALIAS,
274
+ });
275
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
276
+ }
277
+
278
+ if (name === "commhub_get_all_status") {
279
+ const result = await callCommHub("get_all_status", {});
280
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
281
+ }
282
+
283
+ return { content: [{ type: "text", text: JSON.stringify({ error: "unknown tool" }) }] };
284
+ });
285
+
286
+ // ── SSE Listener: subscribe to /events/:alias ─────
287
+ async function connectSSE() {
288
+ const url = `${COMMHUB_URL}/events/${encodeURIComponent(ALIAS)}`;
289
+ const headers: Record<string, string> = {};
290
+ if (AUTH_TOKEN) headers.Authorization = `Bearer ${AUTH_TOKEN}`;
291
+
292
+ log(`connecting to ${url}`);
293
+
294
+ while (true) {
295
+ try {
296
+ const res = await fetch(url, { headers });
297
+ if (!res.ok) {
298
+ log(`SSE error: ${res.status} ${res.statusText}`);
299
+ await sleep(5000);
300
+ continue;
301
+ }
302
+
303
+ const reader = res.body!.getReader();
304
+ const decoder = new TextDecoder();
305
+ let buffer = "";
306
+
307
+ while (true) {
308
+ const { done, value } = await reader.read();
309
+ if (done) break;
310
+
311
+ buffer += decoder.decode(value, { stream: true });
312
+ const lines = buffer.split("\n\n");
313
+ buffer = lines.pop() || "";
314
+
315
+ for (const block of lines) {
316
+ const dataLine = block.split("\n").find((l) => l.startsWith("data: "));
317
+ if (!dataLine) continue;
318
+
319
+ try {
320
+ const event = JSON.parse(dataLine.slice(6));
321
+ await handleSSEEvent(event);
322
+ } catch (e) {
323
+ log(`parse error: ${e}`);
324
+ }
325
+ }
326
+ }
327
+
328
+ log("SSE stream ended, reconnecting...");
329
+ } catch (err) {
330
+ log(`SSE connection error: ${err}`);
331
+ }
332
+
333
+ await sleep(3000);
334
+ }
335
+ }
336
+
337
+ async function handleSSEEvent(event: any) {
338
+ if (event.type === "connected") {
339
+ log(`SSE connected as "${ALIAS}"`);
340
+ return;
341
+ }
342
+
343
+ if (event.type === "new_message") {
344
+ log(`← message from ${event.from}: ${(event.message as string).slice(0, 60)}`);
345
+
346
+ await mcp.notification({
347
+ method: "notifications/claude/channel",
348
+ params: {
349
+ content: event.message,
350
+ meta: {
351
+ sender: event.from || "hub",
352
+ sender_id: "commhub",
353
+ priority: "normal",
354
+ },
355
+ },
356
+ });
357
+
358
+ // Auto-ack the message in inbox
359
+ if (event.message_id) {
360
+ await callCommHub("ack_inbox", { alias: ALIAS, message_id: event.message_id });
361
+ }
362
+ return;
363
+ }
364
+
365
+ if (event.type === "new_task" || event.type === "broadcast") {
366
+ log(`← ${event.type}: inbox_count=${event.inbox_count} priority=${event.priority || "normal"}`);
367
+
368
+ const inbox = await callCommHub("get_inbox", {
369
+ alias: ALIAS,
370
+ limit: 5,
371
+ });
372
+
373
+ if (inbox?.ok && inbox.messages?.length > 0) {
374
+ for (const msg of inbox.messages) {
375
+ const meta: Record<string, string> = {
376
+ sender: msg.from_session || "hub",
377
+ sender_id: "commhub",
378
+ task_id: msg.id,
379
+ priority: msg.priority || "normal",
380
+ };
381
+
382
+ await mcp.notification({
383
+ method: "notifications/claude/channel",
384
+ params: {
385
+ content: msg.content,
386
+ meta,
387
+ },
388
+ });
389
+
390
+ log(`→ injected task ${msg.id.slice(0, 8)} from ${msg.from_session}: ${(msg.content as string).slice(0, 60)}`);
391
+
392
+ await callCommHub("ack_inbox", {
393
+ alias: ALIAS,
394
+ message_id: msg.id,
395
+ });
396
+ }
397
+ }
398
+ }
399
+ }
400
+
401
+ // ── Main ────────────────────────────────────────────
402
+ async function main() {
403
+ const transport = new StdioServerTransport();
404
+ await mcp.connect(transport);
405
+ log("MCP stdio connected");
406
+
407
+ log("starting SSE listener...");
408
+ connectSSE().catch((err) => log(`SSE fatal: ${err}`));
409
+
410
+ callCommHub("report_status", {
411
+ resume_id: RESUME_ID,
412
+ alias: ALIAS,
413
+ status: "idle",
414
+ server: hostname(),
415
+ hostname: hostname(),
416
+ agent: "claude-code",
417
+ project_dir: process.cwd(),
418
+ tmux_name: TMUX_NAME || undefined,
419
+ })
420
+ .then(() => log(`registered as "${ALIAS}" (${RESUME_ID.slice(0, 8)})`))
421
+ .catch((e) => log(`warning: could not register: ${e}`));
422
+
423
+ // Heartbeat: report_status every 3 minutes to prevent offline timeout
424
+ setInterval(() => {
425
+ callCommHub("report_status", {
426
+ resume_id: RESUME_ID,
427
+ alias: ALIAS,
428
+ status: "idle",
429
+ server: hostname(),
430
+ hostname: hostname(),
431
+ agent: "claude-code",
432
+ project_dir: process.cwd(),
433
+ tmux_name: TMUX_NAME || undefined,
434
+ }).catch((e) => log(`heartbeat failed: ${e}`));
435
+ }, 3 * 60 * 1000);
436
+
437
+ log("ready — waiting for events");
438
+ }
439
+
440
+ main().catch((err) => {
441
+ log(`fatal: ${err}`);
442
+ process.exit(1);
443
+ });
444
+
445
+ async function gracefulShutdown() {
446
+ log("shutting down, reporting offline...");
447
+ await callCommHub("report_status", {
448
+ resume_id: RESUME_ID,
449
+ alias: ALIAS,
450
+ status: "error",
451
+ task: "session disconnected",
452
+ }).catch(() => {});
453
+ process.exit(0);
454
+ }
455
+
456
+ process.stdin.on("end", () => gracefulShutdown());
457
+ process.on("SIGTERM", () => gracefulShutdown());
458
+ process.on("SIGINT", () => gracefulShutdown());