@cotal-ai/connector-opencode 0.151.3 → 0.153.1

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/serve.js CHANGED
@@ -1,4 +1,4 @@
1
- import{execFileSync as execFileSync2,spawn}from"node:child_process";import{createServer}from"node:net";import{once}from"node:events";import{randomBytes as randomBytes2}from"node:crypto";import{existsSync,mkdirSync as mkdirSync2,readdirSync,readFileSync as readFileSync2,rmdirSync,rmSync as rmSync2,writeFileSync as writeFileSync2}from"node:fs";import{delimiter,join}from"node:path";import{execFileSync}from"node:child_process";import{linkSync,mkdirSync,readFileSync,rmSync,statSync,unlinkSync,writeFileSync}from"node:fs";import{randomBytes}from"node:crypto";import{dirname}from"node:path";function processStatFields(pid){try{const stat=readFileSync(`/proc/${pid}/stat`,"utf8");const after=stat.lastIndexOf(") ");if(after<0||stat.slice(0,stat.indexOf(" "))!==String(pid))return void 0;return stat.slice(after+2).trim().split(/\s+/)}catch{return void 0}}function processStartToken(pid){if(process.platform==="win32")return void 0;if(process.platform==="linux")return processStatFields(pid)?.[19];try{const out=execFileSync("ps",["-o","lstart=","-p",String(pid)],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim();return out||void 0}catch{return void 0}}function sleepSync(ms){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,ms)}function pidAlive(pid){try{process.kill(pid,0);return true}catch(e){const code=e.code;if(code==="ESRCH")return false;if(code==="EPERM")return true;throw e}}function readOwner(path){let raw;try{raw=readFileSync(path,"utf8")}catch(e){const code=e.code;if(code==="ENOENT")return void 0;if(code==="EISDIR"||code==="EACCES"||code==="EPERM")throw new Error(`advisory lock path ${path} is unreadable (${code}) - remove the stray file or directory there and retry`);throw e}let ino;try{ino=statSync(path).ino}catch{}try{return{owner:JSON.parse(raw),ino}}catch{return{ino}}}function inspectLock(path){const inspection=inspectLockSnapshot(path);return inspection.state==="active"?{state:"active",owner:inspection.owner}:{state:inspection.state}}function inspectLockSnapshot(path){const found=readOwner(path);if(found===void 0)return{state:"absent"};const{owner,ino}=found;if(!owner||typeof owner.pid!=="number"||!Number.isInteger(owner.pid)||owner.pid<=0||typeof owner.nonce!=="string"){return{state:"stale",ino}}if(!pidAlive(owner.pid))return{state:"stale",ino};if(owner.start!==void 0){const fields=process.platform==="linux"?processStatFields(owner.pid):void 0;const now=process.platform==="linux"?fields?.[19]:processStartToken(owner.pid);if(now===owner.start&&fields?.[0]==="Z")return{state:"stale",ino};if(now!==void 0&&now!==owner.start)return{state:"stale",ino}}return{state:"active",owner,ino}}function publishAtomic(path,content){const tmp=`${path}.${randomBytes(8).toString("hex")}.tmp`;writeFileSync(tmp,content,{mode:384,flag:"wx"});try{linkSync(tmp,path);return true}catch(e){if(e.code==="EEXIST")return false;throw e}finally{try{unlinkSync(tmp)}catch{}}}function reclaimStale(path,inspectedIno){const sentinel=`${path}.reclaim`;const mine=JSON.stringify({pid:process.pid,start:processStartToken(process.pid),nonce:randomBytes(8).toString("hex"),ts:Date.now()});if(!publishAtomic(sentinel,mine)){const reclaimer=inspectLock(sentinel);if(reclaimer.state==="stale")throw new Error(`a lock reclaim was interrupted and its sentinel remains (${sentinel}) - if no cotal process is running, remove it and retry`);return reclaimer.state==="active"?reclaimer.owner:void 0}try{const inspection=inspectLockSnapshot(path);if(inspection.state!=="stale")return;if(inspectedIno!==void 0&&inspection.ino!==void 0&&inspection.ino!==inspectedIno)return;unlinkSync(path)}catch(e){if(e.code!=="ENOENT")throw e}finally{try{unlinkSync(sentinel)}catch{}}}function newAttempt(path,opts){mkdirSync(dirname(path),{recursive:true,mode:448});const nonce=randomBytes(12).toString("hex");const owner={pid:process.pid,start:processStartToken(process.pid),nonce,ts:Date.now(),...opts.label?{label:opts.label}:{}};return{path,nonce,content:`${JSON.stringify(owner)}
2
- `}}function tryAcquireOnce({path,nonce,content}){if(publishAtomic(path,content)){let released=false;return{kind:"acquired",held:{nonce,release(){if(released)return;released=true;const found=readOwner(path);if(found?.owner?.nonce===nonce){try{unlinkSync(path)}catch{}}}}}}const inspection=inspectLockSnapshot(path);if(inspection.state==="absent")return{kind:"retry"};if(inspection.state==="stale"){const reclaimer=reclaimStale(path,inspection.ino);return reclaimer?{kind:"reclaiming",owner:reclaimer}:{kind:"retry"}}return{kind:"held",owner:inspection.owner}}function reclaimPollMs(pollMs){return Math.min(pollMs,50)}function acquireLock(path,opts={}){const waitMs=opts.waitMs??3e5;const pollMs=opts.pollMs??200;const attempt=newAttempt(path,opts);const deadline=Date.now()+waitMs;for(;;){const result=tryAcquireOnce(attempt);if(result.kind==="acquired")return result.held;if(result.kind==="retry")continue;const left=deadline-Date.now();if(left<=0)throw lockTimeoutError(path,opts,result.owner,waitMs);sleepSync(Math.min(result.kind==="reclaiming"?reclaimPollMs(pollMs):pollMs,left))}}function lockTimeoutError(path,opts,owner,waitMs){return opts.onTimeout?.(owner)??new Error(`${opts.label??"a lock"} is held by a live process (pid ${owner.pid}) and did not release within ${Math.round(waitMs/1e3)}s - retry, or if it is wedged, remove ${path}`)}function opencodeLine(raw,bin){const m=raw.match(/(?:^|v)(\d+)\.\d+/);const major=m?Number(m[1]):NaN;if(major===1||major===2)return major;throw new Error(`opencode connector: unsupported OpenCode version "${raw}" from ${bin}; this connector supports the 1.x (opencode-ai) and 2.x (@opencode/cli) lines`)}function resolveOpencodeBin(){const override=process.env.COTAL_OPENCODE_BIN?.trim()||process.env.COTAL_OPENCODE_RESOLVED_BIN?.trim();if(override)return override;if(process.platform!=="win32")return"opencode";for(const dir of(process.env.PATH??"").split(delimiter)){if(!dir)continue;for(const exe of[join(dir,"opencode.exe"),join(dir,"node_modules","opencode-ai","bin","opencode.exe")]){if(existsSync(exe))return exe}}return"opencode"}var BIN=resolveOpencodeBin();var USERNAME="opencode";var SECRET=process.env.OPENCODE_SERVER_PASSWORD||randomBytes2(24).toString("hex");var RECORD_THEN_EXEC=`printf %s "$$" > "$1" && shift && exec "$@"`;async function freePort(){const srv=createServer();srv.listen(0,"127.0.0.1");await once(srv,"listening");const port=srv.address().port;await new Promise(r=>srv.close(()=>r()));return port}async function terminate(child){if(child.exitCode!==null||child.signalCode!==null)return;child.kill("SIGTERM");const dead=await Promise.race([once(child,"exit").then(()=>true),new Promise(r=>setTimeout(()=>r(false),3e3))]);if(!dead){child.kill("SIGKILL");await once(child,"exit")}}function processCommand(pid){try{if(process.platform==="win32"){return execFileSync2("powershell.exe",["-NoProfile","-NonInteractive","-Command",`(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}return execFileSync2("ps",["-p",String(pid),"-o","command="],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return void 0}}function pidAlive2(pid){try{process.kill(pid,0);return true}catch{return false}}function isLiveOpencodeServe(pid){if(!Number.isInteger(pid)||pid<=0||!pidAlive2(pid))return false;const cmd=processCommand(pid);if(!cmd)return pidAlive2(pid);return/\bserve\b/.test(cmd)&&(/(?:^|[\\/\s])opencode(?:\.exe)?(?:\s|$)/i.test(cmd)||/--hostname\s+127\.0\.0\.1\b/.test(cmd)&&/--port\s+\d+\b/.test(cmd))}async function launch(tuiArgv,serveEnv){const port=process.env.COTAL_OPENCODE_PORT?.trim()||String(await freePort());const url=`http://127.0.0.1:${port}`;const name=process.env.COTAL_NAME?.trim()||"agent";const dataRoot=process.env.COTAL_OPENCODE_HOME?.trim();if(!dataRoot)throw new Error("COTAL_OPENCODE_HOME is not set \u2014 the connector must pin the agent's data root");const agentHome=join(dataRoot,".cotal","opencode",name);const dbPath=join(agentHome,"opencode.db");const claim=acquireLock(join(agentHome,"launch.lock"),{waitMs:0,label:`agent "${name}"'s launch`,onTimeout:owner=>new Error(`agent "${name}" is already running (launcher pid ${owner.pid}) \u2014 kill it first`)});process.on("exit",()=>claim.release());let records=[];try{records=readdirSync(agentHome).filter(file=>/^serve(?:\.[0-9a-f]{32})?\.pid$/.test(file))}catch(e){if(e.code!=="ENOENT")throw e}for(const file of records){const record=join(agentHome,file);let recorded;try{try{recorded=readFileSync2(record,"utf8")}catch(e){if(e.code!=="EISDIR")throw e;rmdirSync(record)}}catch(e){if(e.code!=="ENOENT")throw e}if(recorded===void 0)continue;const pid=Number(recorded);if(isLiveOpencodeServe(pid))throw new Error(`agent "${name}" is already running (opencode serve pid ${pid}) \u2014 kill it first`);rmSync2(record,{force:true})}const versionRaw=execFileSync2(BIN,["--version"],{encoding:"utf8"}).trim();const line=opencodeLine(versionRaw,BIN);mkdirSync2(agentHome,{recursive:true});const pidFile=join(agentHome,`serve.${randomBytes2(16).toString("hex")}.pid`);const serveArgv=[BIN,"serve","--hostname","127.0.0.1","--port",port];const[serveCommand,...serveArgs]=process.platform==="win32"?serveArgv:["/bin/sh","-c",RECORD_THEN_EXEC,"cotal-serve",pidFile,...serveArgv];const serve=spawn(serveCommand,serveArgs,{env:{...process.env,...serveEnv?.(agentHome),COTAL_OPENCODE_SERVER_URL:url,COTAL_OPENCODE_LINE:String(line),OPENCODE_SERVER_USERNAME:USERNAME,OPENCODE_SERVER_PASSWORD:SECRET,OPENCODE_DB:dbPath},stdio:["ignore","pipe","pipe"]});if(process.platform==="win32")writeFileSync2(pidFile,String(serve.pid));serve.on("exit",()=>rmSync2(pidFile,{force:true}));let tui;for(const sig of["SIGINT","SIGTERM"])process.on(sig,()=>{void terminate(serve);if(tui)void terminate(tui)});let sessionId;let attached=false;let onSession;const scan=d=>{if(!attached)process.stderr.write(d);if(!sessionId){const m=d.toString().match(/\[cotal-session\] (\S+)/);if(m){sessionId=m[1];onSession?.(sessionId)}}};serve.stdout?.on("data",scan);serve.stderr?.on("data",scan);let bootFailed=false;serve.on("exit",(code,signal)=>{if(!attached)process.exit(bootFailed?1:code??(signal?1:0))});const auth=`Basic ${Buffer.from(`${USERNAME}:${SECRET}`).toString("base64")}`;const pokePath=line===2?"/api/config":"/session";void(async()=>{for(let i=0;i<300&&!sessionId;i++){try{await fetch(`${url}${pokePath}`,{headers:{authorization:auth},signal:AbortSignal.timeout(1500)})}catch{}await new Promise(r=>setTimeout(r,200))}})();const id=await new Promise(resolve=>{if(sessionId)return resolve(sessionId);onSession=resolve;setTimeout(()=>resolve(sessionId),6e4)});if(!id){process.stderr.write(`[cotal-connector] serve: agent session never came up (~60s) \u2014 aborting. Check the boot log above for plugin/mesh errors (OPENCODE_DB=${dbPath})
1
+ import{execFileSync as execFileSync2,spawn}from"node:child_process";import{createServer}from"node:net";import{once}from"node:events";import{randomBytes as randomBytes2}from"node:crypto";import{existsSync,mkdirSync as mkdirSync3,readdirSync,readFileSync as readFileSync2,rmdirSync,rmSync as rmSync2,writeFileSync as writeFileSync2}from"node:fs";import{delimiter,join}from"node:path";import{execFileSync}from"node:child_process";import{linkSync as linkSync2,mkdirSync as mkdirSync2,readFileSync,rmSync,statSync,unlinkSync as unlinkSync2}from"node:fs";import{randomBytes}from"node:crypto";import{dirname}from"node:path";import{chmodSync,closeSync,constants,fchmodSync,fstatSync,ftruncateSync,linkSync,lstatSync,mkdirSync,openSync,renameSync,unlinkSync,writeFileSync}from"node:fs";var isWin=process.platform==="win32";function notPrivate(path,why){const refused=new Error(`refusing to write "${path}": it ${why}; it was left unchanged`);refused.code="EPERM";return refused}function openPrivate(path,flags){if(isWin){const fd2=openSync(path,flags,384);try{fchmodSync(fd2,384)}catch(e){closeSync(fd2);throw e}return fd2}let fd;try{fd=openSync(path,flags|constants.O_NOFOLLOW|constants.O_NONBLOCK,384)}catch(e){if(e.code==="ELOOP")throw notPrivate(path,"is a symlink");throw e}try{const info=fstatSync(fd);if(!info.isFile())throw notPrivate(path,"is not a regular file");if(info.uid!==process.getuid())throw notPrivate(path,`is owned by uid ${info.uid}, not this user`);fchmodSync(fd,384);return fd}catch(e){closeSync(fd);throw e}}function createPrivateRecord(path,data){writeBytes(path,data,true)}function writeBytes(path,data,exclusive){const fd=openPrivate(path,constants.O_WRONLY|constants.O_CREAT|(exclusive?constants.O_EXCL:0));try{try{ftruncateSync(fd);writeFileSync(fd,data)}finally{closeSync(fd)}}catch(e){if(exclusive){try{unlinkSync(path)}catch{}}throw e}}function processStatFields(pid){try{const stat=readFileSync(`/proc/${pid}/stat`,"utf8");const after=stat.lastIndexOf(") ");if(after<0||stat.slice(0,stat.indexOf(" "))!==String(pid))return void 0;return stat.slice(after+2).trim().split(/\s+/)}catch{return void 0}}function processStartToken(pid){if(process.platform==="win32")return void 0;if(process.platform==="linux")return processStatFields(pid)?.[19];try{const out=execFileSync("ps",["-o","lstart=","-p",String(pid)],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim();return out||void 0}catch{return void 0}}function sleepSync(ms){Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,ms)}function pidAlive(pid){try{process.kill(pid,0);return true}catch(e){const code=e.code;if(code==="ESRCH")return false;if(code==="EPERM")return true;throw e}}function readOwner(path){let raw;try{raw=readFileSync(path,"utf8")}catch(e){const code=e.code;if(code==="ENOENT")return void 0;if(code==="EISDIR"||code==="EACCES"||code==="EPERM")throw new Error(`advisory lock path ${path} is unreadable (${code}) - remove the stray file or directory there and retry`);throw e}let ino;try{ino=statSync(path).ino}catch{}try{return{owner:JSON.parse(raw),ino}}catch{return{ino}}}function inspectLock(path){const inspection=inspectLockSnapshot(path);return inspection.state==="active"?{state:"active",owner:inspection.owner}:{state:inspection.state}}function inspectLockSnapshot(path){const found=readOwner(path);if(found===void 0)return{state:"absent"};const{owner,ino}=found;if(!owner||typeof owner.pid!=="number"||!Number.isInteger(owner.pid)||owner.pid<=0||typeof owner.nonce!=="string"){return{state:"stale",ino}}if(!pidAlive(owner.pid))return{state:"stale",ino};if(owner.start!==void 0){const fields=process.platform==="linux"?processStatFields(owner.pid):void 0;const now=process.platform==="linux"?fields?.[19]:processStartToken(owner.pid);if(now===owner.start&&fields?.[0]==="Z")return{state:"stale",ino};if(now!==void 0&&now!==owner.start)return{state:"stale",ino}}return{state:"active",owner,ino}}function publishAtomic(path,content){const tmp=`${path}.${randomBytes(8).toString("hex")}.tmp`;createPrivateRecord(tmp,content);try{linkSync2(tmp,path);return true}catch(e){if(e.code==="EEXIST")return false;throw e}finally{try{unlinkSync2(tmp)}catch{}}}function reclaimStale(path,inspectedIno){const sentinel=`${path}.reclaim`;const mine=JSON.stringify({pid:process.pid,start:processStartToken(process.pid),nonce:randomBytes(8).toString("hex"),ts:Date.now()});if(!publishAtomic(sentinel,mine)){const reclaimer=inspectLock(sentinel);if(reclaimer.state==="stale")throw new Error(`a lock reclaim was interrupted and its sentinel remains (${sentinel}) - if no cotal process is running, remove it and retry`);return reclaimer.state==="active"?reclaimer.owner:void 0}try{const inspection=inspectLockSnapshot(path);if(inspection.state!=="stale")return;if(inspectedIno!==void 0&&inspection.ino!==void 0&&inspection.ino!==inspectedIno)return;unlinkSync2(path)}catch(e){if(e.code!=="ENOENT")throw e}finally{try{unlinkSync2(sentinel)}catch{}}}function newAttempt(path,opts){mkdirSync2(dirname(path),{recursive:true,mode:448});const nonce=randomBytes(12).toString("hex");const owner={pid:process.pid,start:processStartToken(process.pid),nonce,ts:Date.now(),...opts.label?{label:opts.label}:{}};return{path,nonce,content:`${JSON.stringify(owner)}
2
+ `}}function tryAcquireOnce({path,nonce,content}){if(publishAtomic(path,content)){let released=false;return{kind:"acquired",held:{nonce,release(){if(released)return;released=true;const found=readOwner(path);if(found?.owner?.nonce===nonce){try{unlinkSync2(path)}catch{}}}}}}const inspection=inspectLockSnapshot(path);if(inspection.state==="absent")return{kind:"retry"};if(inspection.state==="stale"){const reclaimer=reclaimStale(path,inspection.ino);return reclaimer?{kind:"reclaiming",owner:reclaimer}:{kind:"retry"}}return{kind:"held",owner:inspection.owner}}function reclaimPollMs(pollMs){return Math.min(pollMs,50)}function acquireLock(path,opts={}){const waitMs=opts.waitMs??3e5;const pollMs=opts.pollMs??200;const attempt=newAttempt(path,opts);const deadline=Date.now()+waitMs;for(;;){const result=tryAcquireOnce(attempt);if(result.kind==="acquired")return result.held;if(result.kind==="retry")continue;const left=deadline-Date.now();if(left<=0)throw lockTimeoutError(path,opts,result.owner,waitMs);sleepSync(Math.min(result.kind==="reclaiming"?reclaimPollMs(pollMs):pollMs,left))}}function lockTimeoutError(path,opts,owner,waitMs){return opts.onTimeout?.(owner)??new Error(`${opts.label??"a lock"} is held by a live process (pid ${owner.pid}) and did not release within ${Math.round(waitMs/1e3)}s - retry, or if it is wedged, remove ${path}`)}function opencodeLine(raw,bin){const m=raw.match(/(?:^|v)(\d+)\.\d+/);const major=m?Number(m[1]):NaN;if(major===1||major===2)return major;throw new Error(`opencode connector: unsupported OpenCode version "${raw}" from ${bin}; this connector supports the 1.x (opencode-ai) and 2.x (@opencode/cli) lines`)}function resolveOpencodeBin(){const override=process.env.COTAL_OPENCODE_BIN?.trim()||process.env.COTAL_OPENCODE_RESOLVED_BIN?.trim();if(override)return override;if(process.platform!=="win32")return"opencode";for(const dir of(process.env.PATH??"").split(delimiter)){if(!dir)continue;for(const exe of[join(dir,"opencode.exe"),join(dir,"node_modules","opencode-ai","bin","opencode.exe")]){if(existsSync(exe))return exe}}return"opencode"}var BIN=resolveOpencodeBin();var USERNAME="opencode";var SECRET=process.env.OPENCODE_SERVER_PASSWORD||randomBytes2(24).toString("hex");var RECORD_THEN_EXEC=`printf %s "$$" > "$1" && shift && exec "$@"`;async function freePort(){const srv=createServer();srv.listen(0,"127.0.0.1");await once(srv,"listening");const port=srv.address().port;await new Promise(r=>srv.close(()=>r()));return port}async function terminate(child){if(child.exitCode!==null||child.signalCode!==null)return;child.kill("SIGTERM");const dead=await Promise.race([once(child,"exit").then(()=>true),new Promise(r=>setTimeout(()=>r(false),3e3))]);if(!dead){child.kill("SIGKILL");await once(child,"exit")}}function processCommand(pid){try{if(process.platform==="win32"){return execFileSync2("powershell.exe",["-NoProfile","-NonInteractive","-Command",`(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}return execFileSync2("ps",["-p",String(pid),"-o","command="],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return void 0}}function pidAlive2(pid){try{process.kill(pid,0);return true}catch{return false}}function isLiveOpencodeServe(pid){if(!Number.isInteger(pid)||pid<=0||!pidAlive2(pid))return false;const cmd=processCommand(pid);if(!cmd)return pidAlive2(pid);return/\bserve\b/.test(cmd)&&(/(?:^|[\\/\s])opencode(?:\.exe)?(?:\s|$)/i.test(cmd)||/--hostname\s+127\.0\.0\.1\b/.test(cmd)&&/--port\s+\d+\b/.test(cmd))}async function launch(tuiArgv,serveEnv){const port=process.env.COTAL_OPENCODE_PORT?.trim()||String(await freePort());const url=`http://127.0.0.1:${port}`;const name=process.env.COTAL_NAME?.trim()||"agent";const dataRoot=process.env.COTAL_OPENCODE_HOME?.trim();if(!dataRoot)throw new Error("COTAL_OPENCODE_HOME is not set \u2014 the connector must pin the agent's data root");const agentHome=join(dataRoot,".cotal","opencode",name);const dbPath=join(agentHome,"opencode.db");const claim=acquireLock(join(agentHome,"launch.lock"),{waitMs:0,label:`agent "${name}"'s launch`,onTimeout:owner=>new Error(`agent "${name}" is already running (launcher pid ${owner.pid}) \u2014 kill it first`)});process.on("exit",()=>claim.release());let records=[];try{records=readdirSync(agentHome).filter(file=>/^serve(?:\.[0-9a-f]{32})?\.pid$/.test(file))}catch(e){if(e.code!=="ENOENT")throw e}for(const file of records){const record=join(agentHome,file);let recorded;try{try{recorded=readFileSync2(record,"utf8")}catch(e){if(e.code!=="EISDIR")throw e;rmdirSync(record)}}catch(e){if(e.code!=="ENOENT")throw e}if(recorded===void 0)continue;const pid=Number(recorded);if(isLiveOpencodeServe(pid))throw new Error(`agent "${name}" is already running (opencode serve pid ${pid}) \u2014 kill it first`);rmSync2(record,{force:true})}const versionRaw=execFileSync2(BIN,["--version"],{encoding:"utf8"}).trim();const line=opencodeLine(versionRaw,BIN);mkdirSync3(agentHome,{recursive:true});const pidFile=join(agentHome,`serve.${randomBytes2(16).toString("hex")}.pid`);const serveArgv=[BIN,"serve","--hostname","127.0.0.1","--port",port];const[serveCommand,...serveArgs]=process.platform==="win32"?serveArgv:["/bin/sh","-c",RECORD_THEN_EXEC,"cotal-serve",pidFile,...serveArgv];const serve=spawn(serveCommand,serveArgs,{env:{...process.env,...serveEnv?.(agentHome),COTAL_OPENCODE_SERVER_URL:url,COTAL_OPENCODE_LINE:String(line),OPENCODE_SERVER_USERNAME:USERNAME,OPENCODE_SERVER_PASSWORD:SECRET,OPENCODE_DB:dbPath},stdio:["ignore","pipe","pipe"]});if(process.platform==="win32")writeFileSync2(pidFile,String(serve.pid));serve.on("exit",()=>rmSync2(pidFile,{force:true}));let tui;for(const sig of["SIGINT","SIGTERM"])process.on(sig,()=>{void terminate(serve);if(tui)void terminate(tui)});let sessionId;let attached=false;let onSession;const scan=d=>{if(!attached)process.stderr.write(d);if(!sessionId){const m=d.toString().match(/\[cotal-session\] (\S+)/);if(m){sessionId=m[1];onSession?.(sessionId)}}};serve.stdout?.on("data",scan);serve.stderr?.on("data",scan);let bootFailed=false;serve.on("exit",(code,signal)=>{if(!attached)process.exit(bootFailed?1:code??(signal?1:0))});const auth=`Basic ${Buffer.from(`${USERNAME}:${SECRET}`).toString("base64")}`;const pokePath=line===2?"/api/config":"/session";void(async()=>{for(let i=0;i<300&&!sessionId;i++){try{await fetch(`${url}${pokePath}`,{headers:{authorization:auth},signal:AbortSignal.timeout(1500)})}catch{}await new Promise(r=>setTimeout(r,200))}})();const id=await new Promise(resolve=>{if(sessionId)return resolve(sessionId);onSession=resolve;setTimeout(()=>resolve(sessionId),6e4)});if(!id){process.stderr.write(`[cotal-connector] serve: agent session never came up (~60s) \u2014 aborting. Check the boot log above for plugin/mesh errors (OPENCODE_DB=${dbPath})
3
3
  `);bootFailed=true;await terminate(serve);process.exit(1)}if(process.env.COTAL_SERVE_HEADLESS?.trim()==="1"){process.stdout.write(`[cotal-serve] ${JSON.stringify({port:Number(port),session:id})}
4
4
  `);return}const tuiEnv={...process.env,OPENCODE_SERVER_USERNAME:USERNAME,OPENCODE_SERVER_PASSWORD:SECRET,OPENCODE_DB:dbPath};delete tuiEnv.OPENCODE_CONFIG_CONTENT;for(const k of Object.keys(tuiEnv))if(k.startsWith("COTAL_"))delete tuiEnv[k];attached=true;const[command,...args]=tuiArgv({url,session:id,line});tui=spawn(command,args,{env:tuiEnv,stdio:"inherit"});serve.on("exit",()=>void terminate(tui));tui.on("exit",(code,signal)=>{void terminate(serve).then(()=>process.exit(code??(signal?1:0)))})}void launch(({url,session,line})=>line===2?[BIN,"--server",url,"--session",session]:[BIN,"attach",url,"--session",session]);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cotal-ai/connector-opencode",
3
3
  "description": "Cotal connector for OpenCode: a native in-process plugin that joins a session to the mesh.",
4
- "version": "0.151.3",
4
+ "version": "0.153.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -21,8 +21,8 @@
21
21
  "@cotal-ai/core": ">=0.1.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@cotal-ai/connector-core": "0.151.3",
25
- "@cotal-ai/core": "0.151.3",
24
+ "@cotal-ai/connector-core": "0.153.1",
25
+ "@cotal-ai/core": "0.153.1",
26
26
  "@cotal-ai/smoke-kit": "0.0.0",
27
27
  "@nats-io/transport-node": "^3.4.0",
28
28
  "@opencode-ai/plugin": "^1.16.2",