omnish 1.5.0 → 1.5.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/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- var Jh=Object.defineProperty;var Ut=(e,t)=>()=>(e&&(t=e(e=0)),t);var zh=(e,t)=>{for(var n in t)Jh(e,n,{get:t[n],enumerable:!0})};import Kh from"node:crypto";import mi from"node:fs";import Yh from"node:os";import re from"node:path";function Qh(){let e=process.env.OMNISH_HOME?.trim();if(e)return re.resolve(e);let t=Yh.homedir(),n=re.join(t,".omnish"),o=re.join(t,".whatslive");try{if(mi.existsSync(n))return n;if(mi.existsSync(o))return o}catch{}return n}function yo(e){let t=Kh.createHash("sha1").update(e,"utf8").digest("hex").slice(0,8);return re.join(Ql,t)}function B(e){mi.mkdirSync(e,{recursive:!0,mode:448})}function se(){B(W),B(le),B(it),B(Ql),B(Vl),B(ft),B(Rt),B(Pn),B(gi)}var W,le,it,Ql,Vl,Ue,de,Mn,fo,ht,rr,sr,_,ir,ar,lr,ft,hi,fi,Xl,Rt,go,cr,Pn,at,gi,G=Ut(()=>{"use strict";W=Qh(),le=re.join(W,"auth"),it=re.join(W,"jobs"),Ql=re.join(W,"apps"),Vl=re.join(W,"logs"),Ue=re.join(Vl,"gateway.log"),de=re.join(W,"gateway.pid"),Mn=re.join(W,"gateway-control.json"),fo=re.join(W,"config-ui.json"),ht=re.join(W,"ui.json"),rr=re.join(W,"tunnel-auth.json"),sr=re.join(W,"ui-server.json"),_=re.join(W,"config.json"),ir=re.join(W,"shortcuts.json"),ar=re.join(W,"recipes.json"),lr=re.join(W,"recipes-user.json"),ft=re.join(W,"cowork"),hi=re.join(ft,"tasks.json"),fi=re.join(ft,"pending-runs.json"),Xl=re.join(ft,"completions.sqlite"),Rt=re.join(W,"watch"),go=re.join(Rt,"rules.json"),cr=re.join(Rt,"events.sqlite"),Pn=re.join(W,"bin"),at=re.join(W,"venvs","whisper"),gi=re.join(W,"media","pull")});function oc(e){let t=e.trim();for(;;){let n=t;if(t=t.replace(/^whatsapp:/i,"").trim(),t===n)return t}}function Vh(e){let t=oc(e);if(!t.toLowerCase().endsWith("@g.us"))return!1;let o=t.slice(0,t.length-5);return!o||o.includes("@")?!1:/^[0-9]+(-[0-9]+)*$/.test(o)}function Xh(e){let t=e.match(ec);if(t)return t[1]??null;let n=e.match(tc);if(n)return n[1]??null;let o=e.match(nc);return o?o[1]??null:null}function Zl(e){let t=e.replace(/\D/g,"");return t?`+${t}`:""}function Bt(e){return`${e.replace(/\D/g,"")}@s.whatsapp.net`}function te(e){let t=oc(e);if(!t||Vh(t))return null;if(ec.test(t)||tc.test(t)||nc.test(t)){let o=Xh(t);if(!o)return null;let r=Zl(o);return r.length>1?r:null}if(t.includes("@"))return null;let n=Zl(t);return n.length>1?n:null}function ur(e){return e.map(t=>String(t).trim()).filter(t=>!!t).map(t=>t==="*"?t:te(t)).filter(t=>!!t)}function dr(e){let t=new Set;for(let n of e){if(n==="*")continue;let o=te(String(n));o&&t.add(o)}return t}function Ne(e){let t=e.trim();for(;;){let n=t.toLowerCase();if(n.startsWith("tg:")){t=t.slice(3).trimStart();continue}if(n.startsWith("telegram:")){t=t.slice(9).trimStart();continue}break}return/^\d+$/.test(t)?t:null}function pr(e){let t=new Set;for(let n of e){let o=Ne(String(n));o&&t.add(o)}return t}function yi(e){let t=e.trim();if(!t)return null;let n=t.toLowerCase();if(n.startsWith("tg:")||n.startsWith("telegram:")){let r=Ne(t);return r?{kind:"tg",id:r}:null}let o=te(t);return o?{kind:"wa",normalized:o}:null}function rc(e,t){let n=te(t);return n?e.has(n):!1}var ec,tc,nc,Xe=Ut(()=>{"use strict";ec=/^(\d+)(?::\d+)?@s\.whatsapp\.net$/i,tc=/^(\d+)@c\.us$/i,nc=/^(\d+)@lid$/i});import mr from"node:fs";function Zh(e){return typeof e=="string"&&new Set(["video","audio","subs","transcript","all"]).has(e)?e:I.pullDefaultMode}function ef(e){return e==="downloads"||e==="omnishData"||e==="sessionCwd"||e==="processCwd"||e==="fixed"?e:I.fileReceiveRootMode}function tf(e){return e==="primary"?"primary":"secondary"}function nf(e){if(!e||typeof e!="object")return{};let t={};for(let[n,o]of Object.entries(e)){if(typeof o!="string")continue;let r=o.trim();if(!r)continue;let s=n.trim(),i=s.toLowerCase();if(i.startsWith("wa:")){let l=te(s.slice(3));l&&(t[`wa:${l}`]=r.slice(0,64));continue}if(i.startsWith("tg:")||i.startsWith("telegram:")){let l=Ne(s);l&&(t[`tg:${l}`]=r.slice(0,64));continue}let a=te(s);a&&(t[`wa:${a}`]=r.slice(0,64))}return t}function wo(e){let t=typeof e.appsFlushMs=="number"&&e.appsFlushMs>=0?e.appsFlushMs:I.appsFlushMs,n=e.gatewayMode==="telegram"||e.gatewayMode==="both"||e.gatewayMode==="whatsapp"?e.gatewayMode:I.gatewayMode,o=typeof e.telegramBotToken=="string"?e.telegramBotToken:I.telegramBotToken,r=Array.isArray(e.telegramAllowFrom)?[...new Set(e.telegramAllowFrom.map(s=>Ne(String(s))).filter(s=>!!s))].sort():I.telegramAllowFrom;return{...I,...e,gatewayMode:n,telegramBotToken:o,telegramAllowFrom:r,allowFrom:Array.isArray(e.allowFrom)?ur(e.allowFrom.map(String)).filter(s=>s!=="*"):I.allowFrom,commandPrefix:(()=>{let s=typeof e.commandPrefix=="string"&&e.commandPrefix.length>0?e.commandPrefix:I.commandPrefix;return s==="! "?"!":s})(),syncTimeoutMs:typeof e.syncTimeoutMs=="number"&&e.syncTimeoutMs>0?e.syncTimeoutMs:I.syncTimeoutMs,syncMaxBytes:typeof e.syncMaxBytes=="number"&&e.syncMaxBytes>0?e.syncMaxBytes:I.syncMaxBytes,jobLogTailLines:typeof e.jobLogTailLines=="number"&&e.jobLogTailLines>0?e.jobLogTailLines:I.jobLogTailLines,shell:typeof e.shell=="string"&&e.shell.length>0?e.shell:I.shell,appsCols:typeof e.appsCols=="number"&&e.appsCols>0&&e.appsCols<=500?Math.floor(e.appsCols):I.appsCols,appsRows:typeof e.appsRows=="number"&&e.appsRows>0&&e.appsRows<=200?Math.floor(e.appsRows):I.appsRows,appsFlushMs:t,appsMinIntervalMs:typeof e.appsMinIntervalMs=="number"&&e.appsMinIntervalMs>=0?e.appsMinIntervalMs:I.appsMinIntervalMs,appsMaxFlushBytes:typeof e.appsMaxFlushBytes=="number"&&e.appsMaxFlushBytes>256?Math.floor(e.appsMaxFlushBytes):I.appsMaxFlushBytes,appsMaxSessions:typeof e.appsMaxSessions=="number"&&e.appsMaxSessions>0?Math.min(50,Math.floor(e.appsMaxSessions)):I.appsMaxSessions,appsMaxSessionsTotal:typeof e.appsMaxSessionsTotal=="number"&&e.appsMaxSessionsTotal>0?Math.min(200,Math.floor(e.appsMaxSessionsTotal)):I.appsMaxSessionsTotal,appsMaxWaChars:typeof e.appsMaxWaChars=="number"&&e.appsMaxWaChars>256?Math.floor(e.appsMaxWaChars):I.appsMaxWaChars,appsLogTailLines:typeof e.appsLogTailLines=="number"&&e.appsLogTailLines>0?Math.min(500,Math.floor(e.appsLogTailLines)):I.appsLogTailLines,appsSubmitDelayMs:typeof e.appsSubmitDelayMs=="number"&&e.appsSubmitDelayMs>=0?Math.min(500,Math.floor(e.appsSubmitDelayMs)):I.appsSubmitDelayMs,appsClearInput:typeof e.appsClearInput=="boolean"?e.appsClearInput:I.appsClearInput,appsClearInputDelayMs:typeof e.appsClearInputDelayMs=="number"&&e.appsClearInputDelayMs>=0?Math.min(200,Math.floor(e.appsClearInputDelayMs)):I.appsClearInputDelayMs,appsClearInputSequence:typeof e.appsClearInputSequence=="string"&&e.appsClearInputSequence.length>0?e.appsClearInputSequence.slice(0,200):I.appsClearInputSequence,appsSkipClearOnPasswordPrompt:typeof e.appsSkipClearOnPasswordPrompt=="boolean"?e.appsSkipClearOnPasswordPrompt:I.appsSkipClearOnPasswordPrompt,appsPasswordPromptHint:typeof e.appsPasswordPromptHint=="boolean"?e.appsPasswordPromptHint:I.appsPasswordPromptHint,fileSendMaxBytes:typeof e.fileSendMaxBytes=="number"&&e.fileSendMaxBytes>=0?e.fileSendMaxBytes===0?0:Math.min(2e9,Math.floor(e.fileSendMaxBytes)):I.fileSendMaxBytes,fileReceiveMaxBytes:typeof e.fileReceiveMaxBytes=="number"&&e.fileReceiveMaxBytes>=0?e.fileReceiveMaxBytes===0?0:Math.min(2e9,Math.floor(e.fileReceiveMaxBytes)):I.fileReceiveMaxBytes,fileInboxSubdir:typeof e.fileInboxSubdir=="string"&&e.fileInboxSubdir.length>0&&e.fileInboxSubdir.replace(/[/\\]/g,"").slice(0,80)||I.fileInboxSubdir,fileReceiveRootMode:ef(e.fileReceiveRootMode),fileReceiveRootPath:typeof e.fileReceiveRootPath=="string"?e.fileReceiveRootPath.trim().slice(0,4096):I.fileReceiveRootPath,recipesAllowDangerousBuiltins:typeof e.recipesAllowDangerousBuiltins=="boolean"?e.recipesAllowDangerousBuiltins:I.recipesAllowDangerousBuiltins,recipesMaxTaskChars:typeof e.recipesMaxTaskChars=="number"&&e.recipesMaxTaskChars>=0?e.recipesMaxTaskChars===0?0:Math.min(Number.MAX_SAFE_INTEGER,Math.floor(e.recipesMaxTaskChars)):I.recipesMaxTaskChars,recipesMacroDefaultCommand:typeof e.recipesMacroDefaultCommand=="string"&&e.recipesMacroDefaultCommand.trim().length>0?e.recipesMacroDefaultCommand.trim().slice(0,4096):I.recipesMacroDefaultCommand,recipesRunAttach:typeof e.recipesRunAttach=="boolean"?e.recipesRunAttach:I.recipesRunAttach,clusterEnabled:typeof e.clusterEnabled=="boolean"?e.clusterEnabled:I.clusterEnabled,clusterLabel:typeof e.clusterLabel=="string"?e.clusterLabel.trim().slice(0,128):I.clusterLabel,clusterRole:tf(e.clusterRole),clusterSenderBindings:nf(e.clusterSenderBindings),serviceInstallFromChat:typeof e.serviceInstallFromChat=="boolean"?e.serviceInstallFromChat:I.serviceInstallFromChat,updateCheckEnabled:typeof e.updateCheckEnabled=="boolean"?e.updateCheckEnabled:I.updateCheckEnabled,updateCheckIntervalMs:typeof e.updateCheckIntervalMs=="number"&&e.updateCheckIntervalMs>0?Math.min(6048e5,Math.max(36e5,Math.floor(e.updateCheckIntervalMs))):I.updateCheckIntervalMs,updateCheckPackageName:typeof e.updateCheckPackageName=="string"&&e.updateCheckPackageName.trim().length>0?e.updateCheckPackageName.trim().slice(0,214):I.updateCheckPackageName,updateInfoUrl:typeof e.updateInfoUrl=="string"?e.updateInfoUrl.trim().slice(0,2048):I.updateInfoUrl,chatLlmFallbackEnabled:typeof e.chatLlmFallbackEnabled=="boolean"?e.chatLlmFallbackEnabled:I.chatLlmFallbackEnabled,chatLlmShellCommand:typeof e.chatLlmShellCommand=="string"?e.chatLlmShellCommand.trim().slice(0,8192):I.chatLlmShellCommand,chatLlmTimeoutMs:typeof e.chatLlmTimeoutMs=="number"&&e.chatLlmTimeoutMs>0?Math.min(9e5,Math.floor(e.chatLlmTimeoutMs)):I.chatLlmTimeoutMs,chatLlmMaxInputChars:typeof e.chatLlmMaxInputChars=="number"&&e.chatLlmMaxInputChars>0?Math.min(5e5,Math.floor(e.chatLlmMaxInputChars)):I.chatLlmMaxInputChars,chatLlmMaxOutputChars:typeof e.chatLlmMaxOutputChars=="number"&&e.chatLlmMaxOutputChars>0?Math.min(2e6,Math.floor(e.chatLlmMaxOutputChars)):I.chatLlmMaxOutputChars,chatLlmNeedsTty:typeof e.chatLlmNeedsTty=="boolean"?e.chatLlmNeedsTty:I.chatLlmNeedsTty,chatLlmWorkDir:typeof e.chatLlmWorkDir=="string"?e.chatLlmWorkDir.trim().slice(0,4096):I.chatLlmWorkDir,tunnelEnabled:typeof e.tunnelEnabled=="boolean"?e.tunnelEnabled:I.tunnelEnabled,tunnelRelayUrl:typeof e.tunnelRelayUrl=="string"&&e.tunnelRelayUrl.trim().length>0?e.tunnelRelayUrl.trim().slice(0,2048):I.tunnelRelayUrl,tunnelMaxActive:typeof e.tunnelMaxActive=="number"&&e.tunnelMaxActive>0?Math.min(50,Math.floor(e.tunnelMaxActive)):I.tunnelMaxActive,platformToken:typeof e.platformToken=="string"?e.platformToken.trim():I.platformToken,platformDeviceId:typeof e.platformDeviceId=="string"?e.platformDeviceId.trim().slice(0,128):I.platformDeviceId,webhookEnabled:typeof e.webhookEnabled=="boolean"?e.webhookEnabled:I.webhookEnabled,webhookPort:typeof e.webhookPort=="number"&&e.webhookPort>=0?Math.min(65535,Math.floor(e.webhookPort)):I.webhookPort,webhookHost:typeof e.webhookHost=="string"&&e.webhookHost.trim().length>0?e.webhookHost.trim():I.webhookHost,webhookToken:typeof e.webhookToken=="string"?e.webhookToken.trim():I.webhookToken,watchEnabled:typeof e.watchEnabled=="boolean"?e.watchEnabled:I.watchEnabled,watchDebounceMs:typeof e.watchDebounceMs=="number"&&Number.isFinite(e.watchDebounceMs)?Math.max(500,Math.min(6e4,Math.floor(e.watchDebounceMs))):I.watchDebounceMs,watchMaxEventsPerMinute:typeof e.watchMaxEventsPerMinute=="number"&&Number.isFinite(e.watchMaxEventsPerMinute)?Math.max(1,Math.min(120,Math.floor(e.watchMaxEventsPerMinute))):I.watchMaxEventsPerMinute,watchAutoRestore:typeof e.watchAutoRestore=="boolean"?e.watchAutoRestore:I.watchAutoRestore,pullEnabled:typeof e.pullEnabled=="boolean"?e.pullEnabled:I.pullEnabled,pullInstallFromChat:typeof e.pullInstallFromChat=="boolean"?e.pullInstallFromChat:I.pullInstallFromChat,pullUrlAutoDetect:typeof e.pullUrlAutoDetect=="boolean"?e.pullUrlAutoDetect:I.pullUrlAutoDetect,pullDefaultMode:Zh(e.pullDefaultMode),pullOutputDir:typeof e.pullOutputDir=="string"?e.pullOutputDir.trim().slice(0,4096):I.pullOutputDir,pullMaxBytes:typeof e.pullMaxBytes=="number"&&e.pullMaxBytes>=0?e.pullMaxBytes===0?0:Math.min(2e9,Math.floor(e.pullMaxBytes)):I.pullMaxBytes,pullAutoSend:typeof e.pullAutoSend=="boolean"?e.pullAutoSend:I.pullAutoSend,pullWhisperModel:typeof e.pullWhisperModel=="string"&&e.pullWhisperModel.trim().length>0?e.pullWhisperModel.trim().slice(0,64):I.pullWhisperModel,pullYtDlpPath:typeof e.pullYtDlpPath=="string"?e.pullYtDlpPath.trim().slice(0,4096):I.pullYtDlpPath,pullFfmpegPath:typeof e.pullFfmpegPath=="string"?e.pullFfmpegPath.trim().slice(0,4096):I.pullFfmpegPath,pullWhisperPath:typeof e.pullWhisperPath=="string"?e.pullWhisperPath.trim().slice(0,4096):I.pullWhisperPath}}function O(e){let t=S(),n=wo({...t,...e});return Be(n),n}function S(){if(se(),!mr.existsSync(_)){let e=wo({});return Be(e),e}try{let e=mr.readFileSync(_,"utf8"),t=JSON.parse(e);return wo(t)}catch{return wo({})}}function Be(e){se();let t=wo(e);mr.writeFileSync(_,JSON.stringify(t,null,2)+`
3
- `,{mode:384})}function hr(e){let t=yi(e);if(!t)throw new Error("Invalid entry. Use +E164 (WhatsApp) or tg:<user_id> (Telegram).");let n=S();if(t.kind==="wa"){if(t.normalized==="*")throw new Error("Wildcards are not allowed.");let o=new Set(n.allowFrom);o.add(t.normalized),n.allowFrom=[...o].sort()}else{let o=new Set(n.telegramAllowFrom);o.add(t.id),n.telegramAllowFrom=[...o].sort()}return Be(n),n}function fr(e){let t=yi(e);if(!t)throw new Error("Invalid entry. Use +E164 (WhatsApp) or tg:<user_id> (Telegram).");let n=S();return t.kind==="wa"?n.allowFrom=n.allowFrom.filter(o=>o!==t.normalized):n.telegramAllowFrom=n.telegramAllowFrom.filter(o=>Ne(o)!==t.id),Be(n),n}function Me(e){let t=typeof process.env.TELEGRAM_BOT_TOKEN=="string"?process.env.TELEGRAM_BOT_TOKEN.trim():"";return t||(e.telegramBotToken??"").trim()}function gt(e){let t=e.trim();return/^\d{6,}:[A-Za-z0-9_-]{20,}$/.test(t)}function Ht(e){let t=S();return t.telegramBotToken=e.trim(),Be(t),S()}function En(e){let t=e.trim().toLowerCase();return t==="whatsapp"||t==="wa"||t==="w"?"whatsapp":t==="telegram"||t==="tg"||t==="t"?"telegram":t==="both"||t==="all"||t==="b"?"both":null}function gr(e){let t=S();return t.gatewayMode=e,Be(t),S()}function yr(e){let t=S();return t.clusterEnabled=e,Be(t),S()}function lt(){try{return mr.readdirSync(le).length>0}catch{return!1}}var I,pe=Ut(()=>{"use strict";G();Xe();I={gatewayMode:"whatsapp",telegramBotToken:"",telegramAllowFrom:[],allowFrom:[],commandPrefix:"!",syncTimeoutMs:3e4,syncMaxBytes:32768,jobLogTailLines:80,shell:"/bin/bash",appsCols:120,appsRows:40,appsFlushMs:300,appsMinIntervalMs:800,appsMaxFlushBytes:8192,appsMaxSessions:5,appsMaxSessionsTotal:20,appsMaxWaChars:3500,appsLogTailLines:80,appsSubmitDelayMs:50,appsClearInput:!0,appsClearInputDelayMs:20,appsClearInputSequence:"^A,^K",appsSkipClearOnPasswordPrompt:!0,appsPasswordPromptHint:!0,fileSendMaxBytes:0,fileReceiveMaxBytes:0,fileInboxSubdir:"inbox",fileReceiveRootMode:"downloads",fileReceiveRootPath:"",recipesAllowDangerousBuiltins:!1,recipesMaxTaskChars:0,recipesMacroDefaultCommand:'claude -p "$OMNISH_TASK"',recipesRunAttach:!1,clusterEnabled:!1,clusterLabel:"",clusterRole:"secondary",clusterSenderBindings:{},serviceInstallFromChat:!1,updateCheckEnabled:!1,updateCheckIntervalMs:864e5,updateCheckPackageName:"omnish",updateInfoUrl:"",chatLlmFallbackEnabled:!1,chatLlmShellCommand:"",chatLlmTimeoutMs:12e4,chatLlmMaxInputChars:16e3,chatLlmMaxOutputChars:24e3,chatLlmNeedsTty:!1,chatLlmWorkDir:"",tunnelEnabled:!1,tunnelRelayUrl:"https://tunnel.omnish.dev",platformToken:"",platformDeviceId:"",tunnelMaxActive:5,webhookEnabled:!1,webhookPort:0,webhookHost:"127.0.0.1",webhookToken:"",watchEnabled:!1,watchDebounceMs:2e3,watchMaxEventsPerMinute:30,watchAutoRestore:!0,pullEnabled:!1,pullInstallFromChat:!1,pullUrlAutoDetect:!1,pullDefaultMode:"audio",pullOutputDir:"",pullMaxBytes:0,pullAutoSend:!1,pullWhisperModel:"small",pullYtDlpPath:"",pullFfmpegPath:"",pullWhisperPath:""}});import of from"pino";function An(){return process.env.OMNISH_VERBOSE==="1"||process.env.WHATSVERBOSE==="1"}function sc(){return An()?"info":"silent"}function wi(e){e?process.env.OMNISH_VERBOSE="1":delete process.env.OMNISH_VERBOSE,P.level=sc()}function ic(){return P.child({module:"baileys"})}var P,xe=Ut(()=>{"use strict";P=of({level:sc(),base:{app:"omnish"}})});import Ii from"node:fs";function Pt(){try{let e=Ii.readFileSync(rr,"utf8"),t=JSON.parse(e);return!t||typeof t.token!="string"||!t.token.trim()?null:{token:t.token.trim(),relayUrl:typeof t.relayUrl=="string"?t.relayUrl.trim():void 0}}catch{return null}}function bt(e){se(),Ii.writeFileSync(rr,JSON.stringify({token:e.token.trim(),...e.relayUrl?{relayUrl:e.relayUrl.trim()}:{}},null,2)+`
4
- `,{mode:384})}function Er(){try{Ii.unlinkSync(rr)}catch{}}function ng(){return process.env.OMNISH_TOKEN?.trim()||process.env.OMNISH_TUNNEL_TOKEN?.trim()||process.env.OMNISH_DEVICE_TOKEN?.trim()||""}function kt(){let e=ng();if(e)return e;let t=S().platformToken.trim();return t||(Pt()?.token??"")}function Li(e){let t=process.env.OMNISH_PLATFORM_URL?.trim()||process.env.OMNISH_COMM_LAYER_URL?.trim()||process.env.OMNISH_TUNNEL_RELAY?.trim();if(t)return t.replace(/\/$/,"");let n=S().tunnelRelayUrl.trim();if(n)return n.replace(/\/$/,"");let o=Pt()?.relayUrl?.trim();return o?o.replace(/\/$/,""):e.replace(/\/$/,"")}function ct(e){return Li(e)}var pn=Ut(()=>{"use strict";pe();G()});var Ee,mn=Ut(()=>{"use strict";Ee="https://tunnel.omnish.dev"});function on(e){return(e??"").trim()}function ob(){return on(process.env.OMNISH_PLATFORM_URL)||on(process.env.OMNISH_COMM_LAYER_URL)||on(process.env.OMNISH_TUNNEL_RELAY)}function rb(){return on(process.env.OMNISH_TOKEN)||on(process.env.OMNISH_DEVICE_TOKEN)||on(process.env.OMNISH_TUNNEL_TOKEN)}function ja(){return kt()}function so(){return rb()?"env":S().platformToken.trim()?"config":Pt()?.token?"file":"default"}function Ho(){let e=S();return Li(e.tunnelRelayUrl||Ee)}function io(){return ob()?"env":S().tunnelRelayUrl.trim()?"config":Pt()?.relayUrl?.trim()?"file":"default"}function Ga(){let e=on(process.env.OMNISH_DEVICE_ID);if(e)return e;let t=S().platformDeviceId.trim();if(t)return t}function qa(){return on(process.env.OMNISH_DEVICE_ID)?"env":S().platformDeviceId.trim()?"config":"default"}function ae(){let e=Ho(),t=ja();if(!e||!t)return null;let n=Ga();return{platformUrl:e.replace(/\/$/,""),token:t,deviceId:n}}var st=Ut(()=>{"use strict";pe();pn();mn()});var $p={};zh($p,{fetchPlatformAccount:()=>xn,getAttachedConfig:()=>Ya,getAttachedPlatformSnapshot:()=>ab,loadConfigForSendtoBroadcast:()=>Qa,mergeAttachedPlatformPolicy:()=>Ka,parsePlatformMeResponse:()=>Rp,setAttachedPlatformSnapshot:()=>Ja,setPlatformDefaultDevice:()=>Va,snapshotFromRegisteredAccount:()=>Tp,syncAttachedPlatformPolicy:()=>$s,updatePlatformAllowlists:()=>Ts});function za(e){let t=e.replace(/\/$/,"");return/^https?:\/\//i.test(t)?t:`http://${t}`}function sb(e){return e==="telegram"||e==="both"||e==="whatsapp"?e:"whatsapp"}function ib(e){if(!e||typeof e!="object")return{};let t={};for(let[n,o]of Object.entries(e)){if(!o||typeof o!="object")continue;let r=o,s=typeof r.status=="string"?r.status:"idle";t[n]={status:s,linked:r.linked===!0||s==="linked",...typeof r.tokenConfigured=="boolean"?{tokenConfigured:r.tokenConfigured}:{}}}return t}function Rp(e){let t=e.routing,n=t&&typeof t=="object"?t:null,o=Array.isArray(n?.onlineDeviceIds)?n.onlineDeviceIds.map(String):[],r=n?.defaultDeviceId!=null?String(n.defaultDeviceId):e.defaultDeviceId!=null?String(e.defaultDeviceId):null;return{allowFrom:Array.isArray(e.allowFrom)?e.allowFrom.map(String):[],telegramAllowFrom:Array.isArray(e.telegramAllowFrom)?e.telegramAllowFrom.map(String):[],gatewayMode:sb(e.gatewayMode),connectors:ib(e.connectors),defaultDeviceId:r,routing:{defaultDeviceId:r,onlineDeviceIds:o,onlineCount:typeof n?.onlineCount=="number"?n.onlineCount:o.length}}}function Tp(e,t=[],n=[]){let o=e.defaultDeviceId??null;return{allowFrom:t,telegramAllowFrom:n,gatewayMode:e.gatewayMode,connectors:e.connectors,defaultDeviceId:o,routing:{defaultDeviceId:o,onlineDeviceIds:[],onlineCount:0}}}function Ka(e,t){return{...e,allowFrom:[...t.allowFrom],telegramAllowFrom:[...t.telegramAllowFrom],gatewayMode:t.gatewayMode}}function Ja(e){Rs=e}function ab(){return Rs}function Ya(){let e=S();return Rs?Ka(e,Rs):e}async function Qa(){let e=S(),t=ae();if(!t)return e;try{return Ka(e,await xn(t))}catch(n){return P.warn({err:String(n)},"sendto: could not fetch platform allowlists; using local config.json"),e}}async function xn(e){let t=await fetch(`${za(e.platformUrl)}/v1/me`,{headers:{Authorization:`Bearer ${e.token}`}});if(!t.ok)throw new Error(`Platform GET /v1/me failed: HTTP ${t.status}`);let n=await t.json();return Rp(n)}async function Ts(e,t){let n=await fetch(`${za(e.platformUrl)}/v1/me/allowlists`,{method:"PUT",headers:{"content-type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify(t)}),o=await n.json().catch(()=>({}));if(!n.ok)throw new Error(o.error||`Platform PUT /v1/me/allowlists failed: HTTP ${n.status}`);return{allowFrom:Array.isArray(o.allowFrom)?o.allowFrom.map(String):[],telegramAllowFrom:Array.isArray(o.telegramAllowFrom)?o.telegramAllowFrom.map(String):[]}}async function Va(e,t){let n=await fetch(`${za(e.platformUrl)}/v1/me/default-device`,{method:"PUT",headers:{"content-type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify({deviceId:t})});if(!n.ok){let o=await n.json().catch(()=>({}));throw new Error(o.error||`Platform PUT /v1/me/default-device failed: HTTP ${n.status}`)}}async function $s(e,t){try{let n=await xn(e);return Ja(n),P.info({gatewayMode:n.gatewayMode,waLinked:n.connectors.whatsapp?.linked===!0,tgLinked:n.connectors.telegram?.linked===!0,allowFromCount:n.allowFrom.length,telegramAllowFromCount:n.telegramAllowFrom.length},"attached platform policy loaded"),n}catch(n){if(t){let o=Tp(t);return Ja(o),P.warn({err:String(n)},"platform GET /v1/me failed; using register ack for gatewayMode/connectors only (allowlists from local config until /v1/me works)"),o}return P.warn({err:String(n)},"platform GET /v1/me failed; attached inbound uses local config.json allowlists"),null}}var Rs,jo=Ut(()=>{"use strict";pe();xe();st();Rs=null});pe();import Fv from"node:dns";import _v from"node:crypto";import cn from"node:fs";import Kl from"node:path";import Dh from"node:os";pe();xe();import Gf from"node:os";import qf from"node:fs";G();import ac from"node:crypto";import bo from"node:fs";import bi from"node:os";import In from"node:path";var rf=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/;function vi(e){let t=e.trim().toLowerCase();return rf.test(t)?{ok:!0,name:t}:{ok:!1,error:"Name must be alphanumeric with _ or -, max 32 chars."}}function Qe(e,t){let n=e.trim();return n===""||n==="."?In.resolve(t):n==="~"?bi.homedir():n.startsWith("~/")||n.startsWith("~\\")?In.join(bi.homedir(),n.slice(2)):In.isAbsolute(n)?n:In.resolve(t,n)}function wr(e){return In.join(bi.homedir(),"Cowork",e)}function sf(e){if(!e||typeof e!="object")return null;let t=e,n=typeof t.id=="string"&&t.id.length>=4?t.id.slice(0,32):"",o=typeof t.name=="string"?t.name.trim().toLowerCase():"",r=typeof t.ownerPeerKey=="string"?t.ownerPeerKey:"",s=typeof t.command=="string"?t.command:"";if(!n||!o||!r||!s)return null;let i=typeof t.cwd=="string"?t.cwd:"",a=typeof t.outputDir=="string"&&t.outputDir.trim()?t.outputDir:wr(o),l=typeof t.enabled=="boolean"?t.enabled:!0,d=typeof t.notify=="string"?t.notify.toLowerCase():"self",u=d==="wa"||d==="whatsapp"?"wa":d==="tg"||d==="telegram"?"tg":d==="all"?"all":d==="none"?"none":"self",c={kind:"ondemand"};if(t.schedule&&typeof t.schedule=="object"){let k=t.schedule,M=typeof k.kind=="string"?k.kind:"";if(M==="ondemand")c={kind:"ondemand"};else if(M==="daily"){let R=Number(k.hour),L=Number(k.minute);Number.isFinite(R)&&Number.isFinite(L)&&(c={kind:"daily",hour:R,minute:L})}else if(M==="weekdays"){let R=Number(k.hour),L=Number(k.minute);Number.isFinite(R)&&Number.isFinite(L)&&(c={kind:"weekdays",hour:R,minute:L})}else if(M==="hourly"){let R=Number(k.minute);Number.isFinite(R)&&Number.isInteger(R)&&R>=0&&R<=59&&(c={kind:"hourly",minute:R})}else if(M==="weekly"){let R=Number(k.hour),L=Number(k.minute),$=Number(k.weekday);Number.isFinite(R)&&Number.isFinite(L)&&Number.isInteger($)&&(c={kind:"weekly",weekday:$,hour:R,minute:L})}else if(M==="heartbeat"){let R=Number(k.intervalMs),L=Number(k.graceMs);Number.isFinite(R)&&R>0&&Number.isFinite(L)&&L>0&&(c={kind:"heartbeat",intervalMs:R,graceMs:L})}}let m=null;typeof t.lastCompletedSlotMs=="number"&&Number.isFinite(t.lastCompletedSlotMs)&&(m=t.lastCompletedSlotMs);let h=typeof t.createdAtMs=="number"&&Number.isFinite(t.createdAtMs)?t.createdAtMs:Date.now(),f=typeof t.attachLog=="boolean"?t.attachLog:!1,g=Array.isArray(t.attachFiles)?t.attachFiles.filter(k=>typeof k=="string"&&k.trim().length>0):[],y=typeof t.notifyWhen=="string"?t.notifyWhen.toLowerCase():"always";return{id:n,name:o,ownerPeerKey:r,command:s,cwd:i,outputDir:a,schedule:c,enabled:l,notify:u,notifyWhen:y==="failure"?"failure":y==="state-change"?"state-change":"always",attachLog:f,attachFiles:g,lastCompletedSlotMs:m,createdAtMs:h}}function Pe(){try{let e=bo.readFileSync(hi,"utf8"),t=JSON.parse(e);if(!t||!Array.isArray(t.tasks))return[];let n=[];for(let o of t.tasks){let r=sf(o);r&&n.push(r)}return n}catch{return[]}}function We(e){B(ft);let t={tasks:e},n=In.join(ft,`.tasks.${process.pid}.${ac.randomBytes(4).toString("hex")}.tmp`);bo.writeFileSync(n,JSON.stringify(t,null,2)+`
5
- `,{mode:384}),bo.renameSync(n,hi)}function Ze(e,t,n){let o=t.trim().toLowerCase();return e.find(r=>r.name===o&&r.ownerPeerKey===n)}function ko(){return ac.randomBytes(4).toString("hex")}function lc(e){B(ft),bo.writeFileSync(fi,JSON.stringify(e,null,2)+`
6
- `,{mode:384})}function cc(e){let t=ki();t.push(e),lc(t)}function ki(){try{let e=bo.readFileSync(fi,"utf8"),t=JSON.parse(e);return Array.isArray(t)?t.filter(n=>n&&typeof n=="object"&&typeof n.ownerPeerKey=="string"&&typeof n.name=="string"):[]}catch{return[]}}function uc(e){if(e<=0)return{batch:[],remainingAfter:ki().length};let t=ki();if(t.length===0)return{batch:[],remainingAfter:0};let n=t.slice(0,e),o=t.slice(e);return lc(o),{batch:n,remainingAfter:o.length}}G();G();import af from"better-sqlite3";var dc=1,pc=200,Ln=null;function lf(){B(Rt);let e=new af(cr);e.pragma("journal_mode = WAL"),e.exec(`
2
+ var Of=Object.defineProperty;var Gt=(e,t)=>()=>(e&&(t=e(e=0)),t);var Nf=(e,t)=>{for(var n in t)Of(e,n,{get:t[n],enumerable:!0})};import Ff from"node:crypto";import Ii from"node:fs";import Wf from"node:os";import se from"node:path";function _f(){let e=process.env.OMNISH_HOME?.trim();if(e)return se.resolve(e);let t=Wf.homedir(),n=se.join(t,".omnish"),o=se.join(t,".whatslive");try{if(Ii.existsSync(n))return n;if(Ii.existsSync(o))return o}catch{}return n}function Mo(e){let t=Ff.createHash("sha1").update(e,"utf8").digest("hex").slice(0,8);return se.join(bc,t)}function H(e){Ii.mkdirSync(e,{recursive:!0,mode:448})}function ie(){H(D),H(ce),H(it),H(bc),H(kc),H(yt),H(Mt),H(Un),H(Ni)}var D,ce,it,bc,kc,Be,ge,Dn,To,gt,fr,gr,_,yr,wr,br,yt,Li,Oi,vc,Mt,$o,kr,Un,at,Ni,q=Gt(()=>{"use strict";D=_f(),ce=se.join(D,"auth"),it=se.join(D,"jobs"),bc=se.join(D,"apps"),kc=se.join(D,"logs"),Be=se.join(kc,"gateway.log"),ge=se.join(D,"gateway.pid"),Dn=se.join(D,"gateway-control.json"),To=se.join(D,"config-ui.json"),gt=se.join(D,"ui.json"),fr=se.join(D,"tunnel-auth.json"),gr=se.join(D,"ui-server.json"),_=se.join(D,"config.json"),yr=se.join(D,"shortcuts.json"),wr=se.join(D,"recipes.json"),br=se.join(D,"recipes-user.json"),yt=se.join(D,"cowork"),Li=se.join(yt,"tasks.json"),Oi=se.join(yt,"pending-runs.json"),vc=se.join(yt,"completions.sqlite"),Mt=se.join(D,"watch"),$o=se.join(Mt,"rules.json"),kr=se.join(Mt,"events.sqlite"),Un=se.join(D,"bin"),at=se.join(D,"venvs","whisper"),Ni=se.join(D,"media","pull")});function Tc(e){let t=e.trim();for(;;){let n=t;if(t=t.replace(/^whatsapp:/i,"").trim(),t===n)return t}}function Df(e){let t=Tc(e);if(!t.toLowerCase().endsWith("@g.us"))return!1;let o=t.slice(0,t.length-5);return!o||o.includes("@")?!1:/^[0-9]+(-[0-9]+)*$/.test(o)}function Uf(e){let t=e.match(xc);if(t)return t[1]??null;let n=e.match(Cc);if(n)return n[1]??null;let o=e.match(Rc);return o?o[1]??null:null}function Sc(e){let t=e.replace(/\D/g,"");return t?`+${t}`:""}function Jt(e){return`${e.replace(/\D/g,"")}@s.whatsapp.net`}function ne(e){let t=Tc(e);if(!t||Df(t))return null;if(xc.test(t)||Cc.test(t)||Rc.test(t)){let o=Uf(t);if(!o)return null;let r=Sc(o);return r.length>1?r:null}if(t.includes("@"))return null;let n=Sc(t);return n.length>1?n:null}function vr(e){return e.map(t=>String(t).trim()).filter(t=>!!t).map(t=>t==="*"?t:ne(t)).filter(t=>!!t)}function Sr(e){let t=new Set;for(let n of e){if(n==="*")continue;let o=ne(String(n));o&&t.add(o)}return t}function Ne(e){let t=e.trim();for(;;){let n=t.toLowerCase();if(n.startsWith("tg:")){t=t.slice(3).trimStart();continue}if(n.startsWith("telegram:")){t=t.slice(9).trimStart();continue}break}return/^\d+$/.test(t)?t:null}function xr(e){let t=new Set;for(let n of e){let o=Ne(String(n));o&&t.add(o)}return t}function Fi(e){let t=e.trim();if(!t)return null;let n=t.toLowerCase();if(n.startsWith("tg:")||n.startsWith("telegram:")){let r=Ne(t);return r?{kind:"tg",id:r}:null}let o=ne(t);return o?{kind:"wa",normalized:o}:null}function $c(e,t){let n=ne(t);return n?e.has(n):!1}var xc,Cc,Rc,Ze=Gt(()=>{"use strict";xc=/^(\d+)(?::\d+)?@s\.whatsapp\.net$/i,Cc=/^(\d+)@c\.us$/i,Rc=/^(\d+)@lid$/i});import Cr from"node:fs";function Bf(e){return typeof e.mediaSendFiles=="boolean"?e.mediaSendFiles:typeof e.pullAutoSend=="boolean"?e.pullAutoSend:I.mediaSendFiles}function Hf(e){return typeof e.mediaUrlAutoDl=="boolean"?e.mediaUrlAutoDl:typeof e.pullUrlAutoDetect=="boolean"?e.pullUrlAutoDetect:I.mediaUrlAutoDl}function jf(e){return typeof e.mediaInstallFromChat=="boolean"?e.mediaInstallFromChat:typeof e.pullInstallFromChat=="boolean"?e.pullInstallFromChat:I.mediaInstallFromChat}function Gf(e){return typeof e.mediaOutputDir=="string"?e.mediaOutputDir.trim().slice(0,4096):typeof e.pullOutputDir=="string"?e.pullOutputDir.trim().slice(0,4096):I.mediaOutputDir}function Jf(e){let t=typeof e.mediaMaxBytes=="number"?e.mediaMaxBytes:typeof e.pullMaxBytes=="number"?e.pullMaxBytes:I.mediaMaxBytes;return!Number.isFinite(t)||t<0?I.mediaMaxBytes:t===0?0:Math.min(2e9,Math.floor(t))}function qf(e){let t=typeof e.mediaWhisperModel=="string"&&e.mediaWhisperModel.trim().length>0?e.mediaWhisperModel:typeof e.pullWhisperModel=="string"&&e.pullWhisperModel.trim().length>0?e.pullWhisperModel:I.mediaWhisperModel;return String(t).trim().slice(0,64)}function zf(e){return e==="downloads"||e==="omnishData"||e==="sessionCwd"||e==="processCwd"||e==="fixed"?e:I.fileReceiveRootMode}function Kf(e){return e==="primary"?"primary":"secondary"}function Yf(e){if(!e||typeof e!="object")return{};let t={};for(let[n,o]of Object.entries(e)){if(typeof o!="string")continue;let r=o.trim();if(!r)continue;let s=n.trim(),i=s.toLowerCase();if(i.startsWith("wa:")){let l=ne(s.slice(3));l&&(t[`wa:${l}`]=r.slice(0,64));continue}if(i.startsWith("tg:")||i.startsWith("telegram:")){let l=Ne(s);l&&(t[`tg:${l}`]=r.slice(0,64));continue}let a=ne(s);a&&(t[`wa:${a}`]=r.slice(0,64))}return t}function Po(e){let t=typeof e.appsFlushMs=="number"&&e.appsFlushMs>=0?e.appsFlushMs:I.appsFlushMs,n=e.gatewayMode==="telegram"||e.gatewayMode==="both"||e.gatewayMode==="whatsapp"?e.gatewayMode:I.gatewayMode,o=typeof e.telegramBotToken=="string"?e.telegramBotToken:I.telegramBotToken,r=Array.isArray(e.telegramAllowFrom)?[...new Set(e.telegramAllowFrom.map(s=>Ne(String(s))).filter(s=>!!s))].sort():I.telegramAllowFrom;return{...I,...e,gatewayMode:n,telegramBotToken:o,telegramAllowFrom:r,allowFrom:Array.isArray(e.allowFrom)?vr(e.allowFrom.map(String)).filter(s=>s!=="*"):I.allowFrom,commandPrefix:(()=>{let s=typeof e.commandPrefix=="string"&&e.commandPrefix.length>0?e.commandPrefix:I.commandPrefix;return s==="! "?"!":s})(),syncTimeoutMs:typeof e.syncTimeoutMs=="number"&&e.syncTimeoutMs>0?e.syncTimeoutMs:I.syncTimeoutMs,syncMaxBytes:typeof e.syncMaxBytes=="number"&&e.syncMaxBytes>0?e.syncMaxBytes:I.syncMaxBytes,jobLogTailLines:typeof e.jobLogTailLines=="number"&&e.jobLogTailLines>0?e.jobLogTailLines:I.jobLogTailLines,shell:typeof e.shell=="string"&&e.shell.length>0?e.shell:I.shell,appsCols:typeof e.appsCols=="number"&&e.appsCols>0&&e.appsCols<=500?Math.floor(e.appsCols):I.appsCols,appsRows:typeof e.appsRows=="number"&&e.appsRows>0&&e.appsRows<=200?Math.floor(e.appsRows):I.appsRows,appsFlushMs:t,appsMinIntervalMs:typeof e.appsMinIntervalMs=="number"&&e.appsMinIntervalMs>=0?e.appsMinIntervalMs:I.appsMinIntervalMs,appsMaxFlushBytes:typeof e.appsMaxFlushBytes=="number"&&e.appsMaxFlushBytes>256?Math.floor(e.appsMaxFlushBytes):I.appsMaxFlushBytes,appsMaxSessions:typeof e.appsMaxSessions=="number"&&e.appsMaxSessions>0?Math.min(50,Math.floor(e.appsMaxSessions)):I.appsMaxSessions,appsMaxSessionsTotal:typeof e.appsMaxSessionsTotal=="number"&&e.appsMaxSessionsTotal>0?Math.min(200,Math.floor(e.appsMaxSessionsTotal)):I.appsMaxSessionsTotal,appsMaxWaChars:typeof e.appsMaxWaChars=="number"&&e.appsMaxWaChars>256?Math.floor(e.appsMaxWaChars):I.appsMaxWaChars,appsLogTailLines:typeof e.appsLogTailLines=="number"&&e.appsLogTailLines>0?Math.min(500,Math.floor(e.appsLogTailLines)):I.appsLogTailLines,appsSubmitDelayMs:typeof e.appsSubmitDelayMs=="number"&&e.appsSubmitDelayMs>=0?Math.min(500,Math.floor(e.appsSubmitDelayMs)):I.appsSubmitDelayMs,appsClearInput:typeof e.appsClearInput=="boolean"?e.appsClearInput:I.appsClearInput,appsClearInputDelayMs:typeof e.appsClearInputDelayMs=="number"&&e.appsClearInputDelayMs>=0?Math.min(200,Math.floor(e.appsClearInputDelayMs)):I.appsClearInputDelayMs,appsClearInputSequence:typeof e.appsClearInputSequence=="string"&&e.appsClearInputSequence.length>0?e.appsClearInputSequence.slice(0,200):I.appsClearInputSequence,appsSkipClearOnPasswordPrompt:typeof e.appsSkipClearOnPasswordPrompt=="boolean"?e.appsSkipClearOnPasswordPrompt:I.appsSkipClearOnPasswordPrompt,appsPasswordPromptHint:typeof e.appsPasswordPromptHint=="boolean"?e.appsPasswordPromptHint:I.appsPasswordPromptHint,fileSendMaxBytes:typeof e.fileSendMaxBytes=="number"&&e.fileSendMaxBytes>=0?e.fileSendMaxBytes===0?0:Math.min(2e9,Math.floor(e.fileSendMaxBytes)):I.fileSendMaxBytes,fileReceiveMaxBytes:typeof e.fileReceiveMaxBytes=="number"&&e.fileReceiveMaxBytes>=0?e.fileReceiveMaxBytes===0?0:Math.min(2e9,Math.floor(e.fileReceiveMaxBytes)):I.fileReceiveMaxBytes,fileInboxSubdir:typeof e.fileInboxSubdir=="string"&&e.fileInboxSubdir.length>0&&e.fileInboxSubdir.replace(/[/\\]/g,"").slice(0,80)||I.fileInboxSubdir,fileReceiveRootMode:zf(e.fileReceiveRootMode),fileReceiveRootPath:typeof e.fileReceiveRootPath=="string"?e.fileReceiveRootPath.trim().slice(0,4096):I.fileReceiveRootPath,recipesAllowDangerousBuiltins:typeof e.recipesAllowDangerousBuiltins=="boolean"?e.recipesAllowDangerousBuiltins:I.recipesAllowDangerousBuiltins,recipesMaxTaskChars:typeof e.recipesMaxTaskChars=="number"&&e.recipesMaxTaskChars>=0?e.recipesMaxTaskChars===0?0:Math.min(Number.MAX_SAFE_INTEGER,Math.floor(e.recipesMaxTaskChars)):I.recipesMaxTaskChars,recipesMacroDefaultCommand:typeof e.recipesMacroDefaultCommand=="string"&&e.recipesMacroDefaultCommand.trim().length>0?e.recipesMacroDefaultCommand.trim().slice(0,4096):I.recipesMacroDefaultCommand,recipesRunAttach:typeof e.recipesRunAttach=="boolean"?e.recipesRunAttach:I.recipesRunAttach,clusterEnabled:typeof e.clusterEnabled=="boolean"?e.clusterEnabled:I.clusterEnabled,clusterLabel:typeof e.clusterLabel=="string"?e.clusterLabel.trim().slice(0,128):I.clusterLabel,clusterRole:Kf(e.clusterRole),clusterSenderBindings:Yf(e.clusterSenderBindings),serviceInstallFromChat:typeof e.serviceInstallFromChat=="boolean"?e.serviceInstallFromChat:I.serviceInstallFromChat,updateCheckEnabled:typeof e.updateCheckEnabled=="boolean"?e.updateCheckEnabled:I.updateCheckEnabled,updateCheckIntervalMs:typeof e.updateCheckIntervalMs=="number"&&e.updateCheckIntervalMs>0?Math.min(6048e5,Math.max(36e5,Math.floor(e.updateCheckIntervalMs))):I.updateCheckIntervalMs,updateCheckPackageName:typeof e.updateCheckPackageName=="string"&&e.updateCheckPackageName.trim().length>0?e.updateCheckPackageName.trim().slice(0,214):I.updateCheckPackageName,updateInfoUrl:typeof e.updateInfoUrl=="string"?e.updateInfoUrl.trim().slice(0,2048):I.updateInfoUrl,chatLlmFallbackEnabled:typeof e.chatLlmFallbackEnabled=="boolean"?e.chatLlmFallbackEnabled:I.chatLlmFallbackEnabled,chatLlmShellCommand:typeof e.chatLlmShellCommand=="string"?e.chatLlmShellCommand.trim().slice(0,8192):I.chatLlmShellCommand,chatLlmTimeoutMs:typeof e.chatLlmTimeoutMs=="number"&&e.chatLlmTimeoutMs>0?Math.min(9e5,Math.floor(e.chatLlmTimeoutMs)):I.chatLlmTimeoutMs,chatLlmMaxInputChars:typeof e.chatLlmMaxInputChars=="number"&&e.chatLlmMaxInputChars>0?Math.min(5e5,Math.floor(e.chatLlmMaxInputChars)):I.chatLlmMaxInputChars,chatLlmMaxOutputChars:typeof e.chatLlmMaxOutputChars=="number"&&e.chatLlmMaxOutputChars>0?Math.min(2e6,Math.floor(e.chatLlmMaxOutputChars)):I.chatLlmMaxOutputChars,chatLlmNeedsTty:typeof e.chatLlmNeedsTty=="boolean"?e.chatLlmNeedsTty:I.chatLlmNeedsTty,chatLlmWorkDir:typeof e.chatLlmWorkDir=="string"?e.chatLlmWorkDir.trim().slice(0,4096):I.chatLlmWorkDir,tunnelEnabled:typeof e.tunnelEnabled=="boolean"?e.tunnelEnabled:I.tunnelEnabled,tunnelRelayUrl:typeof e.tunnelRelayUrl=="string"&&e.tunnelRelayUrl.trim().length>0?e.tunnelRelayUrl.trim().slice(0,2048):I.tunnelRelayUrl,tunnelMaxActive:typeof e.tunnelMaxActive=="number"&&e.tunnelMaxActive>0?Math.min(50,Math.floor(e.tunnelMaxActive)):I.tunnelMaxActive,platformToken:typeof e.platformToken=="string"?e.platformToken.trim():I.platformToken,platformDeviceId:typeof e.platformDeviceId=="string"?e.platformDeviceId.trim().slice(0,128):I.platformDeviceId,webhookEnabled:typeof e.webhookEnabled=="boolean"?e.webhookEnabled:I.webhookEnabled,webhookPort:typeof e.webhookPort=="number"&&e.webhookPort>=0?Math.min(65535,Math.floor(e.webhookPort)):I.webhookPort,webhookHost:typeof e.webhookHost=="string"&&e.webhookHost.trim().length>0?e.webhookHost.trim():I.webhookHost,webhookToken:typeof e.webhookToken=="string"?e.webhookToken.trim():I.webhookToken,watchEnabled:typeof e.watchEnabled=="boolean"?e.watchEnabled:I.watchEnabled,watchDebounceMs:typeof e.watchDebounceMs=="number"&&Number.isFinite(e.watchDebounceMs)?Math.max(500,Math.min(6e4,Math.floor(e.watchDebounceMs))):I.watchDebounceMs,watchMaxEventsPerMinute:typeof e.watchMaxEventsPerMinute=="number"&&Number.isFinite(e.watchMaxEventsPerMinute)?Math.max(1,Math.min(120,Math.floor(e.watchMaxEventsPerMinute))):I.watchMaxEventsPerMinute,watchAutoRestore:typeof e.watchAutoRestore=="boolean"?e.watchAutoRestore:I.watchAutoRestore,mediaSendFiles:Bf(e),mediaUrlAutoDl:Hf(e),mediaInstallFromChat:jf(e),mediaOutputDir:Gf(e),mediaMaxBytes:Jf(e),mediaWhisperModel:qf(e),progressUpdates:typeof e.progressUpdates=="boolean"?e.progressUpdates:I.progressUpdates,pullYtDlpPath:typeof e.pullYtDlpPath=="string"?e.pullYtDlpPath.trim().slice(0,4096):I.pullYtDlpPath,pullFfmpegPath:typeof e.pullFfmpegPath=="string"?e.pullFfmpegPath.trim().slice(0,4096):I.pullFfmpegPath,pullWhisperPath:typeof e.pullWhisperPath=="string"?e.pullWhisperPath.trim().slice(0,4096):I.pullWhisperPath}}function N(e){let t=S(),n=Po({...t,...e});return He(n),n}function S(){if(ie(),!Cr.existsSync(_)){let e=Po({});return He(e),e}try{let e=Cr.readFileSync(_,"utf8"),t=JSON.parse(e);return Po(t)}catch{return Po({})}}function He(e){ie();let t=Po(e);Cr.writeFileSync(_,JSON.stringify(t,null,2)+`
3
+ `,{mode:384})}function Rr(e){let t=Fi(e);if(!t)throw new Error("Invalid entry. Use +E164 (WhatsApp) or tg:<user_id> (Telegram).");let n=S();if(t.kind==="wa"){if(t.normalized==="*")throw new Error("Wildcards are not allowed.");let o=new Set(n.allowFrom);o.add(t.normalized),n.allowFrom=[...o].sort()}else{let o=new Set(n.telegramAllowFrom);o.add(t.id),n.telegramAllowFrom=[...o].sort()}return He(n),n}function Tr(e){let t=Fi(e);if(!t)throw new Error("Invalid entry. Use +E164 (WhatsApp) or tg:<user_id> (Telegram).");let n=S();return t.kind==="wa"?n.allowFrom=n.allowFrom.filter(o=>o!==t.normalized):n.telegramAllowFrom=n.telegramAllowFrom.filter(o=>Ne(o)!==t.id),He(n),n}function Me(e){let t=typeof process.env.TELEGRAM_BOT_TOKEN=="string"?process.env.TELEGRAM_BOT_TOKEN.trim():"";return t||(e.telegramBotToken??"").trim()}function wt(e){let t=e.trim();return/^\d{6,}:[A-Za-z0-9_-]{20,}$/.test(t)}function qt(e){let t=S();return t.telegramBotToken=e.trim(),He(t),S()}function Bn(e){let t=e.trim().toLowerCase();return t==="whatsapp"||t==="wa"||t==="w"?"whatsapp":t==="telegram"||t==="tg"||t==="t"?"telegram":t==="both"||t==="all"||t==="b"?"both":null}function $r(e){let t=S();return t.gatewayMode=e,He(t),S()}function Mr(e){let t=S();return t.clusterEnabled=e,He(t),S()}function lt(){try{return Cr.readdirSync(ce).length>0}catch{return!1}}var I,pe=Gt(()=>{"use strict";q();Ze();I={gatewayMode:"whatsapp",telegramBotToken:"",telegramAllowFrom:[],allowFrom:[],commandPrefix:"!",syncTimeoutMs:3e4,syncMaxBytes:32768,jobLogTailLines:80,shell:"/bin/bash",appsCols:120,appsRows:40,appsFlushMs:300,appsMinIntervalMs:800,appsMaxFlushBytes:8192,appsMaxSessions:5,appsMaxSessionsTotal:20,appsMaxWaChars:3500,appsLogTailLines:80,appsSubmitDelayMs:50,appsClearInput:!0,appsClearInputDelayMs:20,appsClearInputSequence:"^A,^K",appsSkipClearOnPasswordPrompt:!0,appsPasswordPromptHint:!0,fileSendMaxBytes:0,fileReceiveMaxBytes:0,fileInboxSubdir:"inbox",fileReceiveRootMode:"downloads",fileReceiveRootPath:"",recipesAllowDangerousBuiltins:!1,recipesMaxTaskChars:0,recipesMacroDefaultCommand:'claude -p "$OMNISH_TASK"',recipesRunAttach:!1,clusterEnabled:!1,clusterLabel:"",clusterRole:"secondary",clusterSenderBindings:{},serviceInstallFromChat:!1,updateCheckEnabled:!1,updateCheckIntervalMs:864e5,updateCheckPackageName:"omnish",updateInfoUrl:"",chatLlmFallbackEnabled:!1,chatLlmShellCommand:"",chatLlmTimeoutMs:12e4,chatLlmMaxInputChars:16e3,chatLlmMaxOutputChars:24e3,chatLlmNeedsTty:!1,chatLlmWorkDir:"",tunnelEnabled:!1,tunnelRelayUrl:"https://tunnel.omnish.dev",platformToken:"",platformDeviceId:"",tunnelMaxActive:5,webhookEnabled:!1,webhookPort:0,webhookHost:"127.0.0.1",webhookToken:"",watchEnabled:!1,watchDebounceMs:2e3,watchMaxEventsPerMinute:30,watchAutoRestore:!0,mediaSendFiles:!0,mediaUrlAutoDl:!1,mediaInstallFromChat:!1,mediaOutputDir:"",mediaMaxBytes:0,mediaWhisperModel:"small",progressUpdates:!0,pullYtDlpPath:"",pullFfmpegPath:"",pullWhisperPath:""}});import Qf from"pino";function Hn(){return process.env.OMNISH_VERBOSE==="1"||process.env.WHATSVERBOSE==="1"}function Mc(){return Hn()?"info":"silent"}function Wi(e){e?process.env.OMNISH_VERBOSE="1":delete process.env.OMNISH_VERBOSE,M.level=Mc()}function Pc(){return M.child({module:"baileys"})}var M,Ce=Gt(()=>{"use strict";M=Qf({level:Mc(),base:{app:"omnish"}})});import Vi from"node:fs";function It(){try{let e=Vi.readFileSync(fr,"utf8"),t=JSON.parse(e);return!t||typeof t.token!="string"||!t.token.trim()?null:{token:t.token.trim(),relayUrl:typeof t.relayUrl=="string"?t.relayUrl.trim():void 0}}catch{return null}}function vt(e){ie(),Vi.writeFileSync(fr,JSON.stringify({token:e.token.trim(),...e.relayUrl?{relayUrl:e.relayUrl.trim()}:{}},null,2)+`
4
+ `,{mode:384})}function Br(){try{Vi.unlinkSync(fr)}catch{}}function Kg(){return process.env.OMNISH_TOKEN?.trim()||process.env.OMNISH_TUNNEL_TOKEN?.trim()||process.env.OMNISH_DEVICE_TOKEN?.trim()||""}function St(){let e=Kg();if(e)return e;let t=S().platformToken.trim();return t||(It()?.token??"")}function Xi(e){let t=process.env.OMNISH_PLATFORM_URL?.trim()||process.env.OMNISH_COMM_LAYER_URL?.trim()||process.env.OMNISH_TUNNEL_RELAY?.trim();if(t)return t.replace(/\/$/,"");let n=S().tunnelRelayUrl.trim();if(n)return n.replace(/\/$/,"");let o=It()?.relayUrl?.trim();return o?o.replace(/\/$/,""):e.replace(/\/$/,"")}function ct(e){return Xi(e)}var bn=Gt(()=>{"use strict";pe();q()});var Ee,kn=Gt(()=>{"use strict";Ee="https://tunnel.omnish.dev"});function on(e){return(e??"").trim()}function Ey(){return on(process.env.OMNISH_PLATFORM_URL)||on(process.env.OMNISH_COMM_LAYER_URL)||on(process.env.OMNISH_TUNNEL_RELAY)}function Ay(){return on(process.env.OMNISH_TOKEN)||on(process.env.OMNISH_DEVICE_TOKEN)||on(process.env.OMNISH_TUNNEL_TOKEN)}function Ea(){return St()}function eo(){return Ay()?"env":S().platformToken.trim()?"config":It()?.token?"file":"default"}function jo(){let e=S();return Xi(e.tunnelRelayUrl||Ee)}function to(){return Ey()?"env":S().tunnelRelayUrl.trim()?"config":It()?.relayUrl?.trim()?"file":"default"}function Aa(){let e=on(process.env.OMNISH_DEVICE_ID);if(e)return e;let t=S().platformDeviceId.trim();if(t)return t}function Ia(){return on(process.env.OMNISH_DEVICE_ID)?"env":S().platformDeviceId.trim()?"config":"default"}function re(){let e=jo(),t=Ea();if(!e||!t)return null;let n=Aa();return{platformUrl:e.replace(/\/$/,""),token:t,deviceId:n}}var Ve=Gt(()=>{"use strict";pe();bn();kn()});var lm={};Nf(lm,{fetchPlatformAccount:()=>In,getAttachedConfig:()=>yl,getAttachedPlatformSnapshot:()=>bk,loadConfigForSendtoBroadcast:()=>wl,mergeAttachedPlatformPolicy:()=>gl,parsePlatformMeResponse:()=>im,setAttachedPlatformSnapshot:()=>hl,setPlatformDefaultDevice:()=>bl,snapshotFromRegisteredAccount:()=>am,syncAttachedPlatformPolicy:()=>Js,updatePlatformAllowlists:()=>Gs});function fl(e){let t=e.replace(/\/$/,"");return/^https?:\/\//i.test(t)?t:`http://${t}`}function yk(e){return e==="telegram"||e==="both"||e==="whatsapp"?e:"whatsapp"}function wk(e){if(!e||typeof e!="object")return{};let t={};for(let[n,o]of Object.entries(e)){if(!o||typeof o!="object")continue;let r=o,s=typeof r.status=="string"?r.status:"idle";t[n]={status:s,linked:r.linked===!0||s==="linked",...typeof r.tokenConfigured=="boolean"?{tokenConfigured:r.tokenConfigured}:{}}}return t}function im(e){let t=e.routing,n=t&&typeof t=="object"?t:null,o=Array.isArray(n?.onlineDeviceIds)?n.onlineDeviceIds.map(String):[],r=n?.defaultDeviceId!=null?String(n.defaultDeviceId):e.defaultDeviceId!=null?String(e.defaultDeviceId):null;return{allowFrom:Array.isArray(e.allowFrom)?e.allowFrom.map(String):[],telegramAllowFrom:Array.isArray(e.telegramAllowFrom)?e.telegramAllowFrom.map(String):[],gatewayMode:yk(e.gatewayMode),connectors:wk(e.connectors),defaultDeviceId:r,routing:{defaultDeviceId:r,onlineDeviceIds:o,onlineCount:typeof n?.onlineCount=="number"?n.onlineCount:o.length}}}function am(e,t=[],n=[]){let o=e.defaultDeviceId??null;return{allowFrom:t,telegramAllowFrom:n,gatewayMode:e.gatewayMode,connectors:e.connectors,defaultDeviceId:o,routing:{defaultDeviceId:o,onlineDeviceIds:[],onlineCount:0}}}function gl(e,t){return{...e,allowFrom:[...t.allowFrom],telegramAllowFrom:[...t.telegramAllowFrom],gatewayMode:t.gatewayMode}}function hl(e){js=e}function bk(){return js}function yl(){let e=S();return js?gl(e,js):e}async function wl(){let e=S(),t=re();if(!t)return e;try{return gl(e,await In(t))}catch(n){return M.warn({err:String(n)},"sendto: could not fetch platform allowlists; using local config.json"),e}}async function In(e){let t=await fetch(`${fl(e.platformUrl)}/v1/me`,{headers:{Authorization:`Bearer ${e.token}`}});if(!t.ok)throw new Error(`Platform GET /v1/me failed: HTTP ${t.status}`);let n=await t.json();return im(n)}async function Gs(e,t){let n=await fetch(`${fl(e.platformUrl)}/v1/me/allowlists`,{method:"PUT",headers:{"content-type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify(t)}),o=await n.json().catch(()=>({}));if(!n.ok)throw new Error(o.error||`Platform PUT /v1/me/allowlists failed: HTTP ${n.status}`);return{allowFrom:Array.isArray(o.allowFrom)?o.allowFrom.map(String):[],telegramAllowFrom:Array.isArray(o.telegramAllowFrom)?o.telegramAllowFrom.map(String):[]}}async function bl(e,t){let n=await fetch(`${fl(e.platformUrl)}/v1/me/default-device`,{method:"PUT",headers:{"content-type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify({deviceId:t})});if(!n.ok){let o=await n.json().catch(()=>({}));throw new Error(o.error||`Platform PUT /v1/me/default-device failed: HTTP ${n.status}`)}}async function Js(e,t){try{let n=await In(e);return hl(n),M.info({gatewayMode:n.gatewayMode,waLinked:n.connectors.whatsapp?.linked===!0,tgLinked:n.connectors.telegram?.linked===!0,allowFromCount:n.allowFrom.length,telegramAllowFromCount:n.telegramAllowFrom.length},"attached platform policy loaded"),n}catch(n){if(t){let o=am(t);return hl(o),M.warn({err:String(n)},"platform GET /v1/me failed; using register ack for gatewayMode/connectors only (allowlists from local config until /v1/me works)"),o}return M.warn({err:String(n)},"platform GET /v1/me failed; attached inbound uses local config.json allowlists"),null}}var js,tr=Gt(()=>{"use strict";pe();Ce();Ve();js=null});pe();import JS from"node:dns";import qS from"node:crypto";import gn from"node:fs";import yc from"node:path";import $f from"node:os";pe();Ce();import Fg from"node:os";import Wg from"node:fs";q();import Ec from"node:crypto";import Eo from"node:fs";import _i from"node:os";import jn from"node:path";var Vf=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/;function Ui(e){let t=e.trim().toLowerCase();return Vf.test(t)?{ok:!0,name:t}:{ok:!1,error:"Name must be alphanumeric with _ or -, max 32 chars."}}function Qe(e,t){let n=e.trim();return n===""||n==="."?jn.resolve(t):n==="~"?_i.homedir():n.startsWith("~/")||n.startsWith("~\\")?jn.join(_i.homedir(),n.slice(2)):jn.isAbsolute(n)?n:jn.resolve(t,n)}function Pr(e){return jn.join(_i.homedir(),"Cowork",e)}function Xf(e){if(!e||typeof e!="object")return null;let t=e,n=typeof t.id=="string"&&t.id.length>=4?t.id.slice(0,32):"",o=typeof t.name=="string"?t.name.trim().toLowerCase():"",r=typeof t.ownerPeerKey=="string"?t.ownerPeerKey:"",s=typeof t.command=="string"?t.command:"";if(!n||!o||!r||!s)return null;let i=typeof t.cwd=="string"?t.cwd:"",a=typeof t.outputDir=="string"&&t.outputDir.trim()?t.outputDir:Pr(o),l=typeof t.enabled=="boolean"?t.enabled:!0,c=typeof t.notify=="string"?t.notify.toLowerCase():"self",d=c==="wa"||c==="whatsapp"?"wa":c==="tg"||c==="telegram"?"tg":c==="all"?"all":c==="none"?"none":"self",u={kind:"ondemand"};if(t.schedule&&typeof t.schedule=="object"){let k=t.schedule,R=typeof k.kind=="string"?k.kind:"";if(R==="ondemand")u={kind:"ondemand"};else if(R==="daily"){let T=Number(k.hour),L=Number(k.minute);Number.isFinite(T)&&Number.isFinite(L)&&(u={kind:"daily",hour:T,minute:L})}else if(R==="weekdays"){let T=Number(k.hour),L=Number(k.minute);Number.isFinite(T)&&Number.isFinite(L)&&(u={kind:"weekdays",hour:T,minute:L})}else if(R==="hourly"){let T=Number(k.minute);Number.isFinite(T)&&Number.isInteger(T)&&T>=0&&T<=59&&(u={kind:"hourly",minute:T})}else if(R==="weekly"){let T=Number(k.hour),L=Number(k.minute),x=Number(k.weekday);Number.isFinite(T)&&Number.isFinite(L)&&Number.isInteger(x)&&(u={kind:"weekly",weekday:x,hour:T,minute:L})}else if(R==="heartbeat"){let T=Number(k.intervalMs),L=Number(k.graceMs);Number.isFinite(T)&&T>0&&Number.isFinite(L)&&L>0&&(u={kind:"heartbeat",intervalMs:T,graceMs:L})}}let m=null;typeof t.lastCompletedSlotMs=="number"&&Number.isFinite(t.lastCompletedSlotMs)&&(m=t.lastCompletedSlotMs);let h=typeof t.createdAtMs=="number"&&Number.isFinite(t.createdAtMs)?t.createdAtMs:Date.now(),f=typeof t.attachLog=="boolean"?t.attachLog:!1,g=Array.isArray(t.attachFiles)?t.attachFiles.filter(k=>typeof k=="string"&&k.trim().length>0):[],y=typeof t.notifyWhen=="string"?t.notifyWhen.toLowerCase():"always";return{id:n,name:o,ownerPeerKey:r,command:s,cwd:i,outputDir:a,schedule:u,enabled:l,notify:d,notifyWhen:y==="failure"?"failure":y==="state-change"?"state-change":"always",attachLog:f,attachFiles:g,lastCompletedSlotMs:m,createdAtMs:h}}function Pe(){try{let e=Eo.readFileSync(Li,"utf8"),t=JSON.parse(e);if(!t||!Array.isArray(t.tasks))return[];let n=[];for(let o of t.tasks){let r=Xf(o);r&&n.push(r)}return n}catch{return[]}}function De(e){H(yt);let t={tasks:e},n=jn.join(yt,`.tasks.${process.pid}.${Ec.randomBytes(4).toString("hex")}.tmp`);Eo.writeFileSync(n,JSON.stringify(t,null,2)+`
5
+ `,{mode:384}),Eo.renameSync(n,Li)}function et(e,t,n){let o=t.trim().toLowerCase();return e.find(r=>r.name===o&&r.ownerPeerKey===n)}function Ao(){return Ec.randomBytes(4).toString("hex")}function Ac(e){H(yt),Eo.writeFileSync(Oi,JSON.stringify(e,null,2)+`
6
+ `,{mode:384})}function Ic(e){let t=Di();t.push(e),Ac(t)}function Di(){try{let e=Eo.readFileSync(Oi,"utf8"),t=JSON.parse(e);return Array.isArray(t)?t.filter(n=>n&&typeof n=="object"&&typeof n.ownerPeerKey=="string"&&typeof n.name=="string"):[]}catch{return[]}}function Lc(e){if(e<=0)return{batch:[],remainingAfter:Di().length};let t=Di();if(t.length===0)return{batch:[],remainingAfter:0};let n=t.slice(0,e),o=t.slice(e);return Ac(o),{batch:n,remainingAfter:o.length}}q();q();import Zf from"better-sqlite3";var Oc=1,Nc=200,Gn=null;function eg(){H(Mt);let e=new Zf(kr);e.pragma("journal_mode = WAL"),e.exec(`
7
7
  CREATE TABLE IF NOT EXISTS watch_meta (
8
8
  key TEXT PRIMARY KEY,
9
9
  value TEXT NOT NULL
@@ -22,121 +22,138 @@ var Jh=Object.defineProperty;var Ut=(e,t)=>()=>(e&&(t=e(e=0)),t);var zh=(e,t)=>{
22
22
  state_key TEXT NOT NULL,
23
23
  updated_at_ms INTEGER NOT NULL
24
24
  );
25
- `);let t=e.prepare("SELECT value FROM watch_meta WHERE key = 'schema_version'").get();return(t?Number(t.value):0)<dc&&e.prepare("INSERT OR REPLACE INTO watch_meta (key, value) VALUES ('schema_version', ?)").run(String(dc)),e}function vo(){return Ln||(Ln=lf()),Ln}function br(){vo()}function mc(){if(Ln){try{Ln.close()}catch{}Ln=null}}function hc(e){let t=vo();t.prepare("INSERT INTO watch_recent (rule_id, rule_name, kind, summary, ts_ms) VALUES (?, ?, ?, ?, ?)").run(e.ruleId,e.ruleName,e.kind,e.summary.slice(0,2e3),e.tsMs);let n=t.prepare("SELECT COUNT(*) AS c FROM watch_recent").get();n.c>pc&&t.prepare(`DELETE FROM watch_recent WHERE id IN (
25
+ `);let t=e.prepare("SELECT value FROM watch_meta WHERE key = 'schema_version'").get();return(t?Number(t.value):0)<Oc&&e.prepare("INSERT OR REPLACE INTO watch_meta (key, value) VALUES ('schema_version', ?)").run(String(Oc)),e}function Io(){return Gn||(Gn=eg()),Gn}function Er(){Io()}function Fc(){if(Gn){try{Gn.close()}catch{}Gn=null}}function Wc(e){let t=Io();t.prepare("INSERT INTO watch_recent (rule_id, rule_name, kind, summary, ts_ms) VALUES (?, ?, ?, ?, ?)").run(e.ruleId,e.ruleName,e.kind,e.summary.slice(0,2e3),e.tsMs);let n=t.prepare("SELECT COUNT(*) AS c FROM watch_recent").get();n.c>Nc&&t.prepare(`DELETE FROM watch_recent WHERE id IN (
26
26
  SELECT id FROM watch_recent ORDER BY ts_ms ASC LIMIT ?
27
- )`).run(n.c-pc)}function fc(e,t){let n=vo(),o=Math.min(Math.max(1,e),50),r=n.prepare("SELECT rule_id, rule_name, kind, summary, ts_ms FROM watch_recent ORDER BY ts_ms DESC LIMIT ?").all(o*3),s=[];for(let i of r)if(!(t&&!t.has(i.rule_id))&&(s.push({ruleId:i.rule_id,ruleName:i.rule_name,kind:i.kind,stateKey:"",summary:i.summary,tsMs:i.ts_ms}),s.length>=o))break;return s}function gc(e){return vo().prepare("SELECT state_key FROM watch_rule_state WHERE rule_id = ?").get(e)?.state_key??null}function yc(e,t,n){vo().prepare("INSERT OR REPLACE INTO watch_rule_state (rule_id, state_key, updated_at_ms) VALUES (?, ?, ?)").run(e,t,n)}import Sc from"node:path";import Tt from"node:path";function cf(e,t){let n=Tt.normalize(e);for(let o of t){let r=Tt.normalize(o);if(n===r||n.startsWith(r+Tt.sep))return!0}return!1}function uf(e,t,n){let o=Tt.relative(t,e);if(o.startsWith("..")||Tt.isAbsolute(o))return!1;let r=o.split(Tt.sep).join("/"),s=n.replace(/\\/g,"/").replace(/^\//,"");if(s.includes("**")){let i=s.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,"___GLOBSTAR___").replace(/\*/g,"[^/]*").replace(/___GLOBSTAR___/g,".*");try{if(new RegExp(`^${i}$`).test(r))return!0;if(s.startsWith("**/")){let l=s.slice(3);if(l&&!l.includes("*"))return r.split("/").includes(l)}return!1}catch{return!1}}if(s.includes("*")){let i=s.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,"[^/]*");try{return new RegExp(`^${i}$`).test(r)}catch{return!1}}return r===s||r.endsWith("/"+s)||r.includes("/"+s+"/")}function kr(e,t){if(t.excludePaths.length&&cf(e,t.excludePaths))return!0;let n=t.path;if(n&&t.excludeGlobs.length){for(let o of t.excludeGlobs)if(uf(e,n,o))return!0}return!1}function wc(e){let t=[];for(let n of e.excludeGlobs){let o=n.replace(/\\/g,"/");o.startsWith("**/")?t.push(o):o.includes("*")?t.push(`**/${o}`):t.push(`**/${o}/**`)}for(let n of e.excludePaths)if(e.path)try{let o=Tt.relative(e.path,n);!o.startsWith("..")&&!Tt.isAbsolute(o)&&t.push(o.split(Tt.sep).join("/")+"/**")}catch{}return t}Xe();function On(e,t,n){if(e==="none")return[];let o=new Set;if((e==="self"||e==="all")&&o.add(t),e==="wa"||e==="all")for(let r of n.allowFrom){let s=te(String(r));s&&o.add(`wa:${Bt(s)}`)}if(e==="tg"||e==="all")for(let r of n.telegramAllowFrom){let s=Ne(String(r));s&&o.add(`tg:${s}`)}return[...o]}function bc(e,t,n){return On(e,t,n)}xe();G();import df from"node:crypto";import Si from"node:fs";var pf=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,mf=1,xi=20;function vr(e){let t=e.trim().toLowerCase();return pf.test(t)?{ok:!0,name:t}:{ok:!1,error:"Name must be alphanumeric with _ or -, max 32 chars."}}function Sr(){return df.randomBytes(8).toString("hex")}function hf(e){if(!Array.isArray(e)||e.length===0)return["create","delete","rename"];let t=new Set(["create","delete","rename","update"]),n=[];for(let o of e)typeof o=="string"&&t.has(o)&&n.push(o);return n.length?n:["create","delete","rename"]}function ff(e){if(!e||typeof e!="object")return null;let t=e,n=typeof t.id=="string"&&t.id.length>=4?t.id.slice(0,32):"",o=typeof t.name=="string"?t.name.trim().toLowerCase():"",r=typeof t.ownerPeerKey=="string"?t.ownerPeerKey:"",s=typeof t.kind=="string"?t.kind:"",i=s==="fs"||s==="pkg"||s==="svc"?s:null;if(!n||!o||!r||!i)return null;let a=typeof t.notify=="string"?t.notify.toLowerCase():"self",l=a==="wa"||a==="whatsapp"?"wa":a==="tg"||a==="telegram"?"tg":a==="all"?"all":a==="none"?"none":"self",d=typeof t.notifyWhen=="string"?t.notifyWhen:"always",u=d==="failure"||d==="state-change"?d:"always",c=Array.isArray(t.units)?t.units.filter(f=>typeof f=="string"&&f.trim().length>0).map(f=>f.trim()):[],m=Array.isArray(t.excludePaths)?t.excludePaths.filter(f=>typeof f=="string"&&f.trim().length>0):[],h=Array.isArray(t.excludeGlobs)?t.excludeGlobs.filter(f=>typeof f=="string"&&f.trim().length>0):[];return{id:n,name:o,ownerPeerKey:r,kind:i,enabled:typeof t.enabled=="boolean"?t.enabled:!0,paused:typeof t.paused=="boolean"?t.paused:!1,notify:l,notifyWhen:u,path:typeof t.path=="string"?t.path:"",events:hf(t.events),units:c,excludePaths:m,excludeGlobs:h,adapterStatus:typeof t.adapterStatus=="string"?t.adapterStatus:"",createdAtMs:typeof t.createdAtMs=="number"&&Number.isFinite(t.createdAtMs)?t.createdAtMs:Date.now()}}function gf(e){let t=JSON.parse(e);return!t||!Array.isArray(t.rules)?[]:t.rules.map(ff).filter(n=>n!==null)}function yf(e){let t=[...e].sort((s,i)=>s.createdAtMs-i.createdAtMs),n=new Set,o=!1,r=[];for(let s of t){if(!n.has(s.name)){n.add(s.name),r.push(s);continue}let i=s.id.slice(0,8),a=`${s.name}-${i}`;a.length>32&&(a=`${s.name.slice(0,32-i.length-1)}-${i}`);let l=0;for(;n.has(a);){l+=1;let d=String(l);a=`${s.name.slice(0,Math.max(1,32-i.length-d.length-1))}-${i}${d}`}n.add(a),P.warn({oldName:s.name,newName:a,id:s.id},"watch: renamed duplicate rule for device-wide namespace"),r.push({...s,name:a}),o=!0}return{rules:r,changed:o}}function De(){try{let e=Si.readFileSync(go,"utf8"),t=gf(e),{rules:n,changed:o}=yf(t);return o&&yt(n),n}catch{return[]}}function wf(e){let t=0,n=0,o=0;for(let r of e)r.enabled?r.paused?n+=1:t+=1:o+=1;return{total:e.length,active:t,paused:n,disabled:o}}function So(){let e=De();return{rules:e,summary:wf(e)}}function yt(e){B(Rt);let n=`${JSON.stringify({version:mf,rules:e},null,2)}
28
- `,o=`${go}.tmp`;Si.writeFileSync(o,n,{mode:384}),Si.renameSync(o,go)}function Ci(){return go}function un(e,t){let n=t.trim().toLowerCase();return e.find(o=>o.name===n)}function kc(e,t){return e.find(n=>n.id===t)}function Nn(e,t){let n=e.findIndex(o=>o.id===t.id);if(n>=0){let o=[...e];return o[n]=t,o}return[...e,t]}function vc(e,t){let n=t.trim().toLowerCase();return e.filter(o=>o.name!==n)}import bf from"node:os";import wt from"node:path";var kf=new Set([".env","id_rsa","id_ed25519","id_ecdsa","known_hosts","credentials.json","secrets.json",".netrc","shadow","passwd"]);function et(e){let t=wt.normalize(e),n=t.toLowerCase(),o=bf.homedir().toLowerCase();if(n.includes(`${wt.sep}.ssh${wt.sep}`)||n.endsWith(`${wt.sep}.ssh`)||n.includes(`${wt.sep}browser${wt.sep}`)&&n.includes("profile")||o&&(n===`${o}/.gnupg`||n.startsWith(`${o}/.gnupg${wt.sep}`))||n.includes(`${wt.sep}keychains${wt.sep}`))return!0;let r=wt.basename(t);return!!(kf.has(r)||r.startsWith(".env.")||r.endsWith(".pem")||r.endsWith(".key"))}var vf=["node_modules",".git",".svn",".hg","__pycache__",".cache",".next","dist","build"],Sf=[".tmp",".swp",".swx","~",".part"];function xc(e,t){let n=new Map,o=new Map;function r(u,c){let m=u.split(Sc.sep);for(let f of m)if(vf.includes(f))return!0;let h=Sc.basename(u);for(let f of Sf)if(h.endsWith(f))return!0;return!!kr(u,c)}function s(u){let c=kc(De(),u);return!c||!c.enabled||c.paused?null:c}function i(u){let c=Date.now(),m=o.get(u);return!m||c-m.windowStart>=6e4?(o.set(u,{windowStart:c,count:1}),!1):m.count>=t.maxEventsPerMinute?!0:(m.count+=1,!1)}async function a(u,c){let m=e.getConfig();if(!m.watchEnabled)return;let h=s(u.id);if(!h||c.kind==="fs"&&c.meta?.path&&(et(c.meta.path)||r(c.meta.path,h)))return;if(hc(c),(h.notifyWhen??"always")==="state-change"){if(gc(h.id)===c.stateKey)return;yc(h.id,c.stateKey,c.tsMs)}let g=bc(h.notify,h.ownerPeerKey,m);if(g.length===0)return;let y=`[watch:${h.name}] ${c.summary}`;await Promise.all(g.map(b=>e.sendToPeer(b,y).catch(()=>{})))}function l(u){let c=n.get(u);if(!c)return;n.delete(u);let m=s(c.ruleId);m&&(i(c.ruleId)||a(m,c.event))}function d(u){for(let[c,m]of n)m.ruleId===u&&(clearTimeout(m.timer),n.delete(c))}return{ingest(u,c){if(!u.enabled||u.paused||c.meta?.path&&(et(c.meta.path)||r(c.meta.path,u)))return;let m=`${u.id}:${c.stateKey||c.summary}`,h=n.get(m);h&&clearTimeout(h.timer);let f=setTimeout(()=>l(m),t.debounceMs);f.unref?.(),n.set(m,{timer:f,event:c,ruleId:u.id})},cancelForRule:d,dispose(){for(let u of n.values())clearTimeout(u.timer);n.clear(),o.clear()}}}function Cc(e,t){let n=e.toLowerCase();return!!(n==="create"&&t.has("create")||n==="delete"&&t.has("delete")||(n==="update"||n==="rename")&&(t.has("update")||t.has("rename")))}xe();import Rc from"node:fs";import xf from"node:path";import{subscribe as Cf}from"@parcel/watcher";var Rf=["**/node_modules/**","**/.git/**","**/.svn/**","**/.hg/**","**/__pycache__/**","**/.cache/**"];function Tf(e){return e==="create"?"create":e==="delete"?"delete":e==="update"?"update":e}function $f(e,t){try{let n=Rc.statSync(t),o=n.isFile()&&n.size<1024*1024?` (${n.size<1024?`${n.size} B`:`${(n.size/1024).toFixed(1)} KB`})`:"";return`fs: ${e} ${t}${o}`}catch{return`fs: ${e} ${t}`}}async function Tc(e,t){let n=e.path;if(!n||!Rc.existsSync(n))return{stop:async()=>{},status:()=>"error: path missing or not found"};if(et(n))return{stop:async()=>{},status:()=>"error: sensitive path denied"};let o=new Set(e.events.map(i=>i.toLowerCase())),r="ok",s=null;try{s=await Cf(n,(i,a)=>{if(i){r=`error: ${i.message}`,P.warn({rule:e.name,err:i.message},"watch fs adapter");return}for(let l of a){let d=Tf(l.type);if(!Cc(d,o))continue;let u=xf.resolve(l.path);if(!u.startsWith(n)||et(u)||kr(u,e))continue;let c=$f(d,u);t({ruleId:e.id,ruleName:e.name,kind:"fs",stateKey:`${d}:${u}`,summary:c,tsMs:Date.now(),meta:{path:u,type:d}})}},{ignore:[...Rf,...wc(e)]})}catch(i){r=`error: ${String(i)}`,P.warn({rule:e.name,err:String(i)},"watch fs subscribe failed")}return{stop:async()=>{s&&(await s.unsubscribe().catch(()=>{}),s=null)},status:()=>r}}import Nf from"node:os";xe();import jt from"node:fs";function Mf(e,t,n){let o=n?.pollMs??2e3,r=0,s=!1,i=null,a=null,l="starting";function d(){if(!s)try{let u=jt.statSync(e);if(u.size<r&&(r=0),u.size<=r)return;let c=jt.openSync(e,"r");try{let m=u.size-r,h=Buffer.alloc(m);jt.readSync(c,h,0,m,r),r=u.size;let f=h.toString("utf8");for(let g of f.split(`
29
- `)){let y=g.trim();y&&t(y)}}finally{jt.closeSync(c)}l="ok"}catch(u){l=`error: ${String(u)}`}}try{jt.existsSync(e)&&(r=jt.statSync(e).size),i=jt.watch(e,()=>d()),i.on("error",()=>{i?.close(),i=null}),l="ok (watch)"}catch{l="ok (poll)",a=setInterval(d,o),a.unref?.(),d()}return{stop(){s=!0,i?.close(),a&&clearInterval(a)},status:()=>l}}function xr(e,t){for(let n of e)try{if(jt.existsSync(n))return Mf(n,t)}catch(o){P.debug({path:n,err:String(o)},"watch log-tail skip path")}return null}var Pf=["/var/log/install.log"];function Ef(e){let t=e.toLowerCase();return t.includes("installed")||t.includes("upgraded")||t.includes("removed")?e.length>240?`pkg: ${e.slice(0,240)}\u2026`:`pkg: ${e}`:null}function $c(e,t){return xr(Pf,n=>{let o=Ef(n);o&&t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:o.slice(0,200),summary:o,tsMs:Date.now()})})}var Af=["/var/log/dpkg.log","/var/log/apt/history.log"];function If(e){if(e.includes("status installed")){let t=e.match(/install\s+(\S+):/);if(t)return`pkg: installed ${t[1]} (dpkg)`}if(e.includes("status removed")||e.includes("remove ")){let t=e.match(/remove\s+(\S+):/);if(t)return`pkg: removed ${t[1]} (dpkg)`}return null}function Lf(e){return e.startsWith("Install:")||e.startsWith("Upgrade:")?`pkg: ${e.slice(0,120)} (apt)`:e.startsWith("Remove:")?`pkg: ${e.slice(0,120)} (apt)`:null}function Mc(e,t){return xr(Af,n=>{let o=If(n)??Lf(n);o&&t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:o,summary:o,tsMs:Date.now()})})}import{spawn as Of}from"node:child_process";function Pc(e,t){let n=!1,o="ok",r=0,s=()=>{if(n)return;let a=Of("powershell.exe",["-NoProfile","-NonInteractive","-Command","Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName=@('MsiInstaller','Windows Installer')} -MaxEvents 5 -ErrorAction SilentlyContinue | Select-Object -Property RecordId,Message | ConvertTo-Json -Compress"],{windowsHide:!0}),l="";a.stdout?.on("data",d=>{l+=String(d)}),a.on("close",d=>{if(n||d!==0||!l.trim()){d!==0&&(o="ok (no events or access denied)"),i();return}try{let u=JSON.parse(l),c=Array.isArray(u)?u:[u];for(let m of c.sort((h,f)=>h.RecordId-f.RecordId)){if(m.RecordId<=r)continue;r=m.RecordId;let h=(m.Message??"").replace(/\s+/g," ").trim().slice(0,300);if(!h)continue;let f=`pkg: ${h}`;t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:`win:${m.RecordId}`,summary:f,tsMs:Date.now()})}o="ok"}catch(u){o=`parse error: ${String(u)}`}i()}),a.on("error",d=>{o=`error: ${String(d)}`,i()})},i=()=>{n||setTimeout(s,3e4).unref?.()};return s(),{stop(){n=!0},status:()=>o}}function Ec(e,t){let n=Nf.platform();if(n==="win32"){let r=Pc(e,t);return{stop:()=>r.stop(),status:r.status}}if(n==="darwin"){let r=$c(e,t);return r?{stop:()=>r.stop(),status:r.status}:{stop:()=>{},status:()=>"error: cannot read /var/log/install.log (try sudo or adm group)"}}let o=Mc(e,t);return o?{stop:()=>o.stop(),status:o.status}:{stop:()=>{},status:()=>"error: cannot read dpkg/apt logs (add user to adm or run gateway with read access)"}}import jf from"node:os";import{spawnSync as Ac}from"node:child_process";var Ff=3e4;function _f(e){let t=new Map;for(let n of e){let o=Ac("launchctl",["print",`system/${n}`],{encoding:"utf8",timeout:1e4});if(o.status!==0){let s=process.getuid?.()??501,i=Ac("launchctl",["print",`gui/${s}/${n}`],{encoding:"utf8",timeout:1e4});if(i.status!==0){t.set(n,"not-found");continue}let a=(i.stdout??"").includes("state = running")?"running":"stopped";t.set(n,a);continue}let r=(o.stdout??"").includes("state = running")?"running":"stopped";t.set(n,r)}return t}function Ic(e,t){let n=e.units,o=!1,r="ok",s=new Map,i=()=>{if(o)return;let a=_f(n);if(a.size===0)r="error: no launchd labels configured";else{r="ok";for(let[l,d]of a){let u=s.get(l);if(u!==void 0&&u!==d){let c=`svc: ${l} ${u} \u2192 ${d}`;t({ruleId:e.id,ruleName:e.name,kind:"svc",stateKey:`${l}:${d}`,summary:c,tsMs:Date.now(),meta:{unit:l,state:d}})}}s=a}o||setTimeout(i,Ff).unref?.()};return i(),{stop(){o=!0},status:()=>r}}import{spawnSync as Lc}from"node:child_process";var Wf=3e4;function Df(e){let t=new Map;if(e.length===0)return t;let n=["show",...e,"--property=ActiveState,SubState,UnitFileState","--no-pager"],o=Lc("systemctl",n,{encoding:"utf8",timeout:15e3});if(o.status!==0)return t;let r="";for(let s of(o.stdout??"").split(`
30
- `)){let i=s.match(/^Unit=(.+)$/);if(i){r=i[1];continue}let a=s.match(/^ActiveState=(.+)$/);a&&r&&t.set(r,a[1].trim())}if(t.size===0)for(let s of e){let i=s.endsWith(".service")?s:`${s}.service`,a=Lc("systemctl",["is-active",i],{encoding:"utf8",timeout:5e3}),l=(a.stdout??a.stderr??"unknown").trim();t.set(i,l)}return t}function Oc(e,t){let n=e.units.map(a=>a.endsWith(".service")?a:`${a}.service`),o=!1,r="ok",s=new Map,i=()=>{if(o)return;let a=Df(n);if(a.size===0)r="error: systemctl unavailable or units not found";else{r="ok";for(let[l,d]of a){let u=s.get(l);if(u!==void 0&&u!==d){let c=`svc: ${l} ${u} \u2192 ${d}`;t({ruleId:e.id,ruleName:e.name,kind:"svc",stateKey:`${l}:${d}`,summary:c,tsMs:Date.now(),meta:{unit:l,state:d}})}}s=a}o||setTimeout(i,Wf).unref?.()};return i(),{stop(){o=!0},status:()=>r}}import{spawnSync as Uf}from"node:child_process";var Bf=3e4;function Hf(e){let t=new Map;for(let n of e){let o=Uf("sc",["query",n],{encoding:"utf8",timeout:1e4,windowsHide:!0});if(o.status!==0){t.set(n,"not-found");continue}let r=o.stdout??"",s="unknown";r.includes("RUNNING")?s="running":r.includes("STOPPED")?s="stopped":r.includes("START_PENDING")?s="start-pending":r.includes("STOP_PENDING")&&(s="stop-pending"),t.set(n,s)}return t}function Nc(e,t){let n=e.units,o=!1,r="ok",s=new Map,i=()=>{if(o)return;let a=Hf(n);if(a.size===0)r="error: no service names configured";else{r="ok";for(let[l,d]of a){let u=s.get(l);if(u!==void 0&&u!==d){let c=`svc: ${l} ${u} \u2192 ${d}`;t({ruleId:e.id,ruleName:e.name,kind:"svc",stateKey:`${l}:${d}`,summary:c,tsMs:Date.now(),meta:{unit:l,state:d}})}}s=a}o||setTimeout(i,Bf).unref?.()};return i(),{stop(){o=!0},status:()=>r}}function Fc(e,t){let n=jf.platform();return n==="win32"?Nc(e,t):n==="darwin"?Ic(e,t):Oc(e,t)}var tt=null,Gt=null;function _c(e){let t=e.getConfig();return xc(e,{debounceMs:Math.max(500,t.watchDebounceMs??2e3),maxEventsPerMinute:Math.max(1,t.watchMaxEventsPerMinute??30)})}function Jf(e){return e.watchEnabled&&e.watchAutoRestore}var Ri=class{deps;pipeline;runtimes=new Map;stopped=!1;constructor(t){this.deps=t,this.pipeline=_c(t)}onEvent=(t,n)=>{this.pipeline.ingest(t,n)};cancelPendingForRule(t){this.pipeline.cancelForRule(t)}async reload(){if(this.pipeline.dispose(),this.pipeline=_c(this.deps),await this.stopAdapters(),this.stopped||!this.deps.getConfig().watchEnabled)return 0;let n=De().filter(o=>o.enabled&&!o.paused);for(let o of n)await this.startRule(o);return this.runtimes.size}async startRule(t){try{let n;if(t.kind==="fs"){let o=Qe(t.path,Gf.homedir()),r={...t,path:o};if(et(o)){t.adapterStatus="denied: sensitive path",this.persistRuleStatus(t);return}if(!qf.existsSync(o)){t.adapterStatus="error: path not found",this.persistRuleStatus(t);return}let s=await Tc(r,i=>this.onEvent(r,i));n={stop:()=>s.stop(),status:s.status}}else if(t.kind==="pkg")n=Ec(t,r=>this.onEvent(t,r));else if(t.kind==="svc"){if(t.units.length===0){t.adapterStatus="error: no units",this.persistRuleStatus(t);return}n=Fc(t,r=>this.onEvent(t,r))}else return;t.adapterStatus=n.status(),this.persistRuleStatus(t),this.runtimes.set(t.id,{rule:t,adapter:n})}catch(n){t.adapterStatus=`error: ${String(n)}`,this.persistRuleStatus(t),P.warn({rule:t.name,err:String(n)},"watch rule start failed")}}persistRuleStatus(t){let n=De(),o=n.findIndex(r=>r.id===t.id);o>=0&&(n[o]={...n[o],adapterStatus:t.adapterStatus},yt(n))}async stopAdapters(){for(let t of this.runtimes.values())try{await t.adapter.stop()}catch{}this.runtimes.clear()}async stop(){this.stopped=!0,await this.stopAdapters(),this.pipeline.dispose(),mc()}getStatusLines(){let t=De();return t.length===0?["(no watch rules)"]:t.map(n=>{let r=this.runtimes.get(n.id)?.adapter.status()??n.adapterStatus,s=n.enabled?n.paused?"paused":"on":"disabled",i=n.kind==="fs"?n.path:n.kind==="svc"?n.units.join(","):"system",a=n.kind==="fs"&&(n.excludePaths.length||n.excludeGlobs.length)?` excludes:${n.excludePaths.length+n.excludeGlobs.length}`:"";return`${n.name} [${n.kind}] ${s} notify=${n.notify} when=${n.notifyWhen} \u2014 ${i}${a} \u2014 ${r}`})}getRunningCount(){return this.runtimes.size}isRuleRunning(t){return this.runtimes.has(t)}};function Wc(){return tt}function Ti(e){tt?.cancelPendingForRule(e)}function zf(){B(Rt),br()}function $i(){Gt&&(tt||(tt=new Ri(Gt)),tt.reload())}function Cr(e){Gt=e,zf();let t=e.getConfig(),{summary:n}=So();return n.total>0&&P.info({rules:n.total,active:n.active,paused:n.paused,disabled:n.disabled},"watch rules loaded from disk"),Jf(t)&&$i(),()=>{tt?.stop(),tt=null,Gt=null}}async function Dc(){return Gt?Gt.getConfig().watchEnabled?($i(),tt?.getRunningCount()??0):(tt?.stop(),tt=null,-1):-1}function qe(){if(!Gt)return;if(!Gt.getConfig().watchEnabled){tt?.stop(),tt=null;return}$i()}G();pe();G();import Qf from"node:os";import Tr from"node:path";G();import Mi from"node:fs";import Bc from"node:os";import qt from"node:path";var Hc=qt.join(W,"sessions.json"),Fn=new Map;function jc(){return{cwd:qt.resolve(Bc.homedir())}}function Kf(e){return e.startsWith("wa:")||e.startsWith("tg:")?e:`wa:${e}`}function Yf(){try{let e=Mi.readFileSync(Hc,"utf8"),t=JSON.parse(e);for(let[n,o]of Object.entries(t)){if(!o||typeof o!="object")continue;let r=Kf(n),i={cwd:typeof o.cwd=="string"&&o.cwd.length>0?qt.resolve(o.cwd):jc().cwd};o.fileReceiveRoot==="sessionCwd"&&(i.fileReceiveRoot="sessionCwd"),Fn.set(r,i)}}catch{}}function Gc(){B(W);let e={};for(let[t,n]of Fn){let o={cwd:n.cwd};n.fileReceiveRoot==="sessionCwd"&&(o.fileReceiveRoot="sessionCwd"),e[t]=o}Mi.writeFileSync(Hc,JSON.stringify(e,null,2)+`
31
- `,{mode:384})}var Uc=!1;function Pi(){Uc||(Yf(),Uc=!0)}function ie(e){Pi();let t=Fn.get(e);return t||(t=jc(),Fn.set(e,t)),t}function Rr(e,t){Pi();let n=qt.resolve(t),o=ie(e);o.cwd=n,Fn.set(e,o),Gc()}function qc(e){return ie(e).fileReceiveRoot==="sessionCwd"?"sessionCwd":"default"}function Ei(e,t){Pi();let n=ie(e);t==="default"?delete n.fileReceiveRoot:n.fileReceiveRoot="sessionCwd",Fn.set(e,n),Gc()}function Jc(e){let t=e.trim();if(/[&|;\n]/.test(t)||!t.startsWith("cd"))return null;if(t==="cd")return{kind:"home"};if(t.length>2&&t[2]!==" "&&t[2]!==" ")return null;let n=t.slice(2).trimStart();return n?{kind:"path",value:n}:{kind:"home"}}function zc(e,t){if(t.kind==="home")return qt.resolve(Bc.homedir());let n=t.value.replace(/^['"]|['"]$/g,"");return qt.isAbsolute(n)?qt.normalize(n):qt.resolve(e,n)}function Kc(e){try{return Mi.statSync(e).isDirectory()?{ok:!0}:{ok:!1,error:`Not a directory: ${e}`}}catch(t){return{ok:!1,error:String(t)}}}var Vf="Omnish";function Yc(){return Tr.join(Qf.homedir(),"Downloads",Vf)}function Jt(e,t){let n=ie(t);if(n.fileReceiveRoot==="sessionCwd")return n.cwd;switch(e.fileReceiveRootMode){case"downloads":return Yc();case"omnishData":return Tr.join(W,e.fileInboxSubdir);case"sessionCwd":return n.cwd;case"processCwd":return process.cwd();case"fixed":{let o=(e.fileReceiveRootPath??"").trim();if(!o)throw new Error('fileReceiveRootPath is required when fileReceiveRootMode is "fixed".');if(!Tr.isAbsolute(o))throw new Error('fileReceiveRootPath must be an absolute path when fileReceiveRootMode is "fixed".');return Tr.resolve(o)}default:return Yc()}}G();import Et from"node:fs";import Vc from"node:path";import Qt from"node:process";var dn="\x1B";function Xf(e){return e.replace(/\u001B\[[\d;]*m/g,"")}function $r(e){return Xf(e).length}function Ai(e,t,n,o,r=2){let s=Math.max(0,t-$r(n));return`${e}${n}${" ".repeat(s)}${" ".repeat(r)}${o}`}function $t(e,t,n,o){if(n.length===0)return[];let r=n.map(i=>o(i.left)),s=Math.max(...r.map($r));return n.map((i,a)=>Ai(t,s,r[a],v(e,i.right)))}var zt={primary:"#b4ff24",foreground:"#eef2f4",muted:"#8a9199",border:"#2a3139",error:"#ef4444",warn:"#a0e614",warnAlt:"#8cce04"};function Zf(e){let t=e.replace(/^#/,"");return t.length!==6||!/^[0-9a-fA-F]+$/.test(t)?null:{r:parseInt(t.slice(0,2),16),g:parseInt(t.slice(2,4),16),b:parseInt(t.slice(4,6),16)}}function Kt(e){let t=Zf(e);return t?`${dn}[38;2;${t.r};${t.g};${t.b}m`:""}function Mr(e){if(process.env.NO_COLOR!==void 0&&process.env.NO_COLOR!=="")return!1;let t=process.env.FORCE_COLOR;return t==="1"||t==="true"?!0:t==="0"||t==="false"?!1:e.isTTY===!0}function Mt(e,t,n){return!t||!Mr(e)?n:`${t}${n}${dn}[0m`}function q(e,t){return Mt(e,`${dn}[1m`,t)}function eg(e,t){return Mr(e)?`${Kt(zt.foreground)}${dn}[1m${t}${dn}[0m`:t}function Q(e,t){return Mt(e,`${dn}[2m`,t)}function Ce(e,t){return Mt(e,`${Kt(zt.primary)}${dn}[1m`,t)}function me(e,t){return Mt(e,Kt(zt.primary),t)}function v(e,t){return Mt(e,Kt(zt.foreground),t)}function w(e,t){return Mt(e,Kt(zt.muted),t)}function tg(e,t){return Mt(e,Kt(zt.border),t)}function Pr(e,t){return Mt(e,Kt(zt.error),t)}function we(e,t){return Mt(e,Kt(zt.warn),t)}function Yt(e,t=60,n="\u2500"){let o=n.repeat(Math.max(1,t));return tg(e,o)}function D(e,t){return Mr(e)?`${`${w(e,"[")}${me(e,"omnish")}${w(e,"]")}`} ${t}`:`[omnish] ${t}`}function T(e,t){return Mr(e)?`${`${Pr(e,"[omnish]")}`} ${t}`:`[omnish] ${t}`}function Qc(e,t){let n=[];for(let o of e)switch(o.kind){case"title":n.push("",Ce(t,o.text),"");break;case"sub":n.push("",eg(t,o.text),"");break;case"gap":n.push("");break;case"p":n.push(v(t,o.text));break;case"bullet":n.push(`${w(t,"\u2022")} ${v(t,o.text)}`);break}return n.join(`
32
- `).replace(/^\n+/,"").trimEnd()}pe();G();pn();mn();function Ar(e){return(e&4)!==0}function Oi(e){return(e&2)!==0}var Xc={error:3,warn:2,info:1};function Zc(e,t){let n=Xc[t];return e.filter(o=>Xc[o.severity]>=n)}function At(e,t={}){let n=[],o=t.gatewayMode??e.gatewayMode,r=o==="whatsapp"||o==="both",s=o==="telegram"||o==="both";if(e.allowFrom.includes("*")&&n.push({severity:"error",code:"allow-wildcard",message:'allowFrom contains "*" \u2014 this must never be used; remove it from config immediately.',detail:`Edit ${_} and delete wildcard entries.`,fixHint:`Edit ${_} and remove any "*" entries from allowFrom.`}),r&&e.allowFrom.length===0&&n.push({severity:"warn",code:"allow-wa-empty",message:"allowFrom is empty \u2014 no WhatsApp identity can run commands.",detail:"Add your number: omnish allow +<E164>",fixHint:"omnish allow +<your_E164_number>"}),s&&e.telegramAllowFrom.length===0&&n.push({severity:"warn",code:"allow-tg-empty",message:"telegramAllowFrom is empty \u2014 no Telegram user can run commands.",detail:"Add your user id: omnish allow tg:<id>",fixHint:"omnish allow tg:<your_telegram_user_id>"}),e.recipesAllowDangerousBuiltins&&n.push({severity:"warn",code:"recipes-dangerous",message:"recipesAllowDangerousBuiltins is enabled \u2014 built-in recipe helpers may add permissive Claude Code flags.",fixHint:`Set recipesAllowDangerousBuiltins to false in ${_} unless you trust every recipe.`}),e.serviceInstallFromChat&&n.push({severity:"warn",code:"service-install-from-chat",message:"serviceInstallFromChat is enabled \u2014 allowlisted users can run /service install and write user-level systemd or LaunchAgent units.",fixHint:`Set serviceInstallFromChat to false in ${_} unless you trust every allowlisted identity.`}),e.pullInstallFromChat&&n.push({severity:"warn",code:"pull-install-from-chat",message:"pullInstallFromChat is enabled \u2014 allowlisted users can run /pull install and download binaries into the omnish data directory.",fixHint:`Set pullInstallFromChat to false in ${_} unless you trust every allowlisted identity.`}),!Vc.isAbsolute(e.shell))n.push({severity:"warn",code:"shell-not-absolute",message:`Shell path is not absolute: ${e.shell}`,fixHint:`Set "shell" to an absolute path (e.g. /bin/bash) in ${_}.`});else try{Et.existsSync(e.shell)||n.push({severity:"error",code:"shell-missing",message:`Configured shell does not exist: ${e.shell}`,fixHint:`Install the shell or update "shell" in ${_} to a valid binary.`})}catch{n.push({severity:"warn",code:"shell-stat-failed",message:`Could not verify shell path: ${e.shell}`,fixHint:`Check permissions and that "shell" in ${_} points to a real file.`})}if(e.fileReceiveRootMode==="fixed"){let a=e.fileReceiveRootPath.trim();a?Vc.isAbsolute(a)||n.push({severity:"error",code:"receive-fixed-not-absolute",message:`fileReceiveRootPath must be absolute when fileReceiveRootMode is "fixed": ${a}`,fixHint:`Use an absolute path for fileReceiveRootPath in ${_}.`}):n.push({severity:"error",code:"receive-fixed-empty",message:'fileReceiveRootMode is "fixed" but fileReceiveRootPath is empty.',fixHint:`Set fileReceiveRootPath to an absolute directory in ${_}, or change fileReceiveRootMode.`})}if(Qt.platform!=="win32"){try{if(Et.existsSync(_)){let d=Et.statSync(_);(Ar(d.mode)||Oi(d.mode))&&n.push({severity:"warn",code:"config-permissions",message:"config.json is accessible to users other than the owner (group/world).",detail:`Recommended: chmod 600 ${_}`,fixHint:`chmod 600 ${_}`})}}catch{}(typeof Qt.getuid=="function"?Qt.getuid():null)===0&&n.push({severity:"warn",code:"run-as-root",message:"Process is running as root \u2014 allowlisted remote access has full system privileges.",fixHint:"Run omnish as a non-root user (systemd user service, container user, etc.)."});let l=!1;try{if(Et.existsSync(W)){let d=Et.statSync(W);(Ar(d.mode)||Oi(d.mode))&&(l=!0,n.push({severity:"warn",code:"data-dir-permissive",message:"Omnish data directory is accessible to group or other users \u2014 auth, jobs, and logs may be exposed.",detail:`Recommended: chmod 700 ${W}`,fixHint:`chmod 700 ${W}`}))}}catch{}try{if(!l&&Et.existsSync(it)){let d=Et.statSync(it);(Ar(d.mode)||Oi(d.mode))&&n.push({severity:"warn",code:"jobs-dir-permissive",message:"Jobs log directory is accessible to group or other users \u2014 command output may leak.",detail:`Recommended: chmod 700 ${it}`,fixHint:`chmod 700 ${it}`})}}catch{}try{if(Et.existsSync(le)){let d=Et.statSync(le);Ar(d.mode)&&n.push({severity:"warn",code:"auth-dir-world-readable",message:"WhatsApp auth directory is readable by others \u2014 session material may be exposed.",detail:`Recommended: chmod 700 ${le}`,fixHint:`chmod 700 ${le}`})}}catch{}}return(typeof Qt.env.TELEGRAM_BOT_TOKEN=="string"?Qt.env.TELEGRAM_BOT_TOKEN.trim():"")&&n.push({severity:"info",code:"telegram-token-env",message:"TELEGRAM_BOT_TOKEN is set in the environment; it overrides config.json until unset.",fixHint:"Unset TELEGRAM_BOT_TOKEN in the shell/service env if you want config.json to apply."}),s&&!Me(e)&&n.push({severity:"error",code:"telegram-no-token",message:"Telegram is enabled for this gateway but no bot token is configured.",detail:"Set telegramBotToken in config or TELEGRAM_BOT_TOKEN.",fixHint:`Set telegramBotToken in ${_} or export TELEGRAM_BOT_TOKEN=<token>.`}),e.clusterEnabled&&s&&n.push({severity:"info",code:"cluster-telegram-tokens",message:"Cluster mode with Telegram: use a distinct BotFather bot/token per omnish host if several gateways run Telegram \u2014 the same token cannot be long-polled twice.",fixHint:"Create separate bots for each machine or run Telegram on fewer hosts."}),e.platformToken.trim()&&n.push({severity:"info",code:"platform-token-config",message:"platformToken is stored in config.json (same trust boundary as your shell account).",fixHint:"Use OMNISH_TOKEN in the environment for CI/ephemeral hosts; env overrides config."}),(Qt.env.OMNISH_TOKEN?.trim()||Qt.env.OMNISH_TUNNEL_TOKEN?.trim()||Qt.env.OMNISH_DEVICE_TOKEN?.trim())&&n.push({severity:"info",code:"platform-token-env",message:"OMNISH_TOKEN (or OMNISH_TUNNEL_TOKEN / OMNISH_DEVICE_TOKEN) is set in the environment.",fixHint:"Unset platform token env vars if you want config.json / tunnel-auth.json to apply."}),e.tunnelEnabled&&(n.push({severity:"warn",code:"tunnel-enabled",message:"Chat tunneling is enabled \u2014 allowlisted users can publish public URLs to local HTTP/TCP services.",fixHint:"Disable tunnelEnabled or restrict allowlists if you do not want remote tunnel control."}),kt()||n.push({severity:"warn",code:"tunnel-no-token",message:"Chat tunneling is enabled but no tunnel token is configured.",fixHint:"Run `omnish tunnel login` or set OMNISH_TUNNEL_TOKEN for the gateway process."})),e.tunnelRelayUrl.trim()&&e.tunnelRelayUrl.trim()!==Ee&&n.push({severity:"info",code:"tunnel-relay-custom",message:`Custom tunnel relay configured: ${e.tunnelRelayUrl.trim()}`,fixHint:"Use the default relay only if you trust the operator of that endpoint."}),n}function xo(e){return e.some(t=>t.severity==="error")}function og(e,t){switch(t){case"error":return Pr(e,"[ERROR]");case"warn":return we(e,"[WARN]");case"info":return w(e,"[INFO]")}}function Ni(e,t,n,o){if(o.length!==0){t.push(q(e,`${n} (${o.length}):`));for(let r of o)t.push(` ${og(e,r.severity)} ${w(e,`${r.code}:`)} ${v(e,r.message)}`),r.detail&&t.push(` ${w(e,r.detail)}`),r.fixHint&&t.push(` ${w(e,`Fix: ${r.fixHint}`)}`);t.push("")}}function Fi(e,t){if(e.length===0)return[`${Ce(t,"Security check:")} ${v(t,"no issues reported by automated rules.")}`,"",w(t,"Note: allowlisted remote shell access is still equivalent to sharing credentials with those identities.")].join(`
33
- `);let n=e.filter(a=>a.severity==="error"),o=e.filter(a=>a.severity==="warn"),r=e.filter(a=>a.severity==="info"),i=[`${Ce(t,"Security check:")} `+v(t,`${n.length} error(s), ${o.length} warning(s), ${r.length} note(s).`),""];return Ni(t,i,"Errors",n),Ni(t,i,"Warnings",o),Ni(t,i,"Notes",r),i.push(w(t,"Allowlisted identities can run commands as this user; treat them like passwords.")),i.join(`
34
- `).trimEnd()}function _i(e){return{errors:e.filter(t=>t.severity==="error").length,warns:e.filter(t=>t.severity==="warn").length,infos:e.filter(t=>t.severity==="info").length}}function eu(e){return`${JSON.stringify({findings:e,summary:_i(e)},null,2)}
35
- `}function tu(e,t){let n=e.filter(a=>a.severity==="error").length,o=e.filter(a=>a.severity==="warn").length,r=e.filter(a=>a.severity==="info").length;if(n===0&&o===0&&r===0)return"security: ok (no automated findings)";let s=[];n&&s.push(`${n} error(s)`),o&&s.push(`${o} warning(s)`),r&&s.push(`${r} note(s)`);let i=t??"run `omnish security` for details";return`security: ${s.join(", ")} \u2014 ${i}`}function nu(e,t,n){let o=e.filter(l=>l.severity==="error").length,r=e.filter(l=>l.severity==="warn").length,s=e.filter(l=>l.severity==="info").length;if(o===0&&r===0&&s===0)return`${Ce(t,"security:")} ${v(t,"ok (no automated findings)")}`;let i=[];o&&i.push(Pr(t,`${o} error(s)`)),r&&i.push(we(t,`${r} warning(s)`)),s&&i.push(w(t,`${s} note(s)`));let a=n??"run `omnish security` for details";return`${Ce(t,"security:")} ${i.join(", ")} ${w(t,`\u2014 ${a}`)}`}var Ae="- ";function p(e){return{wa:e,tg:e}}function fe(e,t){return{wa:e,tg:t,tgHtml:!0}}function Te(e,t){return t==="whatsapp"?{text:e.wa}:e.tgHtml?{text:e.tg,parseModeHtml:!0}:{text:e.tg}}function V(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function Re(e){return e.replace(/[*_~`]/g,t=>({"*":"\u2217",_:"\uFF3F","~":"\u02DC","`":"\u2032"})[t]??t)}function rg(e){let t=[];for(let n of e)switch(n.kind){case"title":t.push("",`*${n.text}*`,"");break;case"sub":t.push("",`_${n.text}_`,"");break;case"gap":t.push("");break;case"p":t.push(n.text);break;case"bullet":t.push(`${Ae}${n.text}`);break}return t.join(`
36
- `).replace(/^\n+/,"").trimEnd()}function sg(e){let t=[];for(let n of e)switch(n.kind){case"title":t.push("",`<b>${V(n.text)}</b>`,"");break;case"sub":t.push("",`<i>${V(n.text)}</i>`,"");break;case"gap":t.push("");break;case"p":t.push(V(n.text));break;case"bullet":t.push(`\u2022 ${V(n.text)}`);break}return t.join(`
37
- `).replace(/^\n+/,"").trimEnd()}function X(e){return{wa:rg(e),tg:sg(e),tgHtml:!0}}function _n(e){return[{kind:"title",text:"Omnish \u2014 quick help"},{kind:"p",text:"Per-chat shell cwd is stored under your data dir (see /wa help)."},{kind:"gap"},{kind:"sub",text:"Run commands"},{kind:"bullet",text:`${e.commandPrefix}<command> \u2014 sync shell in session cwd (timeout ${e.syncTimeoutMs} ms)`},{kind:"bullet",text:`${e.commandPrefix}cd <dir> \u2014 change session cwd (${e.commandPrefix}cd alone \u2192 home)`},{kind:"bullet",text:"!!start | !!stop \u2014 free shell (plain \u2192 sync shell only when no focused /apps session); space optional"},{kind:"bullet",text:"/bg <cmd> \u2014 background job; optional -n name; /jobs, /log, /tail, /kill (id or name)"},{kind:"bullet",text:e.tunnelEnabled?"/tunnel login|logout|status|http|tcp \u2014 login/status anytime; /tunnels needs tunnelEnabled":"/tunnel expose/list off until tunnelEnabled true (login/status still work)"},{kind:"bullet",text:"/apps \u2026 \u2014 interactive app sessions (/apps help)"},{kind:"bullet",text:"/send selectors \u2014 host files \u2192 chat (/file); caption: selectors -- note"},{kind:"bullet",text:"/files \u2014 full help for /send and saving inbound media"},{kind:"bullet",text:"/receive \u2014 set this chat\u2019s inbound folder (e.g. session cwd)"},{kind:"bullet",text:"/computers \xB7 /pcs \xB7 /c \u2014 chat-driven cluster (/c here to take over, /c help)"},{kind:"bullet",text:"/config \u2014 view or change server settings (/config help, /config keys)"},{kind:"bullet",text:"/service \u2014 background service status, install hints, logs (/service help)"},{kind:"bullet",text:e.pullEnabled?"/pull \u2014 download from URL (video/audio/subs/transcript); /pull help":"/pull \u2014 off until pullEnabled true (/pull doctor still works)"},{kind:"bullet",text:"/shortcut \u2014 this chat & shared shortcuts (/shortcut help); !name or /name expands chat override first, then shared"},{kind:"bullet",text:"/run \u2014 recipe-based task runs (/r); per-chat or gateway-shared /run add \u2014 /run list, /run help"},{kind:"bullet",text:"/cowork | /cw \u2014 scheduled & on-demand shell tasks; logs to ~/Cowork/\u2026 (/cowork help)"},{kind:"bullet",text:"/watch \u2014 OS event eye: fs/pkg/svc alerts; pause/stop/rm, excludes (/watch help; docs/features/watch.md)"},{kind:"gap"},{kind:"sub",text:"Setup & gateway"},{kind:"bullet",text:"/wa help \u2014 WhatsApp link, allowlist"},{kind:"bullet",text:"/tg help \u2014 Telegram; /tg token <paste>"},{kind:"bullet",text:"/reload | /restart \u2014 apply config"},{kind:"bullet",text:"/updates \u2014 npm latest + optional notice URL; /updates cached \u2014 last snapshot"},{kind:"bullet",text:"/security \u2014 posture report; /security summary; /security tips; /security help"},{kind:"bullet",text:"/gateway | /gw | /mode \u2014 show or set gatewayMode"},{kind:"bullet",text:"/allow +E164 | /allow tg:id \u2014 allowlist; /deny \u2026; /allowlist"},{kind:"bullet",text:"/docs search <topic> \u2014 find guides and next commands (/docs help)"},{kind:"bullet",text:"/help \u2014 this message"},{kind:"gap"},{kind:"sub",text:"Terms"},{kind:"bullet",text:"Gateway = omnish on this host; shell runs here. Standalone = messengers on this host; attached = communication layer routes chat here."},{kind:"bullet",text:'Communication layer (when shipped) = link chats once, device token on this CLI; shell stays here. Standalone = no layer. /service "platform" = OS.'}]}function Wi(e){let t=e.pullInstallFromChat?[{kind:"bullet",text:"/pull install [--whisper] \u2014 download yt-dlp/ffmpeg into ~/.omnish/bin"}]:[{kind:"bullet",text:"/pull install \u2014 off from chat. Host: omnish pull install \xB7 or /config set pullInstallFromChat true"}];return[{kind:"title",text:"Media pull (/pull)"},{kind:"p",text:"Download from a URL with yt-dlp (+ ffmpeg for video; optional Whisper for transcript). Enable: /config set pullEnabled true"},{kind:"bullet",text:"/pull doctor \u2014 tool status"},{kind:"bullet",text:"/pull setup \u2014 OS install instructions"},...t,{kind:"bullet",text:"/pull video|audio|subs|transcript|all <url>"},{kind:"bullet",text:"Long jobs: transcript/all run in /bg; add --notify for completion message"},{kind:"bullet",text:"Flags: --bg \xB7 --notify (or -N)"},{kind:"bullet",text:e.pullUrlAutoDetect?`Lone URL in chat \u2192 /pull ${e.pullDefaultMode}`:"pullUrlAutoDetect off \u2014 use /pull <mode> <url>"},{kind:"bullet",text:e.pullAutoSend?"pullAutoSend on \u2014 files under fileSendMaxBytes are sent back to chat":"pullAutoSend off \u2014 reply lists paths; use /send"},{kind:"gap"},{kind:"p",text:"Host CLI: omnish pull doctor \xB7 omnish pull install [--whisper]"},{kind:"p",text:"docs/features/media-pull.md"}]}function ou(e){let t=e.serviceInstallFromChat?[{kind:"bullet",text:"/service install \u2014 user-level systemd (Linux) or LaunchAgent (macOS)"},{kind:"bullet",text:"/service uninstall \u2014 remove that unit"}]:[{kind:"bullet",text:"/service install / uninstall \u2014 off by default. Enable: /config set serviceInstallFromChat true (same trust as shell)"}];return[{kind:"title",text:"Service and boot"},{kind:"p",text:"Inspect the gateway, get OS-specific install steps, or (if enabled) write the user service from chat. Same from the host terminal: omnish service help."},{kind:"p",text:'"platform" in status means your OS (Linux/macOS/Windows), not a hosted omnish account.'},{kind:"bullet",text:"/service status \u2014 platform, data dir, pidfile, node + entry script"},{kind:"bullet",text:"/service instructions \u2014 copy-paste commands for this machine"},{kind:"bullet",text:"/service logs [n] \u2014 tail default gateway log (default 80 lines)"},...t,{kind:"gap"},{kind:"p",text:"See https://omnish.dev and docs/guides/background-and-boot.md."}]}function ru(){return[{kind:"title",text:"Chat config"},{kind:"p",text:`Same trust as shell \u2014 allowlisted senders can change many keys saved to ${_}.`},{kind:"gap"},{kind:"bullet",text:"/config show \u2014 full snapshot (telegramBotToken masked)"},{kind:"bullet",text:"/config get <key> \u2014 single field"},{kind:"bullet",text:"/config set <key> <value> \u2014 values can be quoted for paths with spaces"},{kind:"bullet",text:"/config keys \u2014 allowed keys for /config set (includes tunnel*, webhook*, watch*, pull*, chatLlm*, cluster*, \u2026)"},{kind:"gap"},{kind:"p",text:"After edits: /reload while omnish run is active (needed for gatewayMode and telegramBotToken)."}]}function Di(){return[{kind:"title",text:"Gateway mode"},{kind:"p",text:"Which transports omnish uses:"},{kind:"bullet",text:"whatsapp (wa, w) \u2014 WhatsApp DMs only"},{kind:"bullet",text:"telegram (tg, t) \u2014 Telegram bot only"},{kind:"bullet",text:"both (all, b) \u2014 WhatsApp + Telegram"},{kind:"gap"},{kind:"sub",text:"Commands"},{kind:"bullet",text:"/gateway \u2014 current mode + hints (includes last update snapshot if any)"},{kind:"bullet",text:"/gateway telegram \u2014 save (auto-reloads Telegram if gateway is up)"},{kind:"bullet",text:"/gw both \u2014 short form; /mode wa \u2014 same"},{kind:"gap"},{kind:"p",text:`Config: ${_}`}]}function su(e){let t=["*Gateway status*","",`Mode: *${e.gatewayMode}*`,"- whatsapp \u2014 WhatsApp only","- telegram \u2014 Telegram only","- both \u2014 both channels","",`WhatsApp auth: ${e.authPresent?"present":"missing (omnish link)"}`,`Telegram token: ${e.tokenSet?"set":"not set"}`,`Allowlists: ${e.allowN} WA \xB7 ${e.tgAllowN} Telegram`,""];e.updateBrief&&t.push(`Updates: ${Re(e.updateBrief)}`,""),t.push("Change: /gateway both \xB7 /gw tg","/updates \u2014 npm + optional notice URL",`Config: ${Re(_)}`);let n=["<b>Gateway status</b>","",`<b>${V(e.gatewayMode)}</b> <i>current mode</i>`,"\u2022 whatsapp \u2014 WhatsApp only","\u2022 telegram \u2014 Telegram only","\u2022 both \u2014 both channels","",`${e.authPresent?"\u2713":"\u26A0"} WhatsApp auth: ${e.authPresent?"present":"missing \u2014 run omnish link"}`,`${e.tokenSet?"\u2713":"\u26A0"} Telegram token: ${e.tokenSet?"set":"not set"}`,`Allowlists: ${e.allowN} WA \xB7 ${e.tgAllowN} Telegram`,""];return e.updateBrief&&n.push(`<b>Updates</b> ${V(e.updateBrief)}`,""),n.push("<i>Change:</i> /gateway both \xB7 /gw tg","<i>/updates</i> \u2014 npm + optional notice URL",V(`Config: ${_}`)),fe(t.join(`
27
+ )`).run(n.c-Nc)}function _c(e,t){let n=Io(),o=Math.min(Math.max(1,e),50),r=n.prepare("SELECT rule_id, rule_name, kind, summary, ts_ms FROM watch_recent ORDER BY ts_ms DESC LIMIT ?").all(o*3),s=[];for(let i of r)if(!(t&&!t.has(i.rule_id))&&(s.push({ruleId:i.rule_id,ruleName:i.rule_name,kind:i.kind,stateKey:"",summary:i.summary,tsMs:i.ts_ms}),s.length>=o))break;return s}function Dc(e){return Io().prepare("SELECT state_key FROM watch_rule_state WHERE rule_id = ?").get(e)?.state_key??null}function Uc(e,t,n){Io().prepare("INSERT OR REPLACE INTO watch_rule_state (rule_id, state_key, updated_at_ms) VALUES (?, ?, ?)").run(e,t,n)}import Jc from"node:path";import Pt from"node:path";function tg(e,t){let n=Pt.normalize(e);for(let o of t){let r=Pt.normalize(o);if(n===r||n.startsWith(r+Pt.sep))return!0}return!1}function ng(e,t,n){let o=Pt.relative(t,e);if(o.startsWith("..")||Pt.isAbsolute(o))return!1;let r=o.split(Pt.sep).join("/"),s=n.replace(/\\/g,"/").replace(/^\//,"");if(s.includes("**")){let i=s.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,"___GLOBSTAR___").replace(/\*/g,"[^/]*").replace(/___GLOBSTAR___/g,".*");try{if(new RegExp(`^${i}$`).test(r))return!0;if(s.startsWith("**/")){let l=s.slice(3);if(l&&!l.includes("*"))return r.split("/").includes(l)}return!1}catch{return!1}}if(s.includes("*")){let i=s.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,"[^/]*");try{return new RegExp(`^${i}$`).test(r)}catch{return!1}}return r===s||r.endsWith("/"+s)||r.includes("/"+s+"/")}function Ar(e,t){if(t.excludePaths.length&&tg(e,t.excludePaths))return!0;let n=t.path;if(n&&t.excludeGlobs.length){for(let o of t.excludeGlobs)if(ng(e,n,o))return!0}return!1}function Bc(e){let t=[];for(let n of e.excludeGlobs){let o=n.replace(/\\/g,"/");o.startsWith("**/")?t.push(o):o.includes("*")?t.push(`**/${o}`):t.push(`**/${o}/**`)}for(let n of e.excludePaths)if(e.path)try{let o=Pt.relative(e.path,n);!o.startsWith("..")&&!Pt.isAbsolute(o)&&t.push(o.split(Pt.sep).join("/")+"/**")}catch{}return t}Ze();function Jn(e,t,n){if(e==="none")return[];let o=new Set;if((e==="self"||e==="all")&&o.add(t),e==="wa"||e==="all")for(let r of n.allowFrom){let s=ne(String(r));s&&o.add(`wa:${Jt(s)}`)}if(e==="tg"||e==="all")for(let r of n.telegramAllowFrom){let s=Ne(String(r));s&&o.add(`tg:${s}`)}return[...o]}function Hc(e,t,n){return Jn(e,t,n)}Ce();q();import og from"node:crypto";import Bi from"node:fs";var rg=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,sg=1,Hi=20;function Ir(e){let t=e.trim().toLowerCase();return rg.test(t)?{ok:!0,name:t}:{ok:!1,error:"Name must be alphanumeric with _ or -, max 32 chars."}}function Lr(){return og.randomBytes(8).toString("hex")}function ig(e){if(!Array.isArray(e)||e.length===0)return["create","delete","rename"];let t=new Set(["create","delete","rename","update"]),n=[];for(let o of e)typeof o=="string"&&t.has(o)&&n.push(o);return n.length?n:["create","delete","rename"]}function ag(e){if(!e||typeof e!="object")return null;let t=e,n=typeof t.id=="string"&&t.id.length>=4?t.id.slice(0,32):"",o=typeof t.name=="string"?t.name.trim().toLowerCase():"",r=typeof t.ownerPeerKey=="string"?t.ownerPeerKey:"",s=typeof t.kind=="string"?t.kind:"",i=s==="fs"||s==="pkg"||s==="svc"?s:null;if(!n||!o||!r||!i)return null;let a=typeof t.notify=="string"?t.notify.toLowerCase():"self",l=a==="wa"||a==="whatsapp"?"wa":a==="tg"||a==="telegram"?"tg":a==="all"?"all":a==="none"?"none":"self",c=typeof t.notifyWhen=="string"?t.notifyWhen:"always",d=c==="failure"||c==="state-change"?c:"always",u=Array.isArray(t.units)?t.units.filter(f=>typeof f=="string"&&f.trim().length>0).map(f=>f.trim()):[],m=Array.isArray(t.excludePaths)?t.excludePaths.filter(f=>typeof f=="string"&&f.trim().length>0):[],h=Array.isArray(t.excludeGlobs)?t.excludeGlobs.filter(f=>typeof f=="string"&&f.trim().length>0):[];return{id:n,name:o,ownerPeerKey:r,kind:i,enabled:typeof t.enabled=="boolean"?t.enabled:!0,paused:typeof t.paused=="boolean"?t.paused:!1,notify:l,notifyWhen:d,path:typeof t.path=="string"?t.path:"",events:ig(t.events),units:u,excludePaths:m,excludeGlobs:h,adapterStatus:typeof t.adapterStatus=="string"?t.adapterStatus:"",createdAtMs:typeof t.createdAtMs=="number"&&Number.isFinite(t.createdAtMs)?t.createdAtMs:Date.now()}}function lg(e){let t=JSON.parse(e);return!t||!Array.isArray(t.rules)?[]:t.rules.map(ag).filter(n=>n!==null)}function cg(e){let t=[...e].sort((s,i)=>s.createdAtMs-i.createdAtMs),n=new Set,o=!1,r=[];for(let s of t){if(!n.has(s.name)){n.add(s.name),r.push(s);continue}let i=s.id.slice(0,8),a=`${s.name}-${i}`;a.length>32&&(a=`${s.name.slice(0,32-i.length-1)}-${i}`);let l=0;for(;n.has(a);){l+=1;let c=String(l);a=`${s.name.slice(0,Math.max(1,32-i.length-c.length-1))}-${i}${c}`}n.add(a),M.warn({oldName:s.name,newName:a,id:s.id},"watch: renamed duplicate rule for device-wide namespace"),r.push({...s,name:a}),o=!0}return{rules:r,changed:o}}function Ue(){try{let e=Bi.readFileSync($o,"utf8"),t=lg(e),{rules:n,changed:o}=cg(t);return o&&bt(n),n}catch{return[]}}function ug(e){let t=0,n=0,o=0;for(let r of e)r.enabled?r.paused?n+=1:t+=1:o+=1;return{total:e.length,active:t,paused:n,disabled:o}}function Lo(){let e=Ue();return{rules:e,summary:ug(e)}}function bt(e){H(Mt);let n=`${JSON.stringify({version:sg,rules:e},null,2)}
28
+ `,o=`${$o}.tmp`;Bi.writeFileSync(o,n,{mode:384}),Bi.renameSync(o,$o)}function ji(){return $o}function yn(e,t){let n=t.trim().toLowerCase();return e.find(o=>o.name===n)}function jc(e,t){return e.find(n=>n.id===t)}function qn(e,t){let n=e.findIndex(o=>o.id===t.id);if(n>=0){let o=[...e];return o[n]=t,o}return[...e,t]}function Gc(e,t){let n=t.trim().toLowerCase();return e.filter(o=>o.name!==n)}import dg from"node:os";import kt from"node:path";var pg=new Set([".env","id_rsa","id_ed25519","id_ecdsa","known_hosts","credentials.json","secrets.json",".netrc","shadow","passwd"]);function tt(e){let t=kt.normalize(e),n=t.toLowerCase(),o=dg.homedir().toLowerCase();if(n.includes(`${kt.sep}.ssh${kt.sep}`)||n.endsWith(`${kt.sep}.ssh`)||n.includes(`${kt.sep}browser${kt.sep}`)&&n.includes("profile")||o&&(n===`${o}/.gnupg`||n.startsWith(`${o}/.gnupg${kt.sep}`))||n.includes(`${kt.sep}keychains${kt.sep}`))return!0;let r=kt.basename(t);return!!(pg.has(r)||r.startsWith(".env.")||r.endsWith(".pem")||r.endsWith(".key"))}var mg=["node_modules",".git",".svn",".hg","__pycache__",".cache",".next","dist","build"],hg=[".tmp",".swp",".swx","~",".part"];function qc(e,t){let n=new Map,o=new Map;function r(d,u){let m=d.split(Jc.sep);for(let f of m)if(mg.includes(f))return!0;let h=Jc.basename(d);for(let f of hg)if(h.endsWith(f))return!0;return!!Ar(d,u)}function s(d){let u=jc(Ue(),d);return!u||!u.enabled||u.paused?null:u}function i(d){let u=Date.now(),m=o.get(d);return!m||u-m.windowStart>=6e4?(o.set(d,{windowStart:u,count:1}),!1):m.count>=t.maxEventsPerMinute?!0:(m.count+=1,!1)}async function a(d,u){let m=e.getConfig();if(!m.watchEnabled)return;let h=s(d.id);if(!h||u.kind==="fs"&&u.meta?.path&&(tt(u.meta.path)||r(u.meta.path,h)))return;if(Wc(u),(h.notifyWhen??"always")==="state-change"){if(Dc(h.id)===u.stateKey)return;Uc(h.id,u.stateKey,u.tsMs)}let g=Hc(h.notify,h.ownerPeerKey,m);if(g.length===0)return;let y=`[watch:${h.name}] ${u.summary}`;await Promise.all(g.map(b=>e.sendToPeer(b,y).catch(()=>{})))}function l(d){let u=n.get(d);if(!u)return;n.delete(d);let m=s(u.ruleId);m&&(i(u.ruleId)||a(m,u.event))}function c(d){for(let[u,m]of n)m.ruleId===d&&(clearTimeout(m.timer),n.delete(u))}return{ingest(d,u){if(!d.enabled||d.paused||u.meta?.path&&(tt(u.meta.path)||r(u.meta.path,d)))return;let m=`${d.id}:${u.stateKey||u.summary}`,h=n.get(m);h&&clearTimeout(h.timer);let f=setTimeout(()=>l(m),t.debounceMs);f.unref?.(),n.set(m,{timer:f,event:u,ruleId:d.id})},cancelForRule:c,dispose(){for(let d of n.values())clearTimeout(d.timer);n.clear(),o.clear()}}}function zc(e,t){let n=e.toLowerCase();return!!(n==="create"&&t.has("create")||n==="delete"&&t.has("delete")||(n==="update"||n==="rename")&&(t.has("update")||t.has("rename")))}Ce();import Kc from"node:fs";import fg from"node:path";import{subscribe as gg}from"@parcel/watcher";var yg=["**/node_modules/**","**/.git/**","**/.svn/**","**/.hg/**","**/__pycache__/**","**/.cache/**"];function wg(e){return e==="create"?"create":e==="delete"?"delete":e==="update"?"update":e}function bg(e,t){try{let n=Kc.statSync(t),o=n.isFile()&&n.size<1024*1024?` (${n.size<1024?`${n.size} B`:`${(n.size/1024).toFixed(1)} KB`})`:"";return`fs: ${e} ${t}${o}`}catch{return`fs: ${e} ${t}`}}async function Yc(e,t){let n=e.path;if(!n||!Kc.existsSync(n))return{stop:async()=>{},status:()=>"error: path missing or not found"};if(tt(n))return{stop:async()=>{},status:()=>"error: sensitive path denied"};let o=new Set(e.events.map(i=>i.toLowerCase())),r="ok",s=null;try{s=await gg(n,(i,a)=>{if(i){r=`error: ${i.message}`,M.warn({rule:e.name,err:i.message},"watch fs adapter");return}for(let l of a){let c=wg(l.type);if(!zc(c,o))continue;let d=fg.resolve(l.path);if(!d.startsWith(n)||tt(d)||Ar(d,e))continue;let u=bg(c,d);t({ruleId:e.id,ruleName:e.name,kind:"fs",stateKey:`${c}:${d}`,summary:u,tsMs:Date.now(),meta:{path:d,type:c}})}},{ignore:[...yg,...Bc(e)]})}catch(i){r=`error: ${String(i)}`,M.warn({rule:e.name,err:String(i)},"watch fs subscribe failed")}return{stop:async()=>{s&&(await s.unsubscribe().catch(()=>{}),s=null)},status:()=>r}}import $g from"node:os";Ce();import zt from"node:fs";function kg(e,t,n){let o=n?.pollMs??2e3,r=0,s=!1,i=null,a=null,l="starting";function c(){if(!s)try{let d=zt.statSync(e);if(d.size<r&&(r=0),d.size<=r)return;let u=zt.openSync(e,"r");try{let m=d.size-r,h=Buffer.alloc(m);zt.readSync(u,h,0,m,r),r=d.size;let f=h.toString("utf8");for(let g of f.split(`
29
+ `)){let y=g.trim();y&&t(y)}}finally{zt.closeSync(u)}l="ok"}catch(d){l=`error: ${String(d)}`}}try{zt.existsSync(e)&&(r=zt.statSync(e).size),i=zt.watch(e,()=>c()),i.on("error",()=>{i?.close(),i=null}),l="ok (watch)"}catch{l="ok (poll)",a=setInterval(c,o),a.unref?.(),c()}return{stop(){s=!0,i?.close(),a&&clearInterval(a)},status:()=>l}}function Or(e,t){for(let n of e)try{if(zt.existsSync(n))return kg(n,t)}catch(o){M.debug({path:n,err:String(o)},"watch log-tail skip path")}return null}var vg=["/var/log/install.log"];function Sg(e){let t=e.toLowerCase();return t.includes("installed")||t.includes("upgraded")||t.includes("removed")?e.length>240?`pkg: ${e.slice(0,240)}\u2026`:`pkg: ${e}`:null}function Qc(e,t){return Or(vg,n=>{let o=Sg(n);o&&t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:o.slice(0,200),summary:o,tsMs:Date.now()})})}var xg=["/var/log/dpkg.log","/var/log/apt/history.log"];function Cg(e){if(e.includes("status installed")){let t=e.match(/install\s+(\S+):/);if(t)return`pkg: installed ${t[1]} (dpkg)`}if(e.includes("status removed")||e.includes("remove ")){let t=e.match(/remove\s+(\S+):/);if(t)return`pkg: removed ${t[1]} (dpkg)`}return null}function Rg(e){return e.startsWith("Install:")||e.startsWith("Upgrade:")?`pkg: ${e.slice(0,120)} (apt)`:e.startsWith("Remove:")?`pkg: ${e.slice(0,120)} (apt)`:null}function Vc(e,t){return Or(xg,n=>{let o=Cg(n)??Rg(n);o&&t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:o,summary:o,tsMs:Date.now()})})}import{spawn as Tg}from"node:child_process";function Xc(e,t){let n=!1,o="ok",r=0,s=()=>{if(n)return;let a=Tg("powershell.exe",["-NoProfile","-NonInteractive","-Command","Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName=@('MsiInstaller','Windows Installer')} -MaxEvents 5 -ErrorAction SilentlyContinue | Select-Object -Property RecordId,Message | ConvertTo-Json -Compress"],{windowsHide:!0}),l="";a.stdout?.on("data",c=>{l+=String(c)}),a.on("close",c=>{if(n||c!==0||!l.trim()){c!==0&&(o="ok (no events or access denied)"),i();return}try{let d=JSON.parse(l),u=Array.isArray(d)?d:[d];for(let m of u.sort((h,f)=>h.RecordId-f.RecordId)){if(m.RecordId<=r)continue;r=m.RecordId;let h=(m.Message??"").replace(/\s+/g," ").trim().slice(0,300);if(!h)continue;let f=`pkg: ${h}`;t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:`win:${m.RecordId}`,summary:f,tsMs:Date.now()})}o="ok"}catch(d){o=`parse error: ${String(d)}`}i()}),a.on("error",c=>{o=`error: ${String(c)}`,i()})},i=()=>{n||setTimeout(s,3e4).unref?.()};return s(),{stop(){n=!0},status:()=>o}}function Zc(e,t){let n=$g.platform();if(n==="win32"){let r=Xc(e,t);return{stop:()=>r.stop(),status:r.status}}if(n==="darwin"){let r=Qc(e,t);return r?{stop:()=>r.stop(),status:r.status}:{stop:()=>{},status:()=>"error: cannot read /var/log/install.log (try sudo or adm group)"}}let o=Vc(e,t);return o?{stop:()=>o.stop(),status:o.status}:{stop:()=>{},status:()=>"error: cannot read dpkg/apt logs (add user to adm or run gateway with read access)"}}import Ng from"node:os";import{spawnSync as eu}from"node:child_process";var Mg=3e4;function Pg(e){let t=new Map;for(let n of e){let o=eu("launchctl",["print",`system/${n}`],{encoding:"utf8",timeout:1e4});if(o.status!==0){let s=process.getuid?.()??501,i=eu("launchctl",["print",`gui/${s}/${n}`],{encoding:"utf8",timeout:1e4});if(i.status!==0){t.set(n,"not-found");continue}let a=(i.stdout??"").includes("state = running")?"running":"stopped";t.set(n,a);continue}let r=(o.stdout??"").includes("state = running")?"running":"stopped";t.set(n,r)}return t}function tu(e,t){let n=e.units,o=!1,r="ok",s=new Map,i=()=>{if(o)return;let a=Pg(n);if(a.size===0)r="error: no launchd labels configured";else{r="ok";for(let[l,c]of a){let d=s.get(l);if(d!==void 0&&d!==c){let u=`svc: ${l} ${d} \u2192 ${c}`;t({ruleId:e.id,ruleName:e.name,kind:"svc",stateKey:`${l}:${c}`,summary:u,tsMs:Date.now(),meta:{unit:l,state:c}})}}s=a}o||setTimeout(i,Mg).unref?.()};return i(),{stop(){o=!0},status:()=>r}}import{spawnSync as nu}from"node:child_process";var Eg=3e4;function Ag(e){let t=new Map;if(e.length===0)return t;let n=["show",...e,"--property=ActiveState,SubState,UnitFileState","--no-pager"],o=nu("systemctl",n,{encoding:"utf8",timeout:15e3});if(o.status!==0)return t;let r="";for(let s of(o.stdout??"").split(`
30
+ `)){let i=s.match(/^Unit=(.+)$/);if(i){r=i[1];continue}let a=s.match(/^ActiveState=(.+)$/);a&&r&&t.set(r,a[1].trim())}if(t.size===0)for(let s of e){let i=s.endsWith(".service")?s:`${s}.service`,a=nu("systemctl",["is-active",i],{encoding:"utf8",timeout:5e3}),l=(a.stdout??a.stderr??"unknown").trim();t.set(i,l)}return t}function ou(e,t){let n=e.units.map(a=>a.endsWith(".service")?a:`${a}.service`),o=!1,r="ok",s=new Map,i=()=>{if(o)return;let a=Ag(n);if(a.size===0)r="error: systemctl unavailable or units not found";else{r="ok";for(let[l,c]of a){let d=s.get(l);if(d!==void 0&&d!==c){let u=`svc: ${l} ${d} \u2192 ${c}`;t({ruleId:e.id,ruleName:e.name,kind:"svc",stateKey:`${l}:${c}`,summary:u,tsMs:Date.now(),meta:{unit:l,state:c}})}}s=a}o||setTimeout(i,Eg).unref?.()};return i(),{stop(){o=!0},status:()=>r}}import{spawnSync as Ig}from"node:child_process";var Lg=3e4;function Og(e){let t=new Map;for(let n of e){let o=Ig("sc",["query",n],{encoding:"utf8",timeout:1e4,windowsHide:!0});if(o.status!==0){t.set(n,"not-found");continue}let r=o.stdout??"",s="unknown";r.includes("RUNNING")?s="running":r.includes("STOPPED")?s="stopped":r.includes("START_PENDING")?s="start-pending":r.includes("STOP_PENDING")&&(s="stop-pending"),t.set(n,s)}return t}function ru(e,t){let n=e.units,o=!1,r="ok",s=new Map,i=()=>{if(o)return;let a=Og(n);if(a.size===0)r="error: no service names configured";else{r="ok";for(let[l,c]of a){let d=s.get(l);if(d!==void 0&&d!==c){let u=`svc: ${l} ${d} \u2192 ${c}`;t({ruleId:e.id,ruleName:e.name,kind:"svc",stateKey:`${l}:${c}`,summary:u,tsMs:Date.now(),meta:{unit:l,state:c}})}}s=a}o||setTimeout(i,Lg).unref?.()};return i(),{stop(){o=!0},status:()=>r}}function su(e,t){let n=Ng.platform();return n==="win32"?ru(e,t):n==="darwin"?tu(e,t):ou(e,t)}var nt=null,Kt=null;function iu(e){let t=e.getConfig();return qc(e,{debounceMs:Math.max(500,t.watchDebounceMs??2e3),maxEventsPerMinute:Math.max(1,t.watchMaxEventsPerMinute??30)})}function _g(e){return e.watchEnabled&&e.watchAutoRestore}var Gi=class{deps;pipeline;runtimes=new Map;stopped=!1;constructor(t){this.deps=t,this.pipeline=iu(t)}onEvent=(t,n)=>{this.pipeline.ingest(t,n)};cancelPendingForRule(t){this.pipeline.cancelForRule(t)}async reload(){if(this.pipeline.dispose(),this.pipeline=iu(this.deps),await this.stopAdapters(),this.stopped||!this.deps.getConfig().watchEnabled)return 0;let n=Ue().filter(o=>o.enabled&&!o.paused);for(let o of n)await this.startRule(o);return this.runtimes.size}async startRule(t){try{let n;if(t.kind==="fs"){let o=Qe(t.path,Fg.homedir()),r={...t,path:o};if(tt(o)){t.adapterStatus="denied: sensitive path",this.persistRuleStatus(t);return}if(!Wg.existsSync(o)){t.adapterStatus="error: path not found",this.persistRuleStatus(t);return}let s=await Yc(r,i=>this.onEvent(r,i));n={stop:()=>s.stop(),status:s.status}}else if(t.kind==="pkg")n=Zc(t,r=>this.onEvent(t,r));else if(t.kind==="svc"){if(t.units.length===0){t.adapterStatus="error: no units",this.persistRuleStatus(t);return}n=su(t,r=>this.onEvent(t,r))}else return;t.adapterStatus=n.status(),this.persistRuleStatus(t),this.runtimes.set(t.id,{rule:t,adapter:n})}catch(n){t.adapterStatus=`error: ${String(n)}`,this.persistRuleStatus(t),M.warn({rule:t.name,err:String(n)},"watch rule start failed")}}persistRuleStatus(t){let n=Ue(),o=n.findIndex(r=>r.id===t.id);o>=0&&(n[o]={...n[o],adapterStatus:t.adapterStatus},bt(n))}async stopAdapters(){for(let t of this.runtimes.values())try{await t.adapter.stop()}catch{}this.runtimes.clear()}async stop(){this.stopped=!0,await this.stopAdapters(),this.pipeline.dispose(),Fc()}getStatusLines(){let t=Ue();return t.length===0?["(no watch rules)"]:t.map(n=>{let r=this.runtimes.get(n.id)?.adapter.status()??n.adapterStatus,s=n.enabled?n.paused?"paused":"on":"disabled",i=n.kind==="fs"?n.path:n.kind==="svc"?n.units.join(","):"system",a=n.kind==="fs"&&(n.excludePaths.length||n.excludeGlobs.length)?` excludes:${n.excludePaths.length+n.excludeGlobs.length}`:"";return`${n.name} [${n.kind}] ${s} notify=${n.notify} when=${n.notifyWhen} \u2014 ${i}${a} \u2014 ${r}`})}getRunningCount(){return this.runtimes.size}isRuleRunning(t){return this.runtimes.has(t)}};function au(){return nt}function Ji(e){nt?.cancelPendingForRule(e)}function Dg(){H(Mt),Er()}function qi(){Kt&&(nt||(nt=new Gi(Kt)),nt.reload())}function Nr(e){Kt=e,Dg();let t=e.getConfig(),{summary:n}=Lo();return n.total>0&&M.info({rules:n.total,active:n.active,paused:n.paused,disabled:n.disabled},"watch rules loaded from disk"),_g(t)&&qi(),()=>{nt?.stop(),nt=null,Kt=null}}async function lu(){return Kt?Kt.getConfig().watchEnabled?(qi(),nt?.getRunningCount()??0):(nt?.stop(),nt=null,-1):-1}function Je(){if(!Kt)return;if(!Kt.getConfig().watchEnabled){nt?.stop(),nt=null;return}qi()}q();pe();q();import Hg from"node:os";import Wr from"node:path";q();import zi from"node:fs";import uu from"node:os";import Yt from"node:path";var du=Yt.join(D,"sessions.json"),zn=new Map;function pu(){return{cwd:Yt.resolve(uu.homedir())}}function Ug(e){return e.startsWith("wa:")||e.startsWith("tg:")?e:`wa:${e}`}function Bg(){try{let e=zi.readFileSync(du,"utf8"),t=JSON.parse(e);for(let[n,o]of Object.entries(t)){if(!o||typeof o!="object")continue;let r=Ug(n),i={cwd:typeof o.cwd=="string"&&o.cwd.length>0?Yt.resolve(o.cwd):pu().cwd};o.fileReceiveRoot==="sessionCwd"&&(i.fileReceiveRoot="sessionCwd"),zn.set(r,i)}}catch{}}function mu(){H(D);let e={};for(let[t,n]of zn){let o={cwd:n.cwd};n.fileReceiveRoot==="sessionCwd"&&(o.fileReceiveRoot="sessionCwd"),e[t]=o}zi.writeFileSync(du,JSON.stringify(e,null,2)+`
31
+ `,{mode:384})}var cu=!1;function Ki(){cu||(Bg(),cu=!0)}function ae(e){Ki();let t=zn.get(e);return t||(t=pu(),zn.set(e,t)),t}function Fr(e,t){Ki();let n=Yt.resolve(t),o=ae(e);o.cwd=n,zn.set(e,o),mu()}function hu(e){return ae(e).fileReceiveRoot==="sessionCwd"?"sessionCwd":"default"}function Yi(e,t){Ki();let n=ae(e);t==="default"?delete n.fileReceiveRoot:n.fileReceiveRoot="sessionCwd",zn.set(e,n),mu()}function fu(e){let t=e.trim();if(/[&|;\n]/.test(t)||!t.startsWith("cd"))return null;if(t==="cd")return{kind:"home"};if(t.length>2&&t[2]!==" "&&t[2]!==" ")return null;let n=t.slice(2).trimStart();return n?{kind:"path",value:n}:{kind:"home"}}function gu(e,t){if(t.kind==="home")return Yt.resolve(uu.homedir());let n=t.value.replace(/^['"]|['"]$/g,"");return Yt.isAbsolute(n)?Yt.normalize(n):Yt.resolve(e,n)}function yu(e){try{return zi.statSync(e).isDirectory()?{ok:!0}:{ok:!1,error:`Not a directory: ${e}`}}catch(t){return{ok:!1,error:String(t)}}}var jg="Omnish";function wu(){return Wr.join(Hg.homedir(),"Downloads",jg)}function Qt(e,t){let n=ae(t);if(n.fileReceiveRoot==="sessionCwd")return n.cwd;switch(e.fileReceiveRootMode){case"downloads":return wu();case"omnishData":return Wr.join(D,e.fileInboxSubdir);case"sessionCwd":return n.cwd;case"processCwd":return process.cwd();case"fixed":{let o=(e.fileReceiveRootPath??"").trim();if(!o)throw new Error('fileReceiveRootPath is required when fileReceiveRootMode is "fixed".');if(!Wr.isAbsolute(o))throw new Error('fileReceiveRootPath must be an absolute path when fileReceiveRootMode is "fixed".');return Wr.resolve(o)}default:return wu()}}q();import Lt from"node:fs";import ku from"node:path";import en from"node:process";var wn="\x1B";function Gg(e){return e.replace(/\u001B\[[\d;]*m/g,"")}function _r(e){return Gg(e).length}function Qi(e,t,n,o,r=2){let s=Math.max(0,t-_r(n));return`${e}${n}${" ".repeat(s)}${" ".repeat(r)}${o}`}function Et(e,t,n,o){if(n.length===0)return[];let r=n.map(i=>o(i.left)),s=Math.max(...r.map(_r));return n.map((i,a)=>Qi(t,s,r[a],v(e,i.right)))}var Vt={primary:"#b4ff24",foreground:"#eef2f4",muted:"#8a9199",border:"#2a3139",error:"#ef4444",warn:"#a0e614",warnAlt:"#8cce04"};function Jg(e){let t=e.replace(/^#/,"");return t.length!==6||!/^[0-9a-fA-F]+$/.test(t)?null:{r:parseInt(t.slice(0,2),16),g:parseInt(t.slice(2,4),16),b:parseInt(t.slice(4,6),16)}}function Xt(e){let t=Jg(e);return t?`${wn}[38;2;${t.r};${t.g};${t.b}m`:""}function Dr(e){if(process.env.NO_COLOR!==void 0&&process.env.NO_COLOR!=="")return!1;let t=process.env.FORCE_COLOR;return t==="1"||t==="true"?!0:t==="0"||t==="false"?!1:e.isTTY===!0}function At(e,t,n){return!t||!Dr(e)?n:`${t}${n}${wn}[0m`}function z(e,t){return At(e,`${wn}[1m`,t)}function qg(e,t){return Dr(e)?`${Xt(Vt.foreground)}${wn}[1m${t}${wn}[0m`:t}function X(e,t){return At(e,`${wn}[2m`,t)}function Re(e,t){return At(e,`${Xt(Vt.primary)}${wn}[1m`,t)}function ye(e,t){return At(e,Xt(Vt.primary),t)}function v(e,t){return At(e,Xt(Vt.foreground),t)}function w(e,t){return At(e,Xt(Vt.muted),t)}function zg(e,t){return At(e,Xt(Vt.border),t)}function Ur(e,t){return At(e,Xt(Vt.error),t)}function ke(e,t){return At(e,Xt(Vt.warn),t)}function Zt(e,t=60,n="\u2500"){let o=n.repeat(Math.max(1,t));return zg(e,o)}function U(e,t){return Dr(e)?`${`${w(e,"[")}${ye(e,"omnish")}${w(e,"]")}`} ${t}`:`[omnish] ${t}`}function C(e,t){return Dr(e)?`${`${Ur(e,"[omnish]")}`} ${t}`:`[omnish] ${t}`}function bu(e,t){let n=[];for(let o of e)switch(o.kind){case"title":n.push("",Re(t,o.text),"");break;case"sub":n.push("",qg(t,o.text),"");break;case"gap":n.push("");break;case"p":n.push(v(t,o.text));break;case"bullet":n.push(`${w(t,"\u2022")} ${v(t,o.text)}`);break}return n.join(`
32
+ `).replace(/^\n+/,"").trimEnd()}pe();q();bn();kn();function Hr(e){return(e&4)!==0}function Zi(e){return(e&2)!==0}var vu={error:3,warn:2,info:1};function Su(e,t){let n=vu[t];return e.filter(o=>vu[o.severity]>=n)}function Ot(e,t={}){let n=[],o=t.gatewayMode??e.gatewayMode,r=o==="whatsapp"||o==="both",s=o==="telegram"||o==="both";if(e.allowFrom.includes("*")&&n.push({severity:"error",code:"allow-wildcard",message:'allowFrom contains "*" \u2014 this must never be used; remove it from config immediately.',detail:`Edit ${_} and delete wildcard entries.`,fixHint:`Edit ${_} and remove any "*" entries from allowFrom.`}),r&&e.allowFrom.length===0&&n.push({severity:"warn",code:"allow-wa-empty",message:"allowFrom is empty \u2014 no WhatsApp identity can run commands.",detail:"Add your number: omnish allow +<E164>",fixHint:"omnish allow +<your_E164_number>"}),s&&e.telegramAllowFrom.length===0&&n.push({severity:"warn",code:"allow-tg-empty",message:"telegramAllowFrom is empty \u2014 no Telegram user can run commands.",detail:"Add your user id: omnish allow tg:<id>",fixHint:"omnish allow tg:<your_telegram_user_id>"}),e.recipesAllowDangerousBuiltins&&n.push({severity:"warn",code:"recipes-dangerous",message:"recipesAllowDangerousBuiltins is enabled \u2014 built-in recipe helpers may add permissive Claude Code flags.",fixHint:`Set recipesAllowDangerousBuiltins to false in ${_} unless you trust every recipe.`}),e.serviceInstallFromChat&&n.push({severity:"warn",code:"service-install-from-chat",message:"serviceInstallFromChat is enabled \u2014 allowlisted users can run /service install and write user-level systemd or LaunchAgent units.",fixHint:`Set serviceInstallFromChat to false in ${_} unless you trust every allowlisted identity.`}),e.mediaInstallFromChat&&n.push({severity:"warn",code:"media-install-from-chat",message:"mediaInstallFromChat is enabled \u2014 allowlisted users can run /dl install and download binaries into the omnish data directory.",fixHint:`Set mediaInstallFromChat to false in ${_} unless you trust every allowlisted identity.`}),!ku.isAbsolute(e.shell))n.push({severity:"warn",code:"shell-not-absolute",message:`Shell path is not absolute: ${e.shell}`,fixHint:`Set "shell" to an absolute path (e.g. /bin/bash) in ${_}.`});else try{Lt.existsSync(e.shell)||n.push({severity:"error",code:"shell-missing",message:`Configured shell does not exist: ${e.shell}`,fixHint:`Install the shell or update "shell" in ${_} to a valid binary.`})}catch{n.push({severity:"warn",code:"shell-stat-failed",message:`Could not verify shell path: ${e.shell}`,fixHint:`Check permissions and that "shell" in ${_} points to a real file.`})}if(e.fileReceiveRootMode==="fixed"){let a=e.fileReceiveRootPath.trim();a?ku.isAbsolute(a)||n.push({severity:"error",code:"receive-fixed-not-absolute",message:`fileReceiveRootPath must be absolute when fileReceiveRootMode is "fixed": ${a}`,fixHint:`Use an absolute path for fileReceiveRootPath in ${_}.`}):n.push({severity:"error",code:"receive-fixed-empty",message:'fileReceiveRootMode is "fixed" but fileReceiveRootPath is empty.',fixHint:`Set fileReceiveRootPath to an absolute directory in ${_}, or change fileReceiveRootMode.`})}if(en.platform!=="win32"){try{if(Lt.existsSync(_)){let c=Lt.statSync(_);(Hr(c.mode)||Zi(c.mode))&&n.push({severity:"warn",code:"config-permissions",message:"config.json is accessible to users other than the owner (group/world).",detail:`Recommended: chmod 600 ${_}`,fixHint:`chmod 600 ${_}`})}}catch{}(typeof en.getuid=="function"?en.getuid():null)===0&&n.push({severity:"warn",code:"run-as-root",message:"Process is running as root \u2014 allowlisted remote access has full system privileges.",fixHint:"Run omnish as a non-root user (systemd user service, container user, etc.)."});let l=!1;try{if(Lt.existsSync(D)){let c=Lt.statSync(D);(Hr(c.mode)||Zi(c.mode))&&(l=!0,n.push({severity:"warn",code:"data-dir-permissive",message:"Omnish data directory is accessible to group or other users \u2014 auth, jobs, and logs may be exposed.",detail:`Recommended: chmod 700 ${D}`,fixHint:`chmod 700 ${D}`}))}}catch{}try{if(!l&&Lt.existsSync(it)){let c=Lt.statSync(it);(Hr(c.mode)||Zi(c.mode))&&n.push({severity:"warn",code:"jobs-dir-permissive",message:"Jobs log directory is accessible to group or other users \u2014 command output may leak.",detail:`Recommended: chmod 700 ${it}`,fixHint:`chmod 700 ${it}`})}}catch{}try{if(Lt.existsSync(ce)){let c=Lt.statSync(ce);Hr(c.mode)&&n.push({severity:"warn",code:"auth-dir-world-readable",message:"WhatsApp auth directory is readable by others \u2014 session material may be exposed.",detail:`Recommended: chmod 700 ${ce}`,fixHint:`chmod 700 ${ce}`})}}catch{}}return(typeof en.env.TELEGRAM_BOT_TOKEN=="string"?en.env.TELEGRAM_BOT_TOKEN.trim():"")&&n.push({severity:"info",code:"telegram-token-env",message:"TELEGRAM_BOT_TOKEN is set in the environment; it overrides config.json until unset.",fixHint:"Unset TELEGRAM_BOT_TOKEN in the shell/service env if you want config.json to apply."}),s&&!Me(e)&&n.push({severity:"error",code:"telegram-no-token",message:"Telegram is enabled for this gateway but no bot token is configured.",detail:"Set telegramBotToken in config or TELEGRAM_BOT_TOKEN.",fixHint:`Set telegramBotToken in ${_} or export TELEGRAM_BOT_TOKEN=<token>.`}),e.clusterEnabled&&s&&n.push({severity:"info",code:"cluster-telegram-tokens",message:"Cluster mode with Telegram: use a distinct BotFather bot/token per omnish host if several gateways run Telegram \u2014 the same token cannot be long-polled twice.",fixHint:"Create separate bots for each machine or run Telegram on fewer hosts."}),e.platformToken.trim()&&n.push({severity:"info",code:"platform-token-config",message:"platformToken is stored in config.json (same trust boundary as your shell account).",fixHint:"Use OMNISH_TOKEN in the environment for CI/ephemeral hosts; env overrides config."}),(en.env.OMNISH_TOKEN?.trim()||en.env.OMNISH_TUNNEL_TOKEN?.trim()||en.env.OMNISH_DEVICE_TOKEN?.trim())&&n.push({severity:"info",code:"platform-token-env",message:"OMNISH_TOKEN (or OMNISH_TUNNEL_TOKEN / OMNISH_DEVICE_TOKEN) is set in the environment.",fixHint:"Unset platform token env vars if you want config.json / tunnel-auth.json to apply."}),e.tunnelEnabled&&(n.push({severity:"warn",code:"tunnel-enabled",message:"Chat tunneling is enabled \u2014 allowlisted users can publish public URLs to local HTTP/TCP services.",fixHint:"Disable tunnelEnabled or restrict allowlists if you do not want remote tunnel control."}),St()||n.push({severity:"warn",code:"tunnel-no-token",message:"Chat tunneling is enabled but no tunnel token is configured.",fixHint:"Run `omnish tunnel login` or set OMNISH_TUNNEL_TOKEN for the gateway process."})),e.tunnelRelayUrl.trim()&&e.tunnelRelayUrl.trim()!==Ee&&n.push({severity:"info",code:"tunnel-relay-custom",message:`Custom tunnel relay configured: ${e.tunnelRelayUrl.trim()}`,fixHint:"Use the default relay only if you trust the operator of that endpoint."}),n}function Oo(e){return e.some(t=>t.severity==="error")}function Yg(e,t){switch(t){case"error":return Ur(e,"[ERROR]");case"warn":return ke(e,"[WARN]");case"info":return w(e,"[INFO]")}}function ea(e,t,n,o){if(o.length!==0){t.push(z(e,`${n} (${o.length}):`));for(let r of o)t.push(` ${Yg(e,r.severity)} ${w(e,`${r.code}:`)} ${v(e,r.message)}`),r.detail&&t.push(` ${w(e,r.detail)}`),r.fixHint&&t.push(` ${w(e,`Fix: ${r.fixHint}`)}`);t.push("")}}function ta(e,t){if(e.length===0)return[`${Re(t,"Security check:")} ${v(t,"no issues reported by automated rules.")}`,"",w(t,"Note: allowlisted remote shell access is still equivalent to sharing credentials with those identities.")].join(`
33
+ `);let n=e.filter(a=>a.severity==="error"),o=e.filter(a=>a.severity==="warn"),r=e.filter(a=>a.severity==="info"),i=[`${Re(t,"Security check:")} `+v(t,`${n.length} error(s), ${o.length} warning(s), ${r.length} note(s).`),""];return ea(t,i,"Errors",n),ea(t,i,"Warnings",o),ea(t,i,"Notes",r),i.push(w(t,"Allowlisted identities can run commands as this user; treat them like passwords.")),i.join(`
34
+ `).trimEnd()}function na(e){return{errors:e.filter(t=>t.severity==="error").length,warns:e.filter(t=>t.severity==="warn").length,infos:e.filter(t=>t.severity==="info").length}}function xu(e){return`${JSON.stringify({findings:e,summary:na(e)},null,2)}
35
+ `}function Cu(e,t){let n=e.filter(a=>a.severity==="error").length,o=e.filter(a=>a.severity==="warn").length,r=e.filter(a=>a.severity==="info").length;if(n===0&&o===0&&r===0)return"security: ok (no automated findings)";let s=[];n&&s.push(`${n} error(s)`),o&&s.push(`${o} warning(s)`),r&&s.push(`${r} note(s)`);let i=t??"run `omnish security` for details";return`security: ${s.join(", ")} \u2014 ${i}`}function Ru(e,t,n){let o=e.filter(l=>l.severity==="error").length,r=e.filter(l=>l.severity==="warn").length,s=e.filter(l=>l.severity==="info").length;if(o===0&&r===0&&s===0)return`${Re(t,"security:")} ${v(t,"ok (no automated findings)")}`;let i=[];o&&i.push(Ur(t,`${o} error(s)`)),r&&i.push(ke(t,`${r} warning(s)`)),s&&i.push(w(t,`${s} note(s)`));let a=n??"run `omnish security` for details";return`${Re(t,"security:")} ${i.join(", ")} ${w(t,`\u2014 ${a}`)}`}var Ae="- ";function p(e){return{wa:e,tg:e}}function we(e,t){return{wa:e,tg:t,tgHtml:!0}}function me(e,t){return t==="whatsapp"?{text:e.wa}:e.tgHtml?{text:e.tg,parseModeHtml:!0}:{text:e.tg}}function Z(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function Te(e){return e.replace(/[*_~`]/g,t=>({"*":"\u2217",_:"\uFF3F","~":"\u02DC","`":"\u2032"})[t]??t)}function Qg(e){let t=[];for(let n of e)switch(n.kind){case"title":t.push("",`*${n.text}*`,"");break;case"sub":t.push("",`_${n.text}_`,"");break;case"gap":t.push("");break;case"p":t.push(n.text);break;case"bullet":t.push(`${Ae}${n.text}`);break}return t.join(`
36
+ `).replace(/^\n+/,"").trimEnd()}function Vg(e){let t=[];for(let n of e)switch(n.kind){case"title":t.push("",`<b>${Z(n.text)}</b>`,"");break;case"sub":t.push("",`<i>${Z(n.text)}</i>`,"");break;case"gap":t.push("");break;case"p":t.push(Z(n.text));break;case"bullet":t.push(`\u2022 ${Z(n.text)}`);break}return t.join(`
37
+ `).replace(/^\n+/,"").trimEnd()}function ee(e){return{wa:Qg(e),tg:Vg(e),tgHtml:!0}}function Kn(e){return[{kind:"title",text:"Omnish \u2014 quick help"},{kind:"p",text:"Per-chat shell cwd is stored under your data dir (see /wa help)."},{kind:"gap"},{kind:"sub",text:"Run commands"},{kind:"bullet",text:`${e.commandPrefix}<command> \u2014 sync shell in session cwd (timeout ${e.syncTimeoutMs} ms)`},{kind:"bullet",text:`${e.commandPrefix}cd <dir> \u2014 change session cwd (${e.commandPrefix}cd alone \u2192 home)`},{kind:"bullet",text:"!!start | !!stop \u2014 free shell (plain \u2192 sync shell only when no focused /apps session); space optional"},{kind:"bullet",text:"/bg <cmd> \u2014 background job; optional -n name; /jobs, /log, /tail, /kill (id or name)"},{kind:"bullet",text:e.tunnelEnabled?"/tunnel login|logout|status|http|tcp \u2014 login/status anytime; /tunnels needs tunnelEnabled":"/tunnel expose/list off until tunnelEnabled true (login/status still work)"},{kind:"bullet",text:"/apps \u2026 \u2014 interactive app sessions (/apps help)"},{kind:"bullet",text:"/send selectors \u2014 host files \u2192 chat (/file); caption: selectors -- note"},{kind:"bullet",text:"/files \u2014 full help for /send and saving inbound media"},{kind:"bullet",text:"/receive \u2014 set this chat\u2019s inbound folder (e.g. session cwd)"},{kind:"bullet",text:"/computers \xB7 /pcs \xB7 /c \u2014 chat-driven cluster (/c here to take over, /c help)"},{kind:"bullet",text:"/config \u2014 view or change server settings (/config help, /config keys)"},{kind:"bullet",text:"/service \u2014 background service status, install hints, logs (/service help)"},{kind:"bullet",text:"/dl <url> \u2014 download file (.pdf, .zip, \u2026) or video page (background; /dl help)"},{kind:"bullet",text:"/tr <url|path> \u2014 transcribe (background job)"},{kind:"bullet",text:"/edit <url|path> \u2014 trim or convert (background job; /edit help)"},{kind:"bullet",text:"/shortcut \u2014 this chat & shared shortcuts (/shortcut help); !name or /name expands chat override first, then shared"},{kind:"bullet",text:"/run \u2014 recipe-based task runs (/r); per-chat or gateway-shared /run add \u2014 /run list, /run help"},{kind:"bullet",text:"/cowork | /cw \u2014 scheduled & on-demand shell tasks; logs to ~/Cowork/\u2026 (/cowork help)"},{kind:"bullet",text:"/watch \u2014 OS event eye: fs/pkg/svc alerts; pause/stop/rm, excludes (/watch help; docs/features/watch.md)"},{kind:"gap"},{kind:"sub",text:"Setup & gateway"},{kind:"bullet",text:"/wa help \u2014 WhatsApp link, allowlist"},{kind:"bullet",text:"/tg help \u2014 Telegram; /tg token <paste>"},{kind:"bullet",text:"/reload | /restart \u2014 apply config"},{kind:"bullet",text:"/updates \u2014 npm latest + optional notice URL; /updates cached \u2014 last snapshot"},{kind:"bullet",text:"/security \u2014 posture report; /security summary; /security tips; /security help"},{kind:"bullet",text:"/gateway | /gw | /mode \u2014 show or set gatewayMode"},{kind:"bullet",text:"/allow +E164 | /allow tg:id \u2014 allowlist; /deny \u2026; /allowlist"},{kind:"bullet",text:"/docs search <topic> \u2014 find guides and next commands (/docs help)"},{kind:"bullet",text:"/help \u2014 this message"},{kind:"gap"},{kind:"sub",text:"Terms"},{kind:"bullet",text:"Gateway = omnish on this host; shell runs here. Standalone = messengers on this host; attached = communication layer routes chat here."},{kind:"bullet",text:'Communication layer (when shipped) = link chats once, device token on this CLI; shell stays here. Standalone = no layer. /service "platform" = OS.'}]}function oa(e){let t=e.mediaInstallFromChat?[{kind:"bullet",text:"/dl install [--whisper] \u2014 download tools into ~/.omnish/bin"}]:[{kind:"bullet",text:"/dl install \u2014 off from chat. Host: omnish pull install \xB7 or /config set mediaInstallFromChat true"}];return[{kind:"title",text:"Media \u2014 /dl, /tr, /edit"},{kind:"p",text:"Files are sent to chat by default. Opt out: /config set mediaSendFiles false (paths only)."},{kind:"bullet",text:"/dl <url> \u2014 direct HTTP for file URLs (.pdf, .zip, \u2026); yt-dlp for streaming pages (background)"},{kind:"bullet",text:"/tr <url|path> \u2014 Whisper transcript + .srt (+ video if URL); background job"},{kind:"bullet",text:"/edit <url|path> [--from 1:30] [--to 2:00] [--format mp3] [--audio-only] (background job)"},{kind:"bullet",text:"/dl doctor \xB7 /dl setup \xB7 /dl install"},...t,{kind:"bullet",text:e.mediaUrlAutoDl?"Lone URL in chat \u2192 /dl (file ext: HTTP; else yt-dlp)":"mediaUrlAutoDl off \u2014 use /dl <url>"},{kind:"bullet",text:"All media jobs run in background \u2014 /log, /tail; optional --notify on /dl, /tr, /edit"},{kind:"bullet",text:"Step progress on by default (progressUpdates); disable via /config set progressUpdates false"},{kind:"gap"},{kind:"p",text:"Host: omnish pull doctor \xB7 omnish pull install [--whisper]"},{kind:"p",text:"docs/features/media-commands.md"}]}function Tu(e){let t=e.serviceInstallFromChat?[{kind:"bullet",text:"/service install \u2014 user-level systemd (Linux) or LaunchAgent (macOS)"},{kind:"bullet",text:"/service uninstall \u2014 remove that unit"}]:[{kind:"bullet",text:"/service install / uninstall \u2014 off by default. Enable: /config set serviceInstallFromChat true (same trust as shell)"}];return[{kind:"title",text:"Service and boot"},{kind:"p",text:"Inspect the gateway, get OS-specific install steps, or (if enabled) write the user service from chat. Same from the host terminal: omnish service help."},{kind:"p",text:'"platform" in status means your OS (Linux/macOS/Windows), not a hosted omnish account.'},{kind:"bullet",text:"/service status \u2014 platform, data dir, pidfile, node + entry script"},{kind:"bullet",text:"/service instructions \u2014 copy-paste commands for this machine"},{kind:"bullet",text:"/service logs [n] \u2014 tail default gateway log (default 80 lines)"},...t,{kind:"gap"},{kind:"p",text:"See https://omnish.dev and docs/guides/background-and-boot.md."}]}function $u(){return[{kind:"title",text:"Chat config"},{kind:"p",text:`Same trust as shell \u2014 allowlisted senders can change many keys saved to ${_}.`},{kind:"gap"},{kind:"bullet",text:"/config show \u2014 full snapshot (telegramBotToken masked)"},{kind:"bullet",text:"/config get <key> \u2014 single field"},{kind:"bullet",text:"/config set <key> <value> \u2014 values can be quoted for paths with spaces"},{kind:"bullet",text:"/config keys \u2014 allowed keys for /config set (includes tunnel*, webhook*, watch*, pull*, chatLlm*, cluster*, \u2026)"},{kind:"gap"},{kind:"p",text:"After edits: /reload while omnish run is active (needed for gatewayMode and telegramBotToken)."}]}function ra(){return[{kind:"title",text:"Gateway mode"},{kind:"p",text:"Which transports omnish uses:"},{kind:"bullet",text:"whatsapp (wa, w) \u2014 WhatsApp DMs only"},{kind:"bullet",text:"telegram (tg, t) \u2014 Telegram bot only"},{kind:"bullet",text:"both (all, b) \u2014 WhatsApp + Telegram"},{kind:"gap"},{kind:"sub",text:"Commands"},{kind:"bullet",text:"/gateway \u2014 current mode + hints (includes last update snapshot if any)"},{kind:"bullet",text:"/gateway telegram \u2014 save (auto-reloads Telegram if gateway is up)"},{kind:"bullet",text:"/gw both \u2014 short form; /mode wa \u2014 same"},{kind:"gap"},{kind:"p",text:`Config: ${_}`}]}function Mu(e){let t=["*Gateway status*","",`Mode: *${e.gatewayMode}*`,"- whatsapp \u2014 WhatsApp only","- telegram \u2014 Telegram only","- both \u2014 both channels","",`WhatsApp auth: ${e.authPresent?"present":"missing (omnish link)"}`,`Telegram token: ${e.tokenSet?"set":"not set"}`,`Allowlists: ${e.allowN} WA \xB7 ${e.tgAllowN} Telegram`,""];e.updateBrief&&t.push(`Updates: ${Te(e.updateBrief)}`,""),t.push("Change: /gateway both \xB7 /gw tg","/updates \u2014 npm + optional notice URL",`Config: ${Te(_)}`);let n=["<b>Gateway status</b>","",`<b>${Z(e.gatewayMode)}</b> <i>current mode</i>`,"\u2022 whatsapp \u2014 WhatsApp only","\u2022 telegram \u2014 Telegram only","\u2022 both \u2014 both channels","",`${e.authPresent?"\u2713":"\u26A0"} WhatsApp auth: ${e.authPresent?"present":"missing \u2014 run omnish link"}`,`${e.tokenSet?"\u2713":"\u26A0"} Telegram token: ${e.tokenSet?"set":"not set"}`,`Allowlists: ${e.allowN} WA \xB7 ${e.tgAllowN} Telegram`,""];return e.updateBrief&&n.push(`<b>Updates</b> ${Z(e.updateBrief)}`,""),n.push("<i>Change:</i> /gateway both \xB7 /gw tg","<i>/updates</i> \u2014 npm + optional notice URL",Z(`Config: ${_}`)),we(t.join(`
38
38
  `),n.join(`
39
- `))}function Co(e){let t=["*Update check*","",`Running: *${Re(e.runningVersion)}*`,`Checked (UTC): ${e.checkedAtIso}`,`npm package: ${Re(e.registryPackage)}`];e.registryError?t.push("",`Registry: ${Re(e.registryError)}`):e.registryLatest&&t.push("",e.updateAvailable?`*Newer version on npm:* ${Re(e.registryLatest)} (upgrade when ready).`:`npm latest: ${Re(e.registryLatest)} (this install is current or newer).`),e.infoError?t.push("",`Notice URL: ${Re(e.infoError)}`):e.infoMessage&&(t.push("",`Notice: ${Re(e.infoMessage)}`),e.infoLink&&t.push(Re(e.infoLink))),t.push("","Config: updateCheckEnabled, updateCheckIntervalMs, updateCheckPackageName, updateInfoUrl");let n=["<b>Update check</b>","",`<b>Running</b> <code>${V(e.runningVersion)}</code>`,`<b>Checked (UTC)</b> ${V(e.checkedAtIso)}`,`<b>npm package</b> <code>${V(e.registryPackage)}</code>`];return e.registryError?n.push("",`<b>Registry</b> ${V(e.registryError)}`):e.registryLatest&&n.push("",e.updateAvailable?`<b>Newer on npm</b> <code>${V(e.registryLatest)}</code>`:`<b>npm latest</b> <code>${V(e.registryLatest)}</code> (current or newer here)`),e.infoError?n.push("",`<b>Notice URL</b> ${V(e.infoError)}`):e.infoMessage&&(n.push("",`<b>Notice</b> ${V(e.infoMessage)}`),e.infoLink&&n.push(V(e.infoLink))),n.push("","<i>Config keys:</i> updateCheckEnabled, updateCheckIntervalMs, updateCheckPackageName, updateInfoUrl"),fe(t.join(`
39
+ `))}function No(e){let t=["*Update check*","",`Running: *${Te(e.runningVersion)}*`,`Checked (UTC): ${e.checkedAtIso}`,`npm package: ${Te(e.registryPackage)}`];e.registryError?t.push("",`Registry: ${Te(e.registryError)}`):e.registryLatest&&t.push("",e.updateAvailable?`*Newer version on npm:* ${Te(e.registryLatest)} (upgrade when ready).`:`npm latest: ${Te(e.registryLatest)} (this install is current or newer).`),e.infoError?t.push("",`Notice URL: ${Te(e.infoError)}`):e.infoMessage&&(t.push("",`Notice: ${Te(e.infoMessage)}`),e.infoLink&&t.push(Te(e.infoLink))),t.push("","Config: updateCheckEnabled, updateCheckIntervalMs, updateCheckPackageName, updateInfoUrl");let n=["<b>Update check</b>","",`<b>Running</b> <code>${Z(e.runningVersion)}</code>`,`<b>Checked (UTC)</b> ${Z(e.checkedAtIso)}`,`<b>npm package</b> <code>${Z(e.registryPackage)}</code>`];return e.registryError?n.push("",`<b>Registry</b> ${Z(e.registryError)}`):e.registryLatest&&n.push("",e.updateAvailable?`<b>Newer on npm</b> <code>${Z(e.registryLatest)}</code>`:`<b>npm latest</b> <code>${Z(e.registryLatest)}</code> (current or newer here)`),e.infoError?n.push("",`<b>Notice URL</b> ${Z(e.infoError)}`):e.infoMessage&&(n.push("",`<b>Notice</b> ${Z(e.infoMessage)}`),e.infoLink&&n.push(Z(e.infoLink))),n.push("","<i>Config keys:</i> updateCheckEnabled, updateCheckIntervalMs, updateCheckPackageName, updateInfoUrl"),we(t.join(`
40
40
  `),n.join(`
41
- `))}function iu(e){return[{kind:"title",text:"WhatsApp setup"},{kind:"p",text:"Do these on the machine running omnish (QR is shown in the terminal, not in chat)."},{kind:"gap"},{kind:"sub",text:"Steps"},{kind:"bullet",text:"Install omnish; build native deps (node-pty, Baileys)."},{kind:"bullet",text:`Data dir: ${W} (override: OMNISH_HOME). Auth: <data-dir>/auth/`},{kind:"bullet",text:"Run: omnish link \u2014 scan QR in WhatsApp \u2192 Linked devices."},{kind:"bullet",text:"Allow your number: omnish allow +<E164>"},{kind:"bullet",text:`Set gatewayMode to "whatsapp" or "both" in ${_}`},{kind:"bullet",text:"Run: omnish run \u2014 then use !cmd or /help here."},{kind:"gap"},{kind:"sub",text:"Troubleshooting"},{kind:"bullet",text:"Denied in logs but allowlist OK: may be LID mapping \u2014 try omnish run --verbose."},{kind:"bullet",text:"401 on link: omnish link --force after pnpm approve-builds && pnpm install"},{kind:"gap"},{kind:"p",text:`Current gatewayMode: ${e.gatewayMode}`}]}function au(e){let t=!!Me(e);return[{kind:"title",text:"Telegram setup"},{kind:"p",text:"Configure on the host that runs omnish."},{kind:"gap"},{kind:"sub",text:"Steps"},{kind:"bullet",text:"@BotFather \u2192 /newbot \u2014 copy the API token."},{kind:"bullet",text:`Token: /tg token <paste>, or ${_}, or TELEGRAM_BOT_TOKEN env (env wins).`},{kind:"bullet",text:'Your numeric id: @userinfobot / @getidsbot, or logs on "telegram denied".'},{kind:"bullet",text:"Allow: omnish allow tg:<user_id>"},{kind:"bullet",text:`gatewayMode: "telegram" or "both" in ${_}; list ids in telegramAllowFrom.`},{kind:"bullet",text:"omnish run \u2014 then DM the bot with !cmd or /help."},{kind:"bullet",text:"After edits: /reload or /gateway both (saves + reloads when running)."},{kind:"gap"},{kind:"sub",text:"Notes"},{kind:"bullet",text:"Private text DMs only; groups ignored. Token stays secret."},{kind:"bullet",text:"/tg token stores token in config; chat history may retain it \u2014 prefer SSH/editor if worried."},{kind:"gap"},{kind:"p",text:`Token on host: ${t?"set (env or config)":"not set \u2014 add telegramBotToken or TELEGRAM_BOT_TOKEN"}`},{kind:"p",text:`gatewayMode: ${e.gatewayMode} \xB7 telegramAllowFrom: ${e.telegramAllowFrom.length} entries`}]}function lu(e){let t=["*Allowlists*",""],n=["<b>Allowlists</b>",""];for(let o of e){let r=o.items.length?o.items.join(", "):"(none)";t.push(`*${o.label}* (${o.items.length})`,Re(r),""),n.push(`<b>${V(o.label)}</b> (${o.items.length})`,V(r),"")}return fe(t.join(`
41
+ `))}function Pu(e){return[{kind:"title",text:"WhatsApp setup"},{kind:"p",text:"Do these on the machine running omnish (QR is shown in the terminal, not in chat)."},{kind:"gap"},{kind:"sub",text:"Steps"},{kind:"bullet",text:"Install omnish; build native deps (node-pty, Baileys)."},{kind:"bullet",text:`Data dir: ${D} (override: OMNISH_HOME). Auth: <data-dir>/auth/`},{kind:"bullet",text:"Run: omnish link \u2014 scan QR in WhatsApp \u2192 Linked devices."},{kind:"bullet",text:"Allow your number: omnish allow +<E164>"},{kind:"bullet",text:`Set gatewayMode to "whatsapp" or "both" in ${_}`},{kind:"bullet",text:"Run: omnish run \u2014 then use !cmd or /help here."},{kind:"gap"},{kind:"sub",text:"Troubleshooting"},{kind:"bullet",text:"Denied in logs but allowlist OK: may be LID mapping \u2014 try omnish run --verbose."},{kind:"bullet",text:"401 on link: omnish link --force after pnpm approve-builds && pnpm install"},{kind:"gap"},{kind:"p",text:`Current gatewayMode: ${e.gatewayMode}`}]}function Eu(e){let t=!!Me(e);return[{kind:"title",text:"Telegram setup"},{kind:"p",text:"Configure on the host that runs omnish."},{kind:"gap"},{kind:"sub",text:"Steps"},{kind:"bullet",text:"@BotFather \u2192 /newbot \u2014 copy the API token."},{kind:"bullet",text:`Token: /tg token <paste>, or ${_}, or TELEGRAM_BOT_TOKEN env (env wins).`},{kind:"bullet",text:'Your numeric id: @userinfobot / @getidsbot, or logs on "telegram denied".'},{kind:"bullet",text:"Allow: omnish allow tg:<user_id>"},{kind:"bullet",text:`gatewayMode: "telegram" or "both" in ${_}; list ids in telegramAllowFrom.`},{kind:"bullet",text:"omnish run \u2014 then DM the bot with !cmd or /help."},{kind:"bullet",text:"After edits: /reload or /gateway both (saves + reloads when running)."},{kind:"gap"},{kind:"sub",text:"Notes"},{kind:"bullet",text:"Private text DMs only; groups ignored. Token stays secret."},{kind:"bullet",text:"/tg token stores token in config; chat history may retain it \u2014 prefer SSH/editor if worried."},{kind:"gap"},{kind:"p",text:`Token on host: ${t?"set (env or config)":"not set \u2014 add telegramBotToken or TELEGRAM_BOT_TOKEN"}`},{kind:"p",text:`gatewayMode: ${e.gatewayMode} \xB7 telegramAllowFrom: ${e.telegramAllowFrom.length} entries`}]}function Au(e){let t=["*Allowlists*",""],n=["<b>Allowlists</b>",""];for(let o of e){let r=o.items.length?o.items.join(", "):"(none)";t.push(`*${o.label}* (${o.items.length})`,Te(r),""),n.push(`<b>${Z(o.label)}</b> (${o.items.length})`,Z(r),"")}return we(t.join(`
42
42
  `).trimEnd(),n.join(`
43
- `).trimEnd())}function Ui(e){let t=[{label:"allowFrom (WhatsApp)",items:e.allowFrom},{label:"telegramAllowFrom",items:e.telegramAllowFrom}],n=["*Allowlist updated*","","Saved to config.",""],o=["<b>Allowlist updated</b>","","Saved to config.",""];for(let r of t){let s=r.items.length?r.items.join(", "):"(none)";n.push(`*${r.label}* (${r.items.length})`,Re(s),""),o.push(`<b>${V(r.label)}</b> (${r.items.length})`,V(s),"")}return fe(n.join(`
43
+ `).trimEnd())}function sa(e){let t=[{label:"allowFrom (WhatsApp)",items:e.allowFrom},{label:"telegramAllowFrom",items:e.telegramAllowFrom}],n=["*Allowlist updated*","","Saved to config.",""],o=["<b>Allowlist updated</b>","","Saved to config.",""];for(let r of t){let s=r.items.length?r.items.join(", "):"(none)";n.push(`*${r.label}* (${r.items.length})`,Te(s),""),o.push(`<b>${Z(r.label)}</b> (${r.items.length})`,Z(s),"")}return we(n.join(`
44
44
  `).trimEnd(),o.join(`
45
- `).trimEnd())}function cu(e){let t=["*Token saved*","","telegramBotToken written to config (not echoed).",`Config: ${Re(_)}`,...e.flatMap(o=>["",o]),"","Send /reload so the gateway picks it up."].join(`
46
- `),n=["<b>Token saved</b>","","telegramBotToken written to config (not echoed).",V(`Config: ${_}`),...e.map(o=>`<i>${V(o)}</i>`),"","Send /reload so the gateway picks it up."].join(`
45
+ `).trimEnd())}function Iu(e){let t=["*Token saved*","","telegramBotToken written to config (not echoed).",`Config: ${Te(_)}`,...e.flatMap(o=>["",o]),"","Send /reload so the gateway picks it up."].join(`
46
+ `),n=["<b>Token saved</b>","","telegramBotToken written to config (not echoed).",Z(`Config: ${_}`),...e.map(o=>`<i>${Z(o)}</i>`),"","Send /reload so the gateway picks it up."].join(`
47
47
  `)+`
48
- `;return fe(t.trimEnd(),n.trimEnd())}function uu(){return X([{kind:"title",text:"That does not look like a bot token"},{kind:"p",text:"Expected from @BotFather: one token, no spaces, like 123456789:AA_heG\u2026"},{kind:"bullet",text:"Example: /tg token 123456789:AAAbcd\u2026"}])}function du(e,t,n){let o=t?`
48
+ `;return we(t.trimEnd(),n.trimEnd())}function Lu(){return ee([{kind:"title",text:"That does not look like a bot token"},{kind:"p",text:"Expected from @BotFather: one token, no spaces, like 123456789:AA_heG\u2026"},{kind:"bullet",text:"Example: /tg token 123456789:AAAbcd\u2026"}])}function Ou(e,t,n){let o=t?`
49
49
 
50
50
  ${t}`:n?`
51
51
 
52
52
  Start omnish run on the host to apply (or /reload from a running gateway).`:"",r=t?`
53
53
 
54
- ${V(t)}`:n?`
54
+ ${Z(t)}`:n?`
55
55
 
56
56
  Start omnish run on the host to apply (or /reload from a running gateway).`:"",s=`*gatewayMode saved*
57
57
 
58
- "${Re(e)}"
59
- ${Re(_)}${o}`,i=`<b>gatewayMode saved</b>
58
+ "${Te(e)}"
59
+ ${Te(_)}${o}`,i=`<b>gatewayMode saved</b>
60
60
 
61
- <code>${V(e)}</code>
62
- ${V(_)}${r}`;return fe(s,i)}function pu(){return X([{kind:"title",text:"/allow \u2014 add to allowlist"},{kind:"bullet",text:"/allow +<E164> \u2014 WhatsApp (country code, no spaces)"},{kind:"bullet",text:"/allow tg:<user_id> \u2014 Telegram numeric id"},{kind:"gap"},{kind:"p",text:"Examples: /allow +15551234567 \xB7 /allow tg:987654321"}])}function mu(){return X([{kind:"title",text:"/deny \u2014 remove from allowlist"},{kind:"p",text:"Same forms as /allow: +E164 or tg:id"}])}function hu(){return X([{kind:"title",text:"/bg \u2014 background job"},{kind:"p",text:"Usage: /bg [flags] <shell command>"},{kind:"bullet",text:"Optional name: /bg -n mybuild npm run build \xB7 /bg --name mybuild \u2026 \xB7 /bg --name=mybuild \u2026"},{kind:"bullet",text:"Notify on completion: /bg --notify <cmd> or /bg -N <cmd> \u2014 sends a message when the job finishes (with exit status)."},{kind:"bullet",text:"Combine flags: /bg -N -n deploy git pull && docker compose up -d"},{kind:"bullet",text:"Then use /log mybuild, /tail mybuild, /kill mybuild (or the 8-char id). Names: letters, digits, . _ - up to 64 chars."},{kind:"bullet",text:"Example: /bg sleep 30 && echo done"}])}function Bi(){return X([{kind:"title",text:"/send \u2014 push a host file to this chat"},{kind:"p",text:"Usage: /send <selectors> [-- caption] \u2014 selectors resolve from session cwd."},{kind:"bullet",text:"/send ./photo.png"},{kind:"bullet",text:"/send ./clip1.mp4,./clip2.mp4 -- Launch set"},{kind:"bullet",text:"/send **/*.mp4"},{kind:"bullet",text:"/send /abs/path/report.pdf -- Q4 draft"},{kind:"bullet",text:"Selectors: file1,file2 | *.ext | **/*.ext"},{kind:"bullet",text:"Alias: /file \u2026"},{kind:"gap"},{kind:"p",text:"Caps: fileSendMaxBytes / fileReceiveMaxBytes (0 = no omnish cap). Where inbound media is saved: fileReceiveRootMode + fileReceiveRootPath / fileInboxSubdir \u2014 send /files for details."}])}function Hi(){return X([{kind:"title",text:"Files \u2014 send & receive"},{kind:"sub",text:"Host \u2192 chat"},{kind:"bullet",text:"/send <selectors> or /file \u2026 \u2014 selectors support file1,file2, *.ext, **/*.ext from this chat\u2019s session cwd (same as ! shell); optional caption after -- "},{kind:"bullet",text:"fileSendMaxBytes in config caps outbound size (0 = no omnish cap)."},{kind:"gap"},{kind:"sub",text:"Chat \u2192 host"},{kind:"bullet",text:"Send a photo, video, document, audio, etc. in this DM; omnish saves it and replies with Saved: <path> (or an error)."},{kind:"bullet",text:"Folders: <root>/<peer>/<YYYY-MM-DD>/<filename> \u2014 root comes from config below."},{kind:"bullet",text:"fileReceiveRootMode: downloads (home/Downloads/Omnish) \xB7 omnishData (data dir + fileInboxSubdir) \xB7 sessionCwd \xB7 processCwd \xB7 fixed (needs absolute fileReceiveRootPath)."},{kind:"bullet",text:"fileReceiveMaxBytes \u2014 inbound size cap (0 = no omnish cap)."},{kind:"gap"},{kind:"p",text:`Edit config on the host (${_}) then use /reload. Tip: omnish status shows the data directory.`},{kind:"gap"},{kind:"p",text:"Per-chat folder: /receive here saves inbound files under your session cwd (!cd); /receive default clears."}])}function ji(){return X([{kind:"title",text:"/receive \u2014 inbound files for this chat"},{kind:"p",text:"Stored on the host with your session (sessions.json). Does not edit config.json."},{kind:"bullet",text:"/receive \u2014 show current setting and resolved save folder"},{kind:"bullet",text:"/receive here \u2014 save uploads under this chat\u2019s session cwd (same as !cd); subfolders: peer / date / file"},{kind:"bullet",text:"/receive default \u2014 clear per-chat rule; use global fileReceiveRootMode (/files, config.json)"},{kind:"gap"},{kind:"p",text:"Aliases: here = cwd = session = dir \xB7 default = global = reset"}])}function fu(e,t){let n=qc(t),o=ie(t),r="";try{r=Jt(e,t)}catch(s){r=`(${String(s)})`}return X(n==="sessionCwd"?[{kind:"title",text:"/receive \u2014 this chat"},{kind:"p",text:"Per-chat: inbound media saves under your session folder (updated when you !cd)."},{kind:"bullet",text:`Session cwd: ${o.cwd}`},{kind:"bullet",text:`Next file root: ${r}`},{kind:"p",text:"Send /receive default to follow server config instead."}]:[{kind:"title",text:"/receive \u2014 this chat"},{kind:"p",text:"Per-chat override: off \u2014 using global fileReceiveRootMode from config."},{kind:"bullet",text:`Global mode: ${e.fileReceiveRootMode}`},{kind:"bullet",text:`Next file root: ${r}`},{kind:"p",text:"Send /receive here to pin saves to your current session folder."}])}function gu(){return X([{kind:"title",text:"Unknown /wa command"},{kind:"p",text:"Send /wa help for setup."}])}function yu(){return X([{kind:"title",text:"Unknown /tg command"},{kind:"p",text:"Send /tg help or /tg token <botfather_token>."}])}function wu(){return X([{kind:"title",text:"Free shell mode on"},{kind:"p",text:"Plain messages run as sync shell (no command prefix)."},{kind:"bullet",text:"Send !!stop to turn off."},{kind:"bullet",text:"Apps: plain DMs no longer go to the focused app \u2014 use >name text or /apps send."}])}function bu(e){return X([{kind:"title",text:"Unknown command"},{kind:"p",text:`Try /help or ${e.commandPrefix}<command>.`},{kind:"gap"},..._n(e)])}function ig(e){let t=e.trim();return t.length<4||/^[/!>]/.test(t)?null:t.includes("?")||/\s/.test(t)?`Looking for how to do something? Try /docs search ${t.length>48?`${t.slice(0,48)}\u2026`:t}`:null}function ku(e,t){let n=t?ig(t):null,o=[{kind:"title",text:"No command matched"},{kind:"p",text:`Use ${JSON.stringify(e.commandPrefix)}, a slash command, or /apps help.`}];return n&&o.push({kind:"p",text:n}),o.push({kind:"gap"},..._n(e)),X(o)}function vu(){let e=[{kind:"title",text:"Unknown mode"},{kind:"p",text:"Use: whatsapp | telegram | both (short: wa, tg, b)"},{kind:"bullet",text:"/gateway both \xB7 /gw wa \xB7 /mode telegram"},{kind:"gap"},...Di()];return X(e)}function Gi(){return[{kind:"title",text:"Shortcuts (this gateway)"},{kind:"p",text:"Stored per-chat or shared for every chat on this gateway. Expansion: this chat wins, then shared. Bare token only \u2014 for task injection use /run recipes."},{kind:"p",text:"Scope flags: -g or --global = shared on this gateway; -p or --chat = private to this chat."},{kind:"gap"},{kind:"sub",text:"Manage"},{kind:"bullet",text:"/shortcut add <name> <command\u2026> \u2014 private by default; add -g|--global to share on this gateway"},{kind:"bullet",text:"/shortcut add -p|--chat <name> <command\u2026> \u2014 explicit private (this chat)"},{kind:"bullet",text:"/shortcut set <name> <command\u2026> \u2014 overwrite in chosen bucket; scope-only (same line): /shortcut set -g <name> or /shortcut set <name> -g (share); /shortcut set -p <name> or /shortcut set <name> -p (private)"},{kind:"bullet",text:"/shortcut list \u2014 merged view (/shortcuts); list --chat | -p | list --global | -g"},{kind:"bullet",text:"/shortcut show <name>; show --global|-g|--chat|-p to read one bucket"},{kind:"bullet",text:"/shortcut remove <name> \u2014 private bucket; remove --global|-g drops shared \xB7 rm, del \xB7 --chat|-p"},{kind:"bullet",text:"/shortcut <name> publish \u2014 share to online catalog (platform login)"},{kind:"bullet",text:"/shortcut online trending | show <publicId> | <publicId> download \u2014 shortcuts only"},{kind:"bullet",text:"/alias \u2026 \u2014 same as /shortcut \u2026 (including /aliases \u2026)"},{kind:"gap"},{kind:"sub",text:"Run"},{kind:"bullet",text:"!name \u2014 expands once to the saved line (e.g. !cd \u2026 updates session cwd)"},{kind:"bullet",text:"/name \u2014 same expansion for slash-style navigation"},{kind:"p",text:"Shortcut bodies may start with !, /, etc.; nested shortcuts are not expanded."}]}function Su(e){if(e.length===0)return p("(no shortcuts \u2014 /shortcut add <name> <command\u2026> or /shortcut add --global <name> <command\u2026>)");let t=e.every(l=>l.scope==="global"),n=e.every(l=>l.scope==="chat"),o=t?"_Shared (every chat)_":n?"_This chat only_":"_This chat + shared_",r=t?"<i>Shared (every chat)</i>":n?"<i>This chat only</i>":"<i>This chat + shared</i>",s=e.some(l=>l.scope==="chat")&&e.some(l=>l.scope==="global"),i=["*Shortcuts*",o,"",...e.map(l=>{let d=s?l.scope==="chat"?"[chat] ":"[global] ":"";return`${Ae}\`${d}${l.name}\` \u2192 ${l.body}`})].join(`
63
- `),a=["<b>Shortcuts</b>",r,"",...e.map(l=>{let u=`${s?l.scope==="chat"?"[chat] ":"[global] ":""}${l.name}`;return`\u2022 <code>${V(u)}</code> \u2192 ${V(l.body)}`})].join(`
64
- `);return fe(i,a)}function Ro(e,t,n="chat"){return p(`Shortcut saved: ${e}
61
+ <code>${Z(e)}</code>
62
+ ${Z(_)}${r}`;return we(s,i)}function Nu(){return ee([{kind:"title",text:"/allow \u2014 add to allowlist"},{kind:"bullet",text:"/allow +<E164> \u2014 WhatsApp (country code, no spaces)"},{kind:"bullet",text:"/allow tg:<user_id> \u2014 Telegram numeric id"},{kind:"gap"},{kind:"p",text:"Examples: /allow +15551234567 \xB7 /allow tg:987654321"}])}function Fu(){return ee([{kind:"title",text:"/deny \u2014 remove from allowlist"},{kind:"p",text:"Same forms as /allow: +E164 or tg:id"}])}function Wu(){return ee([{kind:"title",text:"/bg \u2014 background job"},{kind:"p",text:"Usage: /bg [flags] <shell command>"},{kind:"bullet",text:"Optional name: /bg -n mybuild npm run build \xB7 /bg --name mybuild \u2026 \xB7 /bg --name=mybuild \u2026"},{kind:"bullet",text:"Notify on completion: /bg --notify <cmd> or /bg -N <cmd> \u2014 sends a message when the job finishes (with exit status)."},{kind:"bullet",text:"Combine flags: /bg -N -n deploy git pull && docker compose up -d"},{kind:"bullet",text:"Then use /log mybuild, /tail mybuild, /kill mybuild (or the 8-char id). Names: letters, digits, . _ - up to 64 chars."},{kind:"bullet",text:"Example: /bg sleep 30 && echo done"}])}function ia(){return ee([{kind:"title",text:"/send \u2014 push a host file to this chat"},{kind:"p",text:"Usage: /send <selectors> [-- caption] \u2014 selectors resolve from session cwd."},{kind:"bullet",text:"/send ./photo.png"},{kind:"bullet",text:"/send ./clip1.mp4,./clip2.mp4 -- Launch set"},{kind:"bullet",text:"/send **/*.mp4"},{kind:"bullet",text:"/send /abs/path/report.pdf -- Q4 draft"},{kind:"bullet",text:"Selectors: file1,file2 | *.ext | **/*.ext"},{kind:"bullet",text:"Alias: /file \u2026"},{kind:"gap"},{kind:"p",text:"Caps: fileSendMaxBytes / fileReceiveMaxBytes (0 = no omnish cap). Where inbound media is saved: fileReceiveRootMode + fileReceiveRootPath / fileInboxSubdir \u2014 send /files for details."}])}function aa(){return ee([{kind:"title",text:"Files \u2014 send & receive"},{kind:"sub",text:"Host \u2192 chat"},{kind:"bullet",text:"/send <selectors> or /file \u2026 \u2014 selectors support file1,file2, *.ext, **/*.ext from this chat\u2019s session cwd (same as ! shell); optional caption after -- "},{kind:"bullet",text:"fileSendMaxBytes in config caps outbound size (0 = no omnish cap)."},{kind:"gap"},{kind:"sub",text:"Chat \u2192 host"},{kind:"bullet",text:"Send a photo, video, document, audio, etc. in this DM; omnish saves it and replies with Saved: <path> (or an error)."},{kind:"bullet",text:"Folders: <root>/<peer>/<YYYY-MM-DD>/<filename> \u2014 root comes from config below."},{kind:"bullet",text:"fileReceiveRootMode: downloads (home/Downloads/Omnish) \xB7 omnishData (data dir + fileInboxSubdir) \xB7 sessionCwd \xB7 processCwd \xB7 fixed (needs absolute fileReceiveRootPath)."},{kind:"bullet",text:"fileReceiveMaxBytes \u2014 inbound size cap (0 = no omnish cap)."},{kind:"gap"},{kind:"p",text:`Edit config on the host (${_}) then use /reload. Tip: omnish status shows the data directory.`},{kind:"gap"},{kind:"p",text:"Per-chat folder: /receive here saves inbound files under your session cwd (!cd); /receive default clears."}])}function la(){return ee([{kind:"title",text:"/receive \u2014 inbound files for this chat"},{kind:"p",text:"Stored on the host with your session (sessions.json). Does not edit config.json."},{kind:"bullet",text:"/receive \u2014 show current setting and resolved save folder"},{kind:"bullet",text:"/receive here \u2014 save uploads under this chat\u2019s session cwd (same as !cd); subfolders: peer / date / file"},{kind:"bullet",text:"/receive default \u2014 clear per-chat rule; use global fileReceiveRootMode (/files, config.json)"},{kind:"gap"},{kind:"p",text:"Aliases: here = cwd = session = dir \xB7 default = global = reset"}])}function _u(e,t){let n=hu(t),o=ae(t),r="";try{r=Qt(e,t)}catch(s){r=`(${String(s)})`}return ee(n==="sessionCwd"?[{kind:"title",text:"/receive \u2014 this chat"},{kind:"p",text:"Per-chat: inbound media saves under your session folder (updated when you !cd)."},{kind:"bullet",text:`Session cwd: ${o.cwd}`},{kind:"bullet",text:`Next file root: ${r}`},{kind:"p",text:"Send /receive default to follow server config instead."}]:[{kind:"title",text:"/receive \u2014 this chat"},{kind:"p",text:"Per-chat override: off \u2014 using global fileReceiveRootMode from config."},{kind:"bullet",text:`Global mode: ${e.fileReceiveRootMode}`},{kind:"bullet",text:`Next file root: ${r}`},{kind:"p",text:"Send /receive here to pin saves to your current session folder."}])}function Du(){return ee([{kind:"title",text:"Unknown /wa command"},{kind:"p",text:"Send /wa help for setup."}])}function Uu(){return ee([{kind:"title",text:"Unknown /tg command"},{kind:"p",text:"Send /tg help or /tg token <botfather_token>."}])}function Bu(){return ee([{kind:"title",text:"Free shell mode on"},{kind:"p",text:"Plain messages run as sync shell (no command prefix)."},{kind:"bullet",text:"Send !!stop to turn off."},{kind:"bullet",text:"Apps: plain DMs no longer go to the focused app \u2014 use >name text or /apps send."}])}function Hu(e){return ee([{kind:"title",text:"Unknown command"},{kind:"p",text:`Try /help or ${e.commandPrefix}<command>.`},{kind:"gap"},...Kn(e)])}function Xg(e){let t=e.trim();return t.length<4||/^[/!>]/.test(t)?null:t.includes("?")||/\s/.test(t)?`Looking for how to do something? Try /docs search ${t.length>48?`${t.slice(0,48)}\u2026`:t}`:null}function ju(e,t){let n=t?Xg(t):null,o=[{kind:"title",text:"No command matched"},{kind:"p",text:`Use ${JSON.stringify(e.commandPrefix)}, a slash command, or /apps help.`}];return n&&o.push({kind:"p",text:n}),o.push({kind:"gap"},...Kn(e)),ee(o)}function Gu(){let e=[{kind:"title",text:"Unknown mode"},{kind:"p",text:"Use: whatsapp | telegram | both (short: wa, tg, b)"},{kind:"bullet",text:"/gateway both \xB7 /gw wa \xB7 /mode telegram"},{kind:"gap"},...ra()];return ee(e)}function ca(){return[{kind:"title",text:"Shortcuts (this gateway)"},{kind:"p",text:"Stored per-chat or shared for every chat on this gateway. Expansion: this chat wins, then shared. Bare token only \u2014 for task injection use /run recipes."},{kind:"p",text:"Scope flags: -g or --global = shared on this gateway; -p or --chat = private to this chat."},{kind:"gap"},{kind:"sub",text:"Manage"},{kind:"bullet",text:"/shortcut add <name> <command\u2026> \u2014 private by default; add -g|--global to share on this gateway"},{kind:"bullet",text:"/shortcut add -p|--chat <name> <command\u2026> \u2014 explicit private (this chat)"},{kind:"bullet",text:"/shortcut set <name> <command\u2026> \u2014 overwrite in chosen bucket; scope-only (same line): /shortcut set -g <name> or /shortcut set <name> -g (share); /shortcut set -p <name> or /shortcut set <name> -p (private)"},{kind:"bullet",text:"/shortcut list \u2014 merged view (/shortcuts); list --chat | -p | list --global | -g"},{kind:"bullet",text:"/shortcut show <name>; show --global|-g|--chat|-p to read one bucket"},{kind:"bullet",text:"/shortcut remove <name> \u2014 private bucket; remove --global|-g drops shared \xB7 rm, del \xB7 --chat|-p"},{kind:"bullet",text:"/shortcut <name> publish \u2014 share to online catalog (platform login)"},{kind:"bullet",text:"/shortcut online trending | show <publicId> | <publicId> download \u2014 shortcuts only"},{kind:"bullet",text:"/alias \u2026 \u2014 same as /shortcut \u2026 (including /aliases \u2026)"},{kind:"gap"},{kind:"sub",text:"Run"},{kind:"bullet",text:"!name \u2014 expands once to the saved line (e.g. !cd \u2026 updates session cwd)"},{kind:"bullet",text:"/name \u2014 same expansion for slash-style navigation"},{kind:"p",text:"Shortcut bodies may start with !, /, etc.; nested shortcuts are not expanded."}]}function Ju(e){if(e.length===0)return p("(no shortcuts \u2014 /shortcut add <name> <command\u2026> or /shortcut add --global <name> <command\u2026>)");let t=e.every(l=>l.scope==="global"),n=e.every(l=>l.scope==="chat"),o=t?"_Shared (every chat)_":n?"_This chat only_":"_This chat + shared_",r=t?"<i>Shared (every chat)</i>":n?"<i>This chat only</i>":"<i>This chat + shared</i>",s=e.some(l=>l.scope==="chat")&&e.some(l=>l.scope==="global"),i=["*Shortcuts*",o,"",...e.map(l=>{let c=s?l.scope==="chat"?"[chat] ":"[global] ":"";return`${Ae}\`${c}${l.name}\` \u2192 ${l.body}`})].join(`
63
+ `),a=["<b>Shortcuts</b>",r,"",...e.map(l=>{let d=`${s?l.scope==="chat"?"[chat] ":"[global] ":""}${l.name}`;return`\u2022 <code>${Z(d)}</code> \u2192 ${Z(l.body)}`})].join(`
64
+ `);return we(i,a)}function Fo(e,t,n="chat"){return p(`Shortcut saved: ${e}
65
65
  \u2192 ${t}
66
- (${n==="global"?"Shared on this gateway (every chat unless this chat overrides the name).":"Stored for this chat only."})`)}function xu(e,t="chat"){return p(`Shortcut removed (${t==="global"?"shared":"this chat"}): ${e}`)}function Cu(e){return p(`Unknown shortcut: ${e}`)}function Ru(e,t){return p(`Unknown shortcut "${e}" in ${t==="global"?"shared shortcuts":"this chat"}.`)}function qi(e,t,n){let o=n?`
66
+ (${n==="global"?"Shared on this gateway (every chat unless this chat overrides the name).":"Stored for this chat only."})`)}function qu(e,t="chat"){return p(`Shortcut removed (${t==="global"?"shared":"this chat"}): ${e}`)}function zu(e){return p(`Unknown shortcut: ${e}`)}function Ku(e,t){return p(`Unknown shortcut "${e}" in ${t==="global"?"shared shortcuts":"this chat"}.`)}function ua(e,t,n){let o=n?`
67
67
 
68
68
  ${n}`:"";return p(`${e}
69
- \u2192 ${t}${o}`)}function Tu(){return[{kind:"title",text:"/run \u2014 recipe templates"},{kind:"sub",text:"Invoke"},{kind:"bullet",text:"/run <name> <task\u2026> \u2014 start detached session by default (output muted; /apps attach or >name for input)"},{kind:"bullet",text:"/run <name> --attach|-a <task\u2026> \u2014 attach on start (plain DMs go to session until /apps detach); --detach|-d forces detached"},{kind:"bullet",text:"/r <name> <task\u2026> \u2014 short alias for /run"},{kind:"gap"},{kind:"sub",text:"Queue"},{kind:"bullet",text:"/run <name> -q <task\u2026> or /run <name> --queue <task\u2026> \u2014 FIFO per chat: head starts immediately; others wait in memory until the head exits cleanly (code=0, signal=0)"},{kind:"bullet",text:"Pending counts only jobs not yet started \u2014 the first queued item becomes Active right away, so /run queue can show Pending: 0 while a queued run is still executing"},{kind:"bullet",text:"/run queue \u2014 active session + recipe, numbered waiting list, paused flag"},{kind:"bullet",text:"/run queue resume \u2014 after a pause or non-clean exit, clear pause and start the next waiting item"},{kind:"bullet",text:"/run queue load <file.json> \u2014 enqueue jobs from JSON (paths relative to session cwd); /run queue load json [\u2026] \u2014 same payload inline; attach a file with caption /run queue load"},{kind:"bullet",text:'Queue JSON: [ { "recipe": "<name>", "task": "<text>" }, \u2026 ] or { "tasks": [ \u2026 ] } (max 64 jobs per load; same rules as /run <name> -q)'},{kind:"gap"},{kind:"sub",text:"Discover"},{kind:"bullet",text:"/run list \u2014 featured, gateway-shared, this chat, host templates; list --chat | -p | list --global | -g"},{kind:"bullet",text:"/run show <name> \u2014 merged resolution; show --global|-g|--chat|-p reads one user bucket"},{kind:"gap"},{kind:"sub",text:"Manage"},{kind:"p",text:"Recipe scope flags: -g or --global = shared on this gateway; -p or --chat = private to this chat."},{kind:"bullet",text:'/run add <name> <command\u2026> [--template "\u2026"] \u2014 this chat; add --global|-g share on this gateway'},{kind:"bullet",text:"/run set <name> <command\u2026> \u2014 overwrite in chosen bucket (--global|-g|--chat|-p); scope-only (same body): /run set -g <name> or /run set <name> -g (share); /run set -p <name> or /run set <name> -p (this chat only)"},{kind:"bullet",text:"/run remove <name> \u2014 this chat storage; remove --global|-g clears gateway-shared \xB7 rm, del \xB7 --chat|-p"},{kind:"gap"},{kind:"sub",text:"Notes"},{kind:"bullet",text:"Commands must reference the task env variable (default `$OMNISH_TASK`)."},{kind:"bullet",text:"Template placeholders `<<<OMNISH_TASK>>>`, `<<<```$OMNISH_TASK```>>>`, and `$OMNISH_TASK` are replaced with task text."},{kind:"bullet",text:"Host overrides file: "+ar},{kind:"bullet",text:"Built-in Claude recipes omit dangerous flags unless recipesAllowDangerousBuiltins=true."},{kind:"gap"},{kind:"sub",text:"Online catalog"},{kind:"bullet",text:"/run online trending | search <query> | show <publicId> | <publicId> download \u2014 all kinds"},{kind:"bullet",text:"/run <recipe> publish \u2014 share to platform (requires omnish platform login)"}]}function ag(e){switch(e){case"builtin":return"built-in";case"global":return"host recipes.json";case"shared":return"gateway-shared (/run add --global)";case"peer":return"this chat (/run add)";default:return e}}function $u(e){let t=["*Recipes*","`/run <name> <task>`",""],n=["<b>Recipes</b>","<code>/run &lt;name&gt; &lt;task&gt;</code>",""],o=(r,s)=>{let i=r.description?` \u2014 ${r.description}`:"",a=s&&r.dangerous?" [dangerous flags]":"";t.push(`${Ae}${r.name} \u2014 ${r.label??r.name}${i}${a}`);let l=r.description?` \u2014 ${V(r.description)}`:"",d=s&&r.dangerous?V(" [dangerous flags]"):"";n.push(`\u2022 ${V(r.name)} \u2014 ${V(r.label??r.name)}${l}${d}`)};if(e.featured.length>0){t.push("_Featured:_"),n.push("<i>Featured:</i>");for(let r of e.featured)o(r,!0);t.push(""),n.push("")}if(e.shared.length>0){t.push("_Shared (every chat):_"),n.push("<i>Shared (every chat):</i>");for(let r of e.shared)o(r,!1);t.push(""),n.push("")}if(e.yours.length>0){t.push("_This chat:_"),n.push("<i>This chat:</i>");for(let r of e.yours)o(r,!1);t.push(""),n.push("")}if(e.more.length>0){t.push("_More:_"),n.push("<i>More:</i>");for(let r of e.more)o(r,!1)}return e.featured.length===0&&e.shared.length===0&&e.yours.length===0&&e.more.length===0&&(t.push("(no recipes \u2014 add host file or /run add)"),n.push(V("(no recipes \u2014 add host file or /run add)"))),fe(t.join(`
69
+ \u2192 ${t}${o}`)}function Yu(){return[{kind:"title",text:"/run \u2014 recipe templates"},{kind:"sub",text:"Invoke"},{kind:"bullet",text:"/run <name> <task\u2026> \u2014 start detached session by default (output muted; /apps attach or >name for input)"},{kind:"bullet",text:"/run <name> --attach|-a <task\u2026> \u2014 attach on start (plain DMs go to session until /apps detach); --detach|-d forces detached"},{kind:"bullet",text:"/r <name> <task\u2026> \u2014 short alias for /run"},{kind:"gap"},{kind:"sub",text:"Queue"},{kind:"bullet",text:"/run <name> -q <task\u2026> or /run <name> --queue <task\u2026> \u2014 FIFO per chat: head starts immediately; others wait in memory until the head exits cleanly (code=0, signal=0)"},{kind:"bullet",text:"Pending counts only jobs not yet started \u2014 the first queued item becomes Active right away, so /run queue can show Pending: 0 while a queued run is still executing"},{kind:"bullet",text:"/run queue \u2014 active session + recipe, numbered waiting list, paused flag"},{kind:"bullet",text:"/run queue resume \u2014 after a pause or non-clean exit, clear pause and start the next waiting item"},{kind:"bullet",text:"/run queue load <file.json> \u2014 enqueue jobs from JSON (paths relative to session cwd); /run queue load json [\u2026] \u2014 same payload inline; attach a file with caption /run queue load"},{kind:"bullet",text:'Queue JSON: [ { "recipe": "<name>", "task": "<text>" }, \u2026 ] or { "tasks": [ \u2026 ] } (max 64 jobs per load; same rules as /run <name> -q)'},{kind:"gap"},{kind:"sub",text:"Discover"},{kind:"bullet",text:"/run list \u2014 featured, gateway-shared, this chat, host templates; list --chat | -p | list --global | -g"},{kind:"bullet",text:"/run show <name> \u2014 merged resolution; show --global|-g|--chat|-p reads one user bucket"},{kind:"gap"},{kind:"sub",text:"Manage"},{kind:"p",text:"Recipe scope flags: -g or --global = shared on this gateway; -p or --chat = private to this chat."},{kind:"bullet",text:'/run add <name> <command\u2026> [--template "\u2026"] \u2014 this chat; add --global|-g share on this gateway'},{kind:"bullet",text:"/run set <name> <command\u2026> \u2014 overwrite in chosen bucket (--global|-g|--chat|-p); scope-only (same body): /run set -g <name> or /run set <name> -g (share); /run set -p <name> or /run set <name> -p (this chat only)"},{kind:"bullet",text:"/run remove <name> \u2014 this chat storage; remove --global|-g clears gateway-shared \xB7 rm, del \xB7 --chat|-p"},{kind:"gap"},{kind:"sub",text:"Notes"},{kind:"bullet",text:"Commands must reference the task env variable (default `$OMNISH_TASK`)."},{kind:"bullet",text:"Template placeholders `<<<OMNISH_TASK>>>`, `<<<```$OMNISH_TASK```>>>`, and `$OMNISH_TASK` are replaced with task text."},{kind:"bullet",text:"Host overrides file: "+wr},{kind:"bullet",text:"Built-in Claude recipes omit dangerous flags unless recipesAllowDangerousBuiltins=true."},{kind:"gap"},{kind:"sub",text:"Online catalog"},{kind:"bullet",text:"/run online trending | search <query> | show <publicId> | <publicId> download \u2014 all kinds"},{kind:"bullet",text:"/run <recipe> publish \u2014 share to platform (requires omnish platform login)"}]}function Zg(e){switch(e){case"builtin":return"built-in";case"global":return"host recipes.json";case"shared":return"gateway-shared (/run add --global)";case"peer":return"this chat (/run add)";default:return e}}function Qu(e){let t=["*Recipes*","`/run <name> <task>`",""],n=["<b>Recipes</b>","<code>/run &lt;name&gt; &lt;task&gt;</code>",""],o=(r,s)=>{let i=r.description?` \u2014 ${r.description}`:"",a=s&&r.dangerous?" [dangerous flags]":"";t.push(`${Ae}${r.name} \u2014 ${r.label??r.name}${i}${a}`);let l=r.description?` \u2014 ${Z(r.description)}`:"",c=s&&r.dangerous?Z(" [dangerous flags]"):"";n.push(`\u2022 ${Z(r.name)} \u2014 ${Z(r.label??r.name)}${l}${c}`)};if(e.featured.length>0){t.push("_Featured:_"),n.push("<i>Featured:</i>");for(let r of e.featured)o(r,!0);t.push(""),n.push("")}if(e.shared.length>0){t.push("_Shared (every chat):_"),n.push("<i>Shared (every chat):</i>");for(let r of e.shared)o(r,!1);t.push(""),n.push("")}if(e.yours.length>0){t.push("_This chat:_"),n.push("<i>This chat:</i>");for(let r of e.yours)o(r,!1);t.push(""),n.push("")}if(e.more.length>0){t.push("_More:_"),n.push("<i>More:</i>");for(let r of e.more)o(r,!1)}return e.featured.length===0&&e.shared.length===0&&e.yours.length===0&&e.more.length===0&&(t.push("(no recipes \u2014 add host file or /run add)"),n.push(Z("(no recipes \u2014 add host file or /run add)"))),we(t.join(`
70
70
  `).trimEnd(),n.join(`
71
- `).trimEnd())}function Ji(e,t){let n=e.taskEnv??"OMNISH_TASK",o=[`Recipe: ${e.name}`,`Source: ${ag(e.source)}`,`Label: ${e.label??"(none)"}`];if(e.steps&&e.steps.length>0){o.push(`Type: runbook (${e.steps.length} steps)`);for(let r=0;r<e.steps.length;r++){let s=e.steps[r],i=s.label?` (${s.label})`:"",a=s.continueOnFail?" [continue-on-fail]":"";o.push(` ${r+1}. ${s.cmd}${i}${a}`)}}else o.push(`Task env: ${n}`),o.push(`Command: ${e.command}`);if(e.promptTemplate){o.push(`Template: ${e.promptTemplate.length} chars`);let s=e.promptTemplate.replace(/\s+/g," ").trim().slice(0,200);o.push(`Preview: ${s}${e.promptTemplate.length>200?"\u2026":""}`)}return e.category&&o.push(`Category: ${e.category}`),e.description&&o.push(`Description: ${e.description}`),e.dangerous&&o.push("Note: built-in includes gated dangerous CLI flags."),t&&o.push("",t),p(o.join(`
72
- `))}function To(e,t,n="chat"){let o=n==="global"?"Shared on this gateway (every chat unless this chat overrides the name).":"Stored for this chat only.",r=[`Recipe saved: ${e}`,`\u2192 ${t.command}`,`(task env: ${t.taskEnv??"OMNISH_TASK"})`];return t.promptTemplate&&r.push(`Template stored: ${t.promptTemplate.length} chars`),r.push(`(${o})`),p(r.join(`
73
- `))}function Mu(e,t="chat"){return p(`Recipe removed (${t==="global"?"gateway-shared storage":"this chat"}): ${e}`)}function Pu(e,t){return p(`Unknown recipe "${e}" in ${t==="global"?"gateway-shared storage":"this chat storage"}.`)}function Eu(e,t){let n=t==="global"?"_Gateway-shared recipes_":"_This chat recipes_",o=t==="global"?"<i>Gateway-shared recipes</i>":"<i>This chat recipes</i>";if(e.length===0)return p(t==="global"?"(no gateway-shared recipes \u2014 /run add --global <name> <command\u2026>)":"(no recipes in this chat \u2014 /run add <name> <command\u2026>)");let r=["*Recipes*",n,""],s=["<b>Recipes</b>",o,""];for(let i of e){let a=i.description?` \u2014 ${i.description}`:"";r.push(`${Ae}${i.name} \u2014 ${i.label??i.name}${a}`);let l=i.description?` \u2014 ${V(i.description)}`:"";s.push(`\u2022 ${V(i.name)} \u2014 ${V(i.label??i.name)}${l}`)}return fe(r.join(`
71
+ `).trimEnd())}function da(e,t){let n=e.taskEnv??"OMNISH_TASK",o=[`Recipe: ${e.name}`,`Source: ${Zg(e.source)}`,`Label: ${e.label??"(none)"}`];if(e.steps&&e.steps.length>0){o.push(`Type: runbook (${e.steps.length} steps)`);for(let r=0;r<e.steps.length;r++){let s=e.steps[r],i=s.label?` (${s.label})`:"",a=s.continueOnFail?" [continue-on-fail]":"";o.push(` ${r+1}. ${s.cmd}${i}${a}`)}}else o.push(`Task env: ${n}`),o.push(`Command: ${e.command}`);if(e.promptTemplate){o.push(`Template: ${e.promptTemplate.length} chars`);let s=e.promptTemplate.replace(/\s+/g," ").trim().slice(0,200);o.push(`Preview: ${s}${e.promptTemplate.length>200?"\u2026":""}`)}return e.category&&o.push(`Category: ${e.category}`),e.description&&o.push(`Description: ${e.description}`),e.dangerous&&o.push("Note: built-in includes gated dangerous CLI flags."),t&&o.push("",t),p(o.join(`
72
+ `))}function Wo(e,t,n="chat"){let o=n==="global"?"Shared on this gateway (every chat unless this chat overrides the name).":"Stored for this chat only.",r=[`Recipe saved: ${e}`,`\u2192 ${t.command}`,`(task env: ${t.taskEnv??"OMNISH_TASK"})`];return t.promptTemplate&&r.push(`Template stored: ${t.promptTemplate.length} chars`),r.push(`(${o})`),p(r.join(`
73
+ `))}function Vu(e,t="chat"){return p(`Recipe removed (${t==="global"?"gateway-shared storage":"this chat"}): ${e}`)}function Xu(e,t){return p(`Unknown recipe "${e}" in ${t==="global"?"gateway-shared storage":"this chat storage"}.`)}function Zu(e,t){let n=t==="global"?"_Gateway-shared recipes_":"_This chat recipes_",o=t==="global"?"<i>Gateway-shared recipes</i>":"<i>This chat recipes</i>";if(e.length===0)return p(t==="global"?"(no gateway-shared recipes \u2014 /run add --global <name> <command\u2026>)":"(no recipes in this chat \u2014 /run add <name> <command\u2026>)");let r=["*Recipes*",n,""],s=["<b>Recipes</b>",o,""];for(let i of e){let a=i.description?` \u2014 ${i.description}`:"";r.push(`${Ae}${i.name} \u2014 ${i.label??i.name}${a}`);let l=i.description?` \u2014 ${Z(i.description)}`:"";s.push(`\u2022 ${Z(i.name)} \u2014 ${Z(i.label??i.name)}${l}`)}return we(r.join(`
74
74
  `).trimEnd(),s.join(`
75
- `).trimEnd())}function zi(e){return p(`Unknown recipe: ${e}
76
- /run list`)}function Ki(){return[{kind:"title",text:"Apps (interactive CLI)"},{kind:"sub",text:"Sessions"},{kind:"bullet",text:"/apps start <name> <command\u2026> \u2014 spawn in session cwd; auto-attaches"},{kind:"bullet",text:"/apps attach <name> | /apps detach"},{kind:"bullet",text:"/apps list | /apps info <name>"},{kind:"gap"},{kind:"sub",text:"Input"},{kind:"bullet",text:"/apps send <name> <text> \u2014 text + newline"},{kind:"bullet",text:">name text \u2014 shorthand for send"},{kind:"bullet",text:"/apps key <name> <KEY[,KEY\u2026]> \u2014 Enter, ^C, Up, Esc, \\x1b, \u2026"},{kind:"gap"},{kind:"sub",text:"Output & control"},{kind:"bullet",text:"/apps tail <name> [lines] | /apps since <name>"},{kind:"bullet",text:"/apps mute|unmute <name> | /apps raw <name> on|off"},{kind:"bullet",text:"/apps resize <name> <cols> <rows>"},{kind:"bullet",text:"/apps stop <name> | /apps kill <name> | /apps rm <name>"},{kind:"bullet",text:"/apps <session> publish \u2014 share session command to online catalog"},{kind:"bullet",text:"/apps online trending | show <publicId> | <publicId> download \u2014 browse app templates only"},{kind:"gap"},{kind:"p",text:"Attached: plain DMs go to the focused app. Escaped by ! prefix, /commands, or >other."},{kind:"p",text:"Free shell: !!start \u2014 plain DMs run as sync shell when no focused PTY; !!stop \u2014 off. Attached session wins over free shell for plain text."}]}function lg(e){let{errors:t,warns:n,infos:o}=_i(e),r=[{kind:"title",text:"Security check"}];if(e.length===0)return r.push({kind:"p",text:"No issues reported by automated rules."},{kind:"gap"},{kind:"p",text:"Allowlisted remote shell access is still equivalent to sharing credentials with those identities."}),r;r.push({kind:"p",text:`${t} error(s), ${n} warning(s), ${o} note(s).`}),r.push({kind:"gap"});let s=(i,a)=>{if(a.length!==0){r.push({kind:"sub",text:`${i} (${a.length})`});for(let l of a){let d=l.severity.toUpperCase();r.push({kind:"bullet",text:Re(`[${d}] ${l.code}: ${l.message}`)}),l.detail&&r.push({kind:"bullet",text:Re(l.detail)}),l.fixHint&&r.push({kind:"bullet",text:Re(`Fix: ${l.fixHint}`)})}r.push({kind:"gap"})}};return s("Errors",e.filter(i=>i.severity==="error")),s("Warnings",e.filter(i=>i.severity==="warn")),s("Notes",e.filter(i=>i.severity==="info")),r.push({kind:"p",text:"Allowlisted identities can run commands as this user; treat them like passwords."}),r}function Au(e){return X(lg(e))}function Iu(){return[{kind:"title",text:"Security tips"},{kind:"p",text:"Practical hardening for this gateway:"},{kind:"bullet",text:"Keep allowlists minimal \u2014 only numbers and tg: ids you trust."},{kind:"bullet",text:'Never use "*" in allowFrom; treat allowed identities like passwords.'},{kind:"bullet",text:"chmod 600 config.json and chmod 700 your data dir (~/.omnish or OMNISH_HOME)."},{kind:"bullet",text:"Do not paste bot tokens in group chats; revoke at @BotFather if leaked."},{kind:"bullet",text:"Prefer TELEGRAM_BOT_TOKEN via systemd/env over chat if your Telegram logs worry you."},{kind:"bullet",text:"Run omnish as a normal user, not root, unless you fully accept that risk."},{kind:"gap"},{kind:"p",text:"Send /security on this chat or run omnish security on the host for automated checks."}]}function Lu(){return[{kind:"title",text:"/security commands"},{kind:"bullet",text:"/security \u2014 full report (same checks as omnish security)"},{kind:"bullet",text:"/security summary \u2014 one-line counts"},{kind:"bullet",text:"/security tips \u2014 short hardening checklist"},{kind:"bullet",text:"CLI: omnish security [--json] for scripts and monitoring"}]}function $e(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function Wn(e){let t=e.trim();return t?t.length<=8?"(set)":`${t.slice(0,4)}\u2026${t.slice(-4)}`:"(empty)"}function Ou(e){let t=e.trim();return(t.startsWith('"')&&t.endsWith('"')&&t.length>=2||t.startsWith("'")&&t.endsWith("'")&&t.length>=2)&&(t=t.slice(1,-1)),t}function Ie(e){let t=e.trim().toLowerCase();return t==="true"||t==="1"||t==="yes"||t==="on"?!0:t==="false"||t==="0"||t==="no"||t==="off"?!1:null}var Nu=new Set(["downloads","omnishData","sessionCwd","processCwd","fixed"]),Vt=["gatewayMode","clusterEnabled","clusterRole","clusterLabel","clusterSenderBindings","commandPrefix","syncTimeoutMs","syncMaxBytes","jobLogTailLines","shell","appsCols","appsRows","appsFlushMs","appsMinIntervalMs","appsMaxFlushBytes","appsMaxSessions","appsMaxSessionsTotal","appsMaxWaChars","appsLogTailLines","appsSubmitDelayMs","appsClearInput","appsClearInputDelayMs","appsClearInputSequence","appsSkipClearOnPasswordPrompt","appsPasswordPromptHint","fileSendMaxBytes","fileReceiveMaxBytes","fileInboxSubdir","fileReceiveRootMode","fileReceiveRootPath","recipesAllowDangerousBuiltins","recipesMaxTaskChars","recipesMacroDefaultCommand","recipesRunAttach","telegramBotToken","serviceInstallFromChat","updateCheckEnabled","updateCheckIntervalMs","updateCheckPackageName","updateInfoUrl","chatLlmFallbackEnabled","chatLlmShellCommand","chatLlmTimeoutMs","chatLlmMaxInputChars","chatLlmMaxOutputChars","chatLlmNeedsTty","chatLlmWorkDir","tunnelEnabled","tunnelRelayUrl","tunnelMaxActive","webhookEnabled","webhookPort","webhookHost","webhookToken","watchEnabled","watchDebounceMs","watchMaxEventsPerMinute","watchAutoRestore","pullEnabled","pullInstallFromChat","pullUrlAutoDetect","pullDefaultMode","pullOutputDir","pullMaxBytes","pullAutoSend","pullWhisperModel","pullYtDlpPath","pullFfmpegPath","pullWhisperPath"];function Fu(e){return Vt.includes(e)}function cg(e){let t=Me(e);return["*Config* (secrets masked)","",`gatewayMode: ${e.gatewayMode}`,`commandPrefix: ${e.commandPrefix}`,`shell: ${e.shell}`,`syncTimeoutMs: ${e.syncTimeoutMs}`,`syncMaxBytes: ${e.syncMaxBytes}`,`jobLogTailLines: ${e.jobLogTailLines}`,"","*Cluster*",`clusterEnabled: ${e.clusterEnabled}`,`clusterRole: ${e.clusterRole}`,`clusterLabel: ${e.clusterLabel||"(hostname)"}`,`clusterSenderBindings: ${Object.keys(e.clusterSenderBindings??{}).length} entries`,"","*Telegram*",`telegramBotToken: ${Wn(t)}`,`telegramAllowFrom: ${e.telegramAllowFrom.length} entries`,"","*WhatsApp allowFrom*",`${e.allowFrom.length} entries`,"","*Apps*",`appsCols \xD7 appsRows: ${e.appsCols}\xD7${e.appsRows}`,`appsFlushMs / appsMinIntervalMs: ${e.appsFlushMs} / ${e.appsMinIntervalMs}`,`appsMaxFlushBytes: ${e.appsMaxFlushBytes}`,`appsMaxSessions / total: ${e.appsMaxSessions} / ${e.appsMaxSessionsTotal}`,`appsMaxWaChars: ${e.appsMaxWaChars}`,`appsLogTailLines: ${e.appsLogTailLines}`,`appsSubmitDelayMs: ${e.appsSubmitDelayMs}`,`appsClearInput: ${e.appsClearInput}`,`appsClearInputDelayMs: ${e.appsClearInputDelayMs}`,`appsClearInputSequence: ${e.appsClearInputSequence}`,`appsSkipClearOnPasswordPrompt: ${e.appsSkipClearOnPasswordPrompt}`,`appsPasswordPromptHint: ${e.appsPasswordPromptHint}`,"","*Files*",`fileSendMaxBytes / fileReceiveMaxBytes: ${e.fileSendMaxBytes} / ${e.fileReceiveMaxBytes}`,`fileInboxSubdir: ${e.fileInboxSubdir}`,`fileReceiveRootMode: ${e.fileReceiveRootMode}`,`fileReceiveRootPath: ${e.fileReceiveRootPath||"(empty)"}`,"","*Recipes*",`recipesAllowDangerousBuiltins: ${e.recipesAllowDangerousBuiltins}`,`recipesMaxTaskChars: ${e.recipesMaxTaskChars}`,`recipesMacroDefaultCommand: ${e.recipesMacroDefaultCommand}`,`recipesRunAttach: ${e.recipesRunAttach}`,"","*Service (chat)*",`serviceInstallFromChat: ${e.serviceInstallFromChat}`,"","*Updates (optional)*",`updateCheckEnabled: ${e.updateCheckEnabled}`,`updateCheckIntervalMs: ${e.updateCheckIntervalMs}`,`updateCheckPackageName: ${e.updateCheckPackageName}`,`updateInfoUrl: ${e.updateInfoUrl?"(set)":"(empty)"}`,"","*Tunneling (chat /tunnel)*",`tunnelEnabled: ${e.tunnelEnabled}`,`tunnelRelayUrl: ${e.tunnelRelayUrl}`,`tunnelMaxActive: ${e.tunnelMaxActive}`,"","*Chat LLM fallback (optional)*",`chatLlmFallbackEnabled: ${e.chatLlmFallbackEnabled}`,`chatLlmShellCommand: ${e.chatLlmShellCommand?"(set)":"(empty)"}`,`chatLlmTimeoutMs: ${e.chatLlmTimeoutMs}`,`chatLlmMaxInputChars / chatLlmMaxOutputChars: ${e.chatLlmMaxInputChars} / ${e.chatLlmMaxOutputChars}`,`chatLlmNeedsTty: ${e.chatLlmNeedsTty}`,`chatLlmWorkDir: ${e.chatLlmWorkDir||"(empty \u2014 temp dir per run)"}`,"","*Webhook receiver (optional)*",`webhookEnabled: ${e.webhookEnabled}`,`webhookPort: ${e.webhookPort} (0 = random)`,`webhookHost: ${e.webhookHost}`,`webhookToken: ${Wn(e.webhookToken)}`,"","*Watch (OS event eye)*",`watchEnabled: ${e.watchEnabled}`,`watchDebounceMs: ${e.watchDebounceMs}`,`watchMaxEventsPerMinute: ${e.watchMaxEventsPerMinute}`,`watchAutoRestore: ${e.watchAutoRestore}`,"","*Media pull (/pull)*",`pullEnabled: ${e.pullEnabled}`,`pullInstallFromChat: ${e.pullInstallFromChat}`,`pullUrlAutoDetect: ${e.pullUrlAutoDetect}`,`pullDefaultMode: ${e.pullDefaultMode}`,`pullOutputDir: ${e.pullOutputDir||"(session/.omnish-pull)"}`,`pullMaxBytes: ${e.pullMaxBytes}`,`pullAutoSend: ${e.pullAutoSend}`,`pullWhisperModel: ${e.pullWhisperModel}`,"",`File: ${_}`].join(`
77
- `)}function ug(e){let t=Me(e);return["<b>Config</b> (secrets masked)","",`<b>gatewayMode</b> ${$e(e.gatewayMode)}`,`<b>commandPrefix</b> ${$e(e.commandPrefix)}`,`<b>shell</b> ${$e(e.shell)}`,`<b>syncTimeoutMs</b> ${e.syncTimeoutMs}`,`<b>syncMaxBytes</b> ${e.syncMaxBytes}`,`<b>jobLogTailLines</b> ${e.jobLogTailLines}`,"","<b>Cluster</b>",`clusterEnabled: ${e.clusterEnabled} \xB7 clusterRole: ${$e(e.clusterRole)}`,`clusterLabel: ${$e(e.clusterLabel||"(hostname)")}`,`clusterSenderBindings: ${Object.keys(e.clusterSenderBindings??{}).length} entries`,"","<b>Telegram</b>",`telegramBotToken: ${$e(Wn(t))}`,`telegramAllowFrom: ${e.telegramAllowFrom.length} entries`,"",`<b>WhatsApp allowFrom</b> ${e.allowFrom.length} entries`,"","<b>Apps</b>",`${e.appsCols}\xD7${e.appsRows} flush ${e.appsFlushMs}/${e.appsMinIntervalMs} ms \u2026`,"","<b>Files</b>",`${$e(e.fileReceiveRootMode)} \xB7 inbox ${$e(e.fileInboxSubdir)}`,"","<b>Service (chat)</b>",`serviceInstallFromChat: ${e.serviceInstallFromChat}`,"","<b>Updates (optional)</b>",`updateCheckEnabled: ${e.updateCheckEnabled} \xB7 interval ms: ${e.updateCheckIntervalMs}`,`package: <code>${$e(e.updateCheckPackageName)}</code> \xB7 info URL: ${e.updateInfoUrl?"set":"empty"}`,"","<b>Tunneling (chat /tunnel)</b>",`tunnelEnabled: ${e.tunnelEnabled} \xB7 relay <code>${$e(e.tunnelRelayUrl)}</code> \xB7 max active: ${e.tunnelMaxActive}`,"","<b>Chat LLM fallback</b>",`enabled: ${e.chatLlmFallbackEnabled} \xB7 command: ${e.chatLlmShellCommand?"set":"empty"}`,`timeout ms: ${e.chatLlmTimeoutMs} \xB7 in/out chars: ${e.chatLlmMaxInputChars} / ${e.chatLlmMaxOutputChars}`,`needsTty: ${e.chatLlmNeedsTty} \xB7 workDir: ${$e(e.chatLlmWorkDir||"(empty)")}`,"","<b>Webhook receiver</b>",`enabled: ${e.webhookEnabled} \xB7 ${$e(e.webhookHost)}:${e.webhookPort} \xB7 token: ${$e(Wn(e.webhookToken))}`,"","<b>Watch (OS event eye)</b>",`enabled: ${e.watchEnabled} \xB7 debounce ms: ${e.watchDebounceMs} \xB7 max/min: ${e.watchMaxEventsPerMinute} \xB7 autoRestore: ${e.watchAutoRestore}`,"",`<code>${$e(_)}</code>`].join(`
78
- `)}function _u(e,t){if(!Fu(t))return null;let n=t;if(n==="telegramBotToken")return`telegramBotToken: ${Wn(Me(e))}`;if(n==="webhookToken")return`webhookToken: ${Wn(e.webhookToken)}`;let o=e[n];return`${t}: ${typeof o=="object"?JSON.stringify(o):String(o)}`}function dg(e,t){let n=_u(e,t);if(!n)return null;let o=n.split(": ");return o.length<2?$e(n):`<b>${$e(o[0])}</b> ${$e(o.slice(1).join(": "))}`}function Ir(e,t){let n=Ou(t),o=!1,r=!1,s=!1;if(e==="telegramBotToken"){if(!gt(n))throw new Error("Invalid bot token format (expect digits:secret from BotFather).");return Ht(n),o=!0,s=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s}}let i=S();switch(e){case"gatewayMode":{let a=En(n);if(!a)throw new Error("gatewayMode: use whatsapp | telegram | both (or wa, tg, b)");i.gatewayMode=a,o=!0;break}case"clusterEnabled":{let a=Ie(n);if(a===null)throw new Error("clusterEnabled: true or false");i.clusterEnabled=a;break}case"clusterRole":{let a=n.trim().toLowerCase();if(a!=="primary"&&a!=="secondary")throw new Error("clusterRole: primary | secondary");i.clusterRole=a;break}case"clusterLabel":i.clusterLabel=n.trim().slice(0,128);break;case"clusterSenderBindings":{let a=n.trim();if(a===""||a==="{}"||a.toLowerCase()==="clear"){i.clusterSenderBindings={};break}let l;try{l=JSON.parse(a)}catch{throw new Error('clusterSenderBindings: pass a JSON object, e.g. {"wa:+15551234567":"workshop-box"}')}if(!l||typeof l!="object"||Array.isArray(l))throw new Error("clusterSenderBindings must be a JSON object of sender \u2192 label/id");let d={};for(let[u,c]of Object.entries(l)){if(typeof c!="string"||!c.trim())throw new Error(`clusterSenderBindings.${u}: value must be a non-empty string`);d[u]=c.trim()}i.clusterSenderBindings=d;break}case"commandPrefix":if(!n.trim())throw new Error("commandPrefix cannot be empty");i.commandPrefix=n.trim().slice(0,32);break;case"syncTimeoutMs":{let a=Number.parseInt(n,10);if(!Number.isFinite(a)||a<=0)throw new Error("syncTimeoutMs: positive integer (ms)");i.syncTimeoutMs=a;break}case"syncMaxBytes":{let a=Number.parseInt(n,10);if(!Number.isFinite(a)||a<=0)throw new Error("syncMaxBytes: positive integer");i.syncMaxBytes=a;break}case"jobLogTailLines":{let a=Number.parseInt(n,10);if(!Number.isFinite(a)||a<=0)throw new Error("jobLogTailLines: positive integer");i.jobLogTailLines=a;break}case"shell":if(!n.trim())throw new Error("shell: non-empty path");i.shell=n.trim().slice(0,4096),r=!0;break;case"recipesMacroDefaultCommand":if(!n.trim())throw new Error('recipesMacroDefaultCommand: non-empty shell snippet with "$OMNISH_TASK"');i.recipesMacroDefaultCommand=Ou(n).trim().slice(0,4096);break;case"appsCols":case"appsRows":case"appsFlushMs":case"appsMinIntervalMs":case"appsMaxFlushBytes":case"appsMaxSessions":case"appsMaxSessionsTotal":case"appsMaxWaChars":case"appsLogTailLines":case"appsSubmitDelayMs":case"appsClearInputDelayMs":case"recipesMaxTaskChars":case"fileSendMaxBytes":case"fileReceiveMaxBytes":{let a=Number.parseInt(n,10);if(!Number.isFinite(a)||a<0)throw new Error(`${e}: non-negative integer`);i[e]=a;break}case"appsClearInput":{let a=Ie(n);if(a===null)throw new Error("appsClearInput: true or false");i.appsClearInput=a;break}case"appsClearInputSequence":i.appsClearInputSequence=n.trim().slice(0,200);break;case"appsSkipClearOnPasswordPrompt":{let a=Ie(n);if(a===null)throw new Error("appsSkipClearOnPasswordPrompt: true or false");i.appsSkipClearOnPasswordPrompt=a;break}case"appsPasswordPromptHint":{let a=Ie(n);if(a===null)throw new Error("appsPasswordPromptHint: true or false");i.appsPasswordPromptHint=a;break}case"fileInboxSubdir":i.fileInboxSubdir=n.trim().slice(0,80);break;case"fileReceiveRootMode":{let a=n.trim();if(!Nu.has(a))throw new Error(`fileReceiveRootMode: ${[...Nu].join(" | ")}`);i.fileReceiveRootMode=a;break}case"fileReceiveRootPath":i.fileReceiveRootPath=n.trim().slice(0,4096);break;case"recipesRunAttach":{let a=Ie(n);if(a===null)throw new Error("recipesRunAttach: true or false");i.recipesRunAttach=a;break}case"recipesAllowDangerousBuiltins":{let a=Ie(n);if(a===null)throw new Error("recipesAllowDangerousBuiltins: true or false");i.recipesAllowDangerousBuiltins=a;break}case"serviceInstallFromChat":{let a=Ie(n);if(a===null)throw new Error("serviceInstallFromChat: true or false");i.serviceInstallFromChat=a;break}case"updateCheckEnabled":{let a=Ie(n);if(a===null)throw new Error("updateCheckEnabled: true or false");i.updateCheckEnabled=a;break}case"updateCheckIntervalMs":{let a=Number.parseInt(n,10);if(!Number.isFinite(a)||a<36e5)throw new Error("updateCheckIntervalMs: integer ms, minimum 3600000 (1 hour)");i.updateCheckIntervalMs=Math.min(6048e5,a);break}case"updateCheckPackageName":if(!n.trim())throw new Error("updateCheckPackageName: non-empty npm package name");i.updateCheckPackageName=n.trim().slice(0,214);break;case"updateInfoUrl":i.updateInfoUrl=n.trim().slice(0,2048);break;case"tunnelEnabled":{let a=Ie(n);if(a===null)throw new Error("tunnelEnabled: true or false");return O({tunnelEnabled:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"tunnelRelayUrl":{let a=n.trim();if(!a)throw new Error("tunnelRelayUrl: non-empty URL");let l;try{l=new URL(a)}catch{throw new Error("tunnelRelayUrl: invalid URL")}if(l.protocol!=="http:"&&l.protocol!=="https:")throw new Error("tunnelRelayUrl: use http:// or https://");return O({tunnelRelayUrl:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"tunnelMaxActive":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<1||a>50)throw new Error("tunnelMaxActive: integer 1\u201350");return O({tunnelMaxActive:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmFallbackEnabled":{let a=Ie(n);if(a===null)throw new Error("chatLlmFallbackEnabled: true or false");return O({chatLlmFallbackEnabled:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmShellCommand":return O({chatLlmShellCommand:n}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"chatLlmTimeoutMs":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<=0)throw new Error("chatLlmTimeoutMs: positive integer (ms)");return O({chatLlmTimeoutMs:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmMaxInputChars":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<=0)throw new Error("chatLlmMaxInputChars: positive integer");return O({chatLlmMaxInputChars:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmMaxOutputChars":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<=0)throw new Error("chatLlmMaxOutputChars: positive integer");return O({chatLlmMaxOutputChars:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmNeedsTty":{let a=Ie(n);if(a===null)throw new Error("chatLlmNeedsTty: true or false");return O({chatLlmNeedsTty:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmWorkDir":return O({chatLlmWorkDir:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"webhookEnabled":{let a=Ie(n);if(a===null)throw new Error("webhookEnabled: true or false");return O({webhookEnabled:a}),o=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"webhookPort":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<0||a>65535)throw new Error("webhookPort: integer 0\u201365535 (0 = random free port)");return O({webhookPort:a}),o=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"webhookHost":if(!n.trim())throw new Error("webhookHost: non-empty bind address");return O({webhookHost:n.trim().slice(0,256)}),o=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"webhookToken":return O({webhookToken:n.trim()}),o=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"watchEnabled":{let a=Ie(n);if(a===null)throw new Error("watchEnabled: true or false");return O({watchEnabled:a}),qe(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"watchDebounceMs":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<500||a>6e4)throw new Error("watchDebounceMs: integer 500\u201360000");return O({watchDebounceMs:a}),qe(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"watchMaxEventsPerMinute":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<1||a>120)throw new Error("watchMaxEventsPerMinute: integer 1\u2013120");return O({watchMaxEventsPerMinute:a}),qe(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"watchAutoRestore":{let a=Ie(n);if(a===null)throw new Error("watchAutoRestore: true or false");return O({watchAutoRestore:a}),qe(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"pullEnabled":{let a=Ie(n);if(a===null)throw new Error("pullEnabled: true or false");return O({pullEnabled:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"pullInstallFromChat":{let a=Ie(n);if(a===null)throw new Error("pullInstallFromChat: true or false");return O({pullInstallFromChat:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"pullUrlAutoDetect":{let a=Ie(n);if(a===null)throw new Error("pullUrlAutoDetect: true or false");return O({pullUrlAutoDetect:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"pullDefaultMode":{if(!new Set(["video","audio","subs","transcript","all"]).has(n.trim()))throw new Error("pullDefaultMode: video|audio|subs|transcript|all");return O({pullDefaultMode:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"pullOutputDir":return O({pullOutputDir:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"pullMaxBytes":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<0)throw new Error("pullMaxBytes: non-negative integer");return O({pullMaxBytes:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"pullAutoSend":{let a=Ie(n);if(a===null)throw new Error("pullAutoSend: true or false");return O({pullAutoSend:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"pullWhisperModel":return O({pullWhisperModel:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"pullYtDlpPath":return O({pullYtDlpPath:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"pullFfmpegPath":return O({pullFfmpegPath:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"pullWhisperPath":return O({pullWhisperPath:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}return Be(i),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}async function Wu(e,t){let n=e.trim(),o=n.toLowerCase();if(!n||o==="help")return X(ru());if(o==="keys"||o==="help keys")return p(["*Configurable keys* (/config set)","",Vt.join(", "),"","Examples:","/config set clusterLabel work-laptop",'/config set fileReceiveRootPath "/path/with spaces"',"/config set gatewayMode both","/config set tunnelEnabled true","/config set chatLlmFallbackEnabled false","/config set webhookEnabled true","/config set watchEnabled true"].join(`
79
- `));if(o==="show"){let i=S();return fe(cg(i),ug(i))}let r=n.match(/^get\s+(\S+)\s*$/i);if(r){let i=r[1],a=S(),l=_u(a,i);if(!l)return p(`Unknown key "${i}". /config keys`);let d=dg(a,i);return fe(`${l}
75
+ `).trimEnd())}function pa(e){return p(`Unknown recipe: ${e}
76
+ /run list`)}function ma(){return[{kind:"title",text:"Apps (interactive CLI)"},{kind:"sub",text:"Sessions"},{kind:"bullet",text:"/apps start <name> <command\u2026> \u2014 spawn in session cwd; auto-attaches"},{kind:"bullet",text:"/apps attach <name> | /apps detach"},{kind:"bullet",text:"/apps list | /apps info <name>"},{kind:"gap"},{kind:"sub",text:"Input"},{kind:"bullet",text:"/apps send <name> <text> \u2014 text + newline"},{kind:"bullet",text:">name text \u2014 shorthand for send"},{kind:"bullet",text:"/apps key <name> <KEY[,KEY\u2026]> \u2014 Enter, ^C, Up, Esc, \\x1b, \u2026"},{kind:"gap"},{kind:"sub",text:"Output & control"},{kind:"bullet",text:"/apps tail <name> [lines] | /apps since <name>"},{kind:"bullet",text:"/apps mute|unmute <name> | /apps raw <name> on|off"},{kind:"bullet",text:"/apps resize <name> <cols> <rows>"},{kind:"bullet",text:"/apps stop <name> | /apps kill <name> | /apps rm <name>"},{kind:"bullet",text:"/apps <session> publish \u2014 share session command to online catalog"},{kind:"bullet",text:"/apps online trending | show <publicId> | <publicId> download \u2014 browse app templates only"},{kind:"gap"},{kind:"p",text:"Attached: plain DMs go to the focused app. Escaped by ! prefix, /commands, or >other."},{kind:"p",text:"Free shell: !!start \u2014 plain DMs run as sync shell when no focused PTY; !!stop \u2014 off. Attached session wins over free shell for plain text."}]}function ey(e){let{errors:t,warns:n,infos:o}=na(e),r=[{kind:"title",text:"Security check"}];if(e.length===0)return r.push({kind:"p",text:"No issues reported by automated rules."},{kind:"gap"},{kind:"p",text:"Allowlisted remote shell access is still equivalent to sharing credentials with those identities."}),r;r.push({kind:"p",text:`${t} error(s), ${n} warning(s), ${o} note(s).`}),r.push({kind:"gap"});let s=(i,a)=>{if(a.length!==0){r.push({kind:"sub",text:`${i} (${a.length})`});for(let l of a){let c=l.severity.toUpperCase();r.push({kind:"bullet",text:Te(`[${c}] ${l.code}: ${l.message}`)}),l.detail&&r.push({kind:"bullet",text:Te(l.detail)}),l.fixHint&&r.push({kind:"bullet",text:Te(`Fix: ${l.fixHint}`)})}r.push({kind:"gap"})}};return s("Errors",e.filter(i=>i.severity==="error")),s("Warnings",e.filter(i=>i.severity==="warn")),s("Notes",e.filter(i=>i.severity==="info")),r.push({kind:"p",text:"Allowlisted identities can run commands as this user; treat them like passwords."}),r}function ed(e){return ee(ey(e))}function td(){return[{kind:"title",text:"Security tips"},{kind:"p",text:"Practical hardening for this gateway:"},{kind:"bullet",text:"Keep allowlists minimal \u2014 only numbers and tg: ids you trust."},{kind:"bullet",text:'Never use "*" in allowFrom; treat allowed identities like passwords.'},{kind:"bullet",text:"chmod 600 config.json and chmod 700 your data dir (~/.omnish or OMNISH_HOME)."},{kind:"bullet",text:"Do not paste bot tokens in group chats; revoke at @BotFather if leaked."},{kind:"bullet",text:"Prefer TELEGRAM_BOT_TOKEN via systemd/env over chat if your Telegram logs worry you."},{kind:"bullet",text:"Run omnish as a normal user, not root, unless you fully accept that risk."},{kind:"gap"},{kind:"p",text:"Send /security on this chat or run omnish security on the host for automated checks."}]}function nd(){return[{kind:"title",text:"/security commands"},{kind:"bullet",text:"/security \u2014 full report (same checks as omnish security)"},{kind:"bullet",text:"/security summary \u2014 one-line counts"},{kind:"bullet",text:"/security tips \u2014 short hardening checklist"},{kind:"bullet",text:"CLI: omnish security [--json] for scripts and monitoring"}]}function $e(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function Yn(e){let t=e.trim();return t?t.length<=8?"(set)":`${t.slice(0,4)}\u2026${t.slice(-4)}`:"(empty)"}function od(e){let t=e.trim();return(t.startsWith('"')&&t.endsWith('"')&&t.length>=2||t.startsWith("'")&&t.endsWith("'")&&t.length>=2)&&(t=t.slice(1,-1)),t}function Le(e){let t=e.trim().toLowerCase();return t==="true"||t==="1"||t==="yes"||t==="on"?!0:t==="false"||t==="0"||t==="no"||t==="off"?!1:null}var rd=new Set(["downloads","omnishData","sessionCwd","processCwd","fixed"]),tn=["gatewayMode","clusterEnabled","clusterRole","clusterLabel","clusterSenderBindings","commandPrefix","syncTimeoutMs","syncMaxBytes","jobLogTailLines","shell","appsCols","appsRows","appsFlushMs","appsMinIntervalMs","appsMaxFlushBytes","appsMaxSessions","appsMaxSessionsTotal","appsMaxWaChars","appsLogTailLines","appsSubmitDelayMs","appsClearInput","appsClearInputDelayMs","appsClearInputSequence","appsSkipClearOnPasswordPrompt","appsPasswordPromptHint","fileSendMaxBytes","fileReceiveMaxBytes","fileInboxSubdir","fileReceiveRootMode","fileReceiveRootPath","recipesAllowDangerousBuiltins","recipesMaxTaskChars","recipesMacroDefaultCommand","recipesRunAttach","telegramBotToken","serviceInstallFromChat","updateCheckEnabled","updateCheckIntervalMs","updateCheckPackageName","updateInfoUrl","chatLlmFallbackEnabled","chatLlmShellCommand","chatLlmTimeoutMs","chatLlmMaxInputChars","chatLlmMaxOutputChars","chatLlmNeedsTty","chatLlmWorkDir","tunnelEnabled","tunnelRelayUrl","tunnelMaxActive","webhookEnabled","webhookPort","webhookHost","webhookToken","watchEnabled","watchDebounceMs","watchMaxEventsPerMinute","watchAutoRestore","mediaSendFiles","mediaUrlAutoDl","mediaInstallFromChat","mediaOutputDir","mediaMaxBytes","mediaWhisperModel","progressUpdates","pullYtDlpPath","pullFfmpegPath","pullWhisperPath"];function sd(e){return tn.includes(e)}function ty(e){let t=Me(e);return["*Config* (secrets masked)","",`gatewayMode: ${e.gatewayMode}`,`commandPrefix: ${e.commandPrefix}`,`shell: ${e.shell}`,`syncTimeoutMs: ${e.syncTimeoutMs}`,`syncMaxBytes: ${e.syncMaxBytes}`,`jobLogTailLines: ${e.jobLogTailLines}`,"","*Cluster*",`clusterEnabled: ${e.clusterEnabled}`,`clusterRole: ${e.clusterRole}`,`clusterLabel: ${e.clusterLabel||"(hostname)"}`,`clusterSenderBindings: ${Object.keys(e.clusterSenderBindings??{}).length} entries`,"","*Telegram*",`telegramBotToken: ${Yn(t)}`,`telegramAllowFrom: ${e.telegramAllowFrom.length} entries`,"","*WhatsApp allowFrom*",`${e.allowFrom.length} entries`,"","*Apps*",`appsCols \xD7 appsRows: ${e.appsCols}\xD7${e.appsRows}`,`appsFlushMs / appsMinIntervalMs: ${e.appsFlushMs} / ${e.appsMinIntervalMs}`,`appsMaxFlushBytes: ${e.appsMaxFlushBytes}`,`appsMaxSessions / total: ${e.appsMaxSessions} / ${e.appsMaxSessionsTotal}`,`appsMaxWaChars: ${e.appsMaxWaChars}`,`appsLogTailLines: ${e.appsLogTailLines}`,`appsSubmitDelayMs: ${e.appsSubmitDelayMs}`,`appsClearInput: ${e.appsClearInput}`,`appsClearInputDelayMs: ${e.appsClearInputDelayMs}`,`appsClearInputSequence: ${e.appsClearInputSequence}`,`appsSkipClearOnPasswordPrompt: ${e.appsSkipClearOnPasswordPrompt}`,`appsPasswordPromptHint: ${e.appsPasswordPromptHint}`,"","*Files*",`fileSendMaxBytes / fileReceiveMaxBytes: ${e.fileSendMaxBytes} / ${e.fileReceiveMaxBytes}`,`fileInboxSubdir: ${e.fileInboxSubdir}`,`fileReceiveRootMode: ${e.fileReceiveRootMode}`,`fileReceiveRootPath: ${e.fileReceiveRootPath||"(empty)"}`,"","*Recipes*",`recipesAllowDangerousBuiltins: ${e.recipesAllowDangerousBuiltins}`,`recipesMaxTaskChars: ${e.recipesMaxTaskChars}`,`recipesMacroDefaultCommand: ${e.recipesMacroDefaultCommand}`,`recipesRunAttach: ${e.recipesRunAttach}`,"","*Service (chat)*",`serviceInstallFromChat: ${e.serviceInstallFromChat}`,"","*Updates (optional)*",`updateCheckEnabled: ${e.updateCheckEnabled}`,`updateCheckIntervalMs: ${e.updateCheckIntervalMs}`,`updateCheckPackageName: ${e.updateCheckPackageName}`,`updateInfoUrl: ${e.updateInfoUrl?"(set)":"(empty)"}`,"","*Tunneling (chat /tunnel)*",`tunnelEnabled: ${e.tunnelEnabled}`,`tunnelRelayUrl: ${e.tunnelRelayUrl}`,`tunnelMaxActive: ${e.tunnelMaxActive}`,"","*Chat LLM fallback (optional)*",`chatLlmFallbackEnabled: ${e.chatLlmFallbackEnabled}`,`chatLlmShellCommand: ${e.chatLlmShellCommand?"(set)":"(empty)"}`,`chatLlmTimeoutMs: ${e.chatLlmTimeoutMs}`,`chatLlmMaxInputChars / chatLlmMaxOutputChars: ${e.chatLlmMaxInputChars} / ${e.chatLlmMaxOutputChars}`,`chatLlmNeedsTty: ${e.chatLlmNeedsTty}`,`chatLlmWorkDir: ${e.chatLlmWorkDir||"(empty \u2014 temp dir per run)"}`,"","*Webhook receiver (optional)*",`webhookEnabled: ${e.webhookEnabled}`,`webhookPort: ${e.webhookPort} (0 = random)`,`webhookHost: ${e.webhookHost}`,`webhookToken: ${Yn(e.webhookToken)}`,"","*Watch (OS event eye)*",`watchEnabled: ${e.watchEnabled}`,`watchDebounceMs: ${e.watchDebounceMs}`,`watchMaxEventsPerMinute: ${e.watchMaxEventsPerMinute}`,`watchAutoRestore: ${e.watchAutoRestore}`,"","*Media (/dl, /tr, /edit)*",`mediaSendFiles: ${e.mediaSendFiles}`,`mediaInstallFromChat: ${e.mediaInstallFromChat}`,`mediaUrlAutoDl: ${e.mediaUrlAutoDl}`,`mediaOutputDir: ${e.mediaOutputDir||"(session/.omnish-media)"}`,`mediaMaxBytes: ${e.mediaMaxBytes}`,`mediaWhisperModel: ${e.mediaWhisperModel}`,`progressUpdates: ${e.progressUpdates}`,"",`File: ${_}`].join(`
77
+ `)}function ny(e){let t=Me(e);return["<b>Config</b> (secrets masked)","",`<b>gatewayMode</b> ${$e(e.gatewayMode)}`,`<b>commandPrefix</b> ${$e(e.commandPrefix)}`,`<b>shell</b> ${$e(e.shell)}`,`<b>syncTimeoutMs</b> ${e.syncTimeoutMs}`,`<b>syncMaxBytes</b> ${e.syncMaxBytes}`,`<b>jobLogTailLines</b> ${e.jobLogTailLines}`,"","<b>Cluster</b>",`clusterEnabled: ${e.clusterEnabled} \xB7 clusterRole: ${$e(e.clusterRole)}`,`clusterLabel: ${$e(e.clusterLabel||"(hostname)")}`,`clusterSenderBindings: ${Object.keys(e.clusterSenderBindings??{}).length} entries`,"","<b>Telegram</b>",`telegramBotToken: ${$e(Yn(t))}`,`telegramAllowFrom: ${e.telegramAllowFrom.length} entries`,"",`<b>WhatsApp allowFrom</b> ${e.allowFrom.length} entries`,"","<b>Apps</b>",`${e.appsCols}\xD7${e.appsRows} flush ${e.appsFlushMs}/${e.appsMinIntervalMs} ms \u2026`,"","<b>Files</b>",`${$e(e.fileReceiveRootMode)} \xB7 inbox ${$e(e.fileInboxSubdir)}`,"","<b>Service (chat)</b>",`serviceInstallFromChat: ${e.serviceInstallFromChat}`,"","<b>Updates (optional)</b>",`updateCheckEnabled: ${e.updateCheckEnabled} \xB7 interval ms: ${e.updateCheckIntervalMs}`,`package: <code>${$e(e.updateCheckPackageName)}</code> \xB7 info URL: ${e.updateInfoUrl?"set":"empty"}`,"","<b>Tunneling (chat /tunnel)</b>",`tunnelEnabled: ${e.tunnelEnabled} \xB7 relay <code>${$e(e.tunnelRelayUrl)}</code> \xB7 max active: ${e.tunnelMaxActive}`,"","<b>Chat LLM fallback</b>",`enabled: ${e.chatLlmFallbackEnabled} \xB7 command: ${e.chatLlmShellCommand?"set":"empty"}`,`timeout ms: ${e.chatLlmTimeoutMs} \xB7 in/out chars: ${e.chatLlmMaxInputChars} / ${e.chatLlmMaxOutputChars}`,`needsTty: ${e.chatLlmNeedsTty} \xB7 workDir: ${$e(e.chatLlmWorkDir||"(empty)")}`,"","<b>Webhook receiver</b>",`enabled: ${e.webhookEnabled} \xB7 ${$e(e.webhookHost)}:${e.webhookPort} \xB7 token: ${$e(Yn(e.webhookToken))}`,"","<b>Watch (OS event eye)</b>",`enabled: ${e.watchEnabled} \xB7 debounce ms: ${e.watchDebounceMs} \xB7 max/min: ${e.watchMaxEventsPerMinute} \xB7 autoRestore: ${e.watchAutoRestore}`,"",`<code>${$e(_)}</code>`].join(`
78
+ `)}function id(e,t){if(!sd(t))return null;let n=t;if(n==="telegramBotToken")return`telegramBotToken: ${Yn(Me(e))}`;if(n==="webhookToken")return`webhookToken: ${Yn(e.webhookToken)}`;let o=e[n];return`${t}: ${typeof o=="object"?JSON.stringify(o):String(o)}`}function oy(e,t){let n=id(e,t);if(!n)return null;let o=n.split(": ");return o.length<2?$e(n):`<b>${$e(o[0])}</b> ${$e(o.slice(1).join(": "))}`}function jr(e,t){let n=od(t),o=!1,r=!1,s=!1;if(e==="telegramBotToken"){if(!wt(n))throw new Error("Invalid bot token format (expect digits:secret from BotFather).");return qt(n),o=!0,s=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s}}let i=S();switch(e){case"gatewayMode":{let a=Bn(n);if(!a)throw new Error("gatewayMode: use whatsapp | telegram | both (or wa, tg, b)");i.gatewayMode=a,o=!0;break}case"clusterEnabled":{let a=Le(n);if(a===null)throw new Error("clusterEnabled: true or false");i.clusterEnabled=a;break}case"clusterRole":{let a=n.trim().toLowerCase();if(a!=="primary"&&a!=="secondary")throw new Error("clusterRole: primary | secondary");i.clusterRole=a;break}case"clusterLabel":i.clusterLabel=n.trim().slice(0,128);break;case"clusterSenderBindings":{let a=n.trim();if(a===""||a==="{}"||a.toLowerCase()==="clear"){i.clusterSenderBindings={};break}let l;try{l=JSON.parse(a)}catch{throw new Error('clusterSenderBindings: pass a JSON object, e.g. {"wa:+15551234567":"workshop-box"}')}if(!l||typeof l!="object"||Array.isArray(l))throw new Error("clusterSenderBindings must be a JSON object of sender \u2192 label/id");let c={};for(let[d,u]of Object.entries(l)){if(typeof u!="string"||!u.trim())throw new Error(`clusterSenderBindings.${d}: value must be a non-empty string`);c[d]=u.trim()}i.clusterSenderBindings=c;break}case"commandPrefix":if(!n.trim())throw new Error("commandPrefix cannot be empty");i.commandPrefix=n.trim().slice(0,32);break;case"syncTimeoutMs":{let a=Number.parseInt(n,10);if(!Number.isFinite(a)||a<=0)throw new Error("syncTimeoutMs: positive integer (ms)");i.syncTimeoutMs=a;break}case"syncMaxBytes":{let a=Number.parseInt(n,10);if(!Number.isFinite(a)||a<=0)throw new Error("syncMaxBytes: positive integer");i.syncMaxBytes=a;break}case"jobLogTailLines":{let a=Number.parseInt(n,10);if(!Number.isFinite(a)||a<=0)throw new Error("jobLogTailLines: positive integer");i.jobLogTailLines=a;break}case"shell":if(!n.trim())throw new Error("shell: non-empty path");i.shell=n.trim().slice(0,4096),r=!0;break;case"recipesMacroDefaultCommand":if(!n.trim())throw new Error('recipesMacroDefaultCommand: non-empty shell snippet with "$OMNISH_TASK"');i.recipesMacroDefaultCommand=od(n).trim().slice(0,4096);break;case"appsCols":case"appsRows":case"appsFlushMs":case"appsMinIntervalMs":case"appsMaxFlushBytes":case"appsMaxSessions":case"appsMaxSessionsTotal":case"appsMaxWaChars":case"appsLogTailLines":case"appsSubmitDelayMs":case"appsClearInputDelayMs":case"recipesMaxTaskChars":case"fileSendMaxBytes":case"fileReceiveMaxBytes":{let a=Number.parseInt(n,10);if(!Number.isFinite(a)||a<0)throw new Error(`${e}: non-negative integer`);i[e]=a;break}case"appsClearInput":{let a=Le(n);if(a===null)throw new Error("appsClearInput: true or false");i.appsClearInput=a;break}case"appsClearInputSequence":i.appsClearInputSequence=n.trim().slice(0,200);break;case"appsSkipClearOnPasswordPrompt":{let a=Le(n);if(a===null)throw new Error("appsSkipClearOnPasswordPrompt: true or false");i.appsSkipClearOnPasswordPrompt=a;break}case"appsPasswordPromptHint":{let a=Le(n);if(a===null)throw new Error("appsPasswordPromptHint: true or false");i.appsPasswordPromptHint=a;break}case"fileInboxSubdir":i.fileInboxSubdir=n.trim().slice(0,80);break;case"fileReceiveRootMode":{let a=n.trim();if(!rd.has(a))throw new Error(`fileReceiveRootMode: ${[...rd].join(" | ")}`);i.fileReceiveRootMode=a;break}case"fileReceiveRootPath":i.fileReceiveRootPath=n.trim().slice(0,4096);break;case"recipesRunAttach":{let a=Le(n);if(a===null)throw new Error("recipesRunAttach: true or false");i.recipesRunAttach=a;break}case"recipesAllowDangerousBuiltins":{let a=Le(n);if(a===null)throw new Error("recipesAllowDangerousBuiltins: true or false");i.recipesAllowDangerousBuiltins=a;break}case"serviceInstallFromChat":{let a=Le(n);if(a===null)throw new Error("serviceInstallFromChat: true or false");i.serviceInstallFromChat=a;break}case"updateCheckEnabled":{let a=Le(n);if(a===null)throw new Error("updateCheckEnabled: true or false");i.updateCheckEnabled=a;break}case"updateCheckIntervalMs":{let a=Number.parseInt(n,10);if(!Number.isFinite(a)||a<36e5)throw new Error("updateCheckIntervalMs: integer ms, minimum 3600000 (1 hour)");i.updateCheckIntervalMs=Math.min(6048e5,a);break}case"updateCheckPackageName":if(!n.trim())throw new Error("updateCheckPackageName: non-empty npm package name");i.updateCheckPackageName=n.trim().slice(0,214);break;case"updateInfoUrl":i.updateInfoUrl=n.trim().slice(0,2048);break;case"tunnelEnabled":{let a=Le(n);if(a===null)throw new Error("tunnelEnabled: true or false");return N({tunnelEnabled:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"tunnelRelayUrl":{let a=n.trim();if(!a)throw new Error("tunnelRelayUrl: non-empty URL");let l;try{l=new URL(a)}catch{throw new Error("tunnelRelayUrl: invalid URL")}if(l.protocol!=="http:"&&l.protocol!=="https:")throw new Error("tunnelRelayUrl: use http:// or https://");return N({tunnelRelayUrl:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"tunnelMaxActive":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<1||a>50)throw new Error("tunnelMaxActive: integer 1\u201350");return N({tunnelMaxActive:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmFallbackEnabled":{let a=Le(n);if(a===null)throw new Error("chatLlmFallbackEnabled: true or false");return N({chatLlmFallbackEnabled:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmShellCommand":return N({chatLlmShellCommand:n}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"chatLlmTimeoutMs":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<=0)throw new Error("chatLlmTimeoutMs: positive integer (ms)");return N({chatLlmTimeoutMs:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmMaxInputChars":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<=0)throw new Error("chatLlmMaxInputChars: positive integer");return N({chatLlmMaxInputChars:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmMaxOutputChars":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<=0)throw new Error("chatLlmMaxOutputChars: positive integer");return N({chatLlmMaxOutputChars:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmNeedsTty":{let a=Le(n);if(a===null)throw new Error("chatLlmNeedsTty: true or false");return N({chatLlmNeedsTty:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmWorkDir":return N({chatLlmWorkDir:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"webhookEnabled":{let a=Le(n);if(a===null)throw new Error("webhookEnabled: true or false");return N({webhookEnabled:a}),o=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"webhookPort":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<0||a>65535)throw new Error("webhookPort: integer 0\u201365535 (0 = random free port)");return N({webhookPort:a}),o=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"webhookHost":if(!n.trim())throw new Error("webhookHost: non-empty bind address");return N({webhookHost:n.trim().slice(0,256)}),o=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"webhookToken":return N({webhookToken:n.trim()}),o=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"watchEnabled":{let a=Le(n);if(a===null)throw new Error("watchEnabled: true or false");return N({watchEnabled:a}),Je(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"watchDebounceMs":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<500||a>6e4)throw new Error("watchDebounceMs: integer 500\u201360000");return N({watchDebounceMs:a}),Je(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"watchMaxEventsPerMinute":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<1||a>120)throw new Error("watchMaxEventsPerMinute: integer 1\u2013120");return N({watchMaxEventsPerMinute:a}),Je(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"watchAutoRestore":{let a=Le(n);if(a===null)throw new Error("watchAutoRestore: true or false");return N({watchAutoRestore:a}),Je(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaSendFiles":{let a=Le(n);if(a===null)throw new Error("mediaSendFiles: true or false");return N({mediaSendFiles:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaInstallFromChat":{let a=Le(n);if(a===null)throw new Error("mediaInstallFromChat: true or false");return N({mediaInstallFromChat:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaUrlAutoDl":{let a=Le(n);if(a===null)throw new Error("mediaUrlAutoDl: true or false");return N({mediaUrlAutoDl:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaOutputDir":return N({mediaOutputDir:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"mediaMaxBytes":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<0)throw new Error("mediaMaxBytes: non-negative integer");return N({mediaMaxBytes:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaWhisperModel":return N({mediaWhisperModel:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"progressUpdates":{let a=Le(n);if(a===null)throw new Error("progressUpdates: true or false");return N({progressUpdates:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"pullYtDlpPath":return N({pullYtDlpPath:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"pullFfmpegPath":return N({pullFfmpegPath:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"pullWhisperPath":return N({pullWhisperPath:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}return He(i),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}async function ad(e,t){let n=e.trim(),o=n.toLowerCase();if(!n||o==="help")return ee($u());if(o==="keys"||o==="help keys")return p(["*Configurable keys* (/config set)","",tn.join(", "),"","Examples:","/config set clusterLabel work-laptop",'/config set fileReceiveRootPath "/path/with spaces"',"/config set gatewayMode both","/config set tunnelEnabled true","/config set chatLlmFallbackEnabled false","/config set webhookEnabled true","/config set watchEnabled true"].join(`
79
+ `));if(o==="show"){let i=S();return we(ty(i),ny(i))}let r=n.match(/^get\s+(\S+)\s*$/i);if(r){let i=r[1],a=S(),l=id(a,i);if(!l)return p(`Unknown key "${i}". /config keys`);let c=oy(a,i);return we(`${l}
80
80
 
81
- ${_}`,`${d}<br/><br/><code>${$e(_)}</code>`)}let s=n.match(/^set\s+(\S+)\s+([\s\S]+)$/i);if(s){let i=s[1],a=s[2]??"";if(!Fu(i))return p(`Unknown key "${i}". /config keys`);try{let{reloadSuggested:l,shellWarning:d,tokenSaved:u}=Ir(i,a),c="";if(t?.reload&&l){let y=await t.reload();c=y.ok?`
81
+ ${_}`,`${c}<br/><br/><code>${$e(_)}</code>`)}let s=n.match(/^set\s+(\S+)\s+([\s\S]+)$/i);if(s){let i=s[1],a=s[2]??"";if(!sd(i))return p(`Unknown key "${i}". /config keys`);try{let{reloadSuggested:l,shellWarning:c,tokenSaved:d}=jr(i,a),u="";if(t?.reload&&l){let y=await t.reload();u=y.ok?`
82
82
  Reload: ${y.summary}`:`
83
- Reload failed: ${y.error}`}else l?c=`
84
- Send /reload while omnish run is active (gatewayMode / Telegram).`:c=`
85
- Send /reload to pick up changes where applicable.`;let m=d?`
86
- \u26A0 shell changed \u2014 affects all commands run via omnish.`:"",h=u?`
87
- telegramBotToken saved (not echoed).`:"",f=`Set *${i}* (saved).${h}${m}${c}`,g=`<b>Set ${$e(i)}</b> (saved).${h?"<br/>token saved (not echoed).":""}${d?"<br/>\u26A0 shell path changed.":""}${$e(c)}`;return fe(f,g)}catch(l){return p(`Error: ${String(l)}`)}}return p("Unknown /config command. Try /config help or /config show")}pe();var Yi=[...Vt,"platformToken","platformDeviceId"],Qi={platform_url:"tunnelRelayUrl",tunnel_relay_url:"tunnelRelayUrl",platform_token:"platformToken",token:"platformToken",omnish_token:"platformToken",platform_device_id:"platformDeviceId",device_id:"platformDeviceId"};for(let e of Yi)Qi[e]=e;function Lr(e){let t=e.trim().toLowerCase().replace(/-/g,"_");return Qi[t]??null}function Du(){return Object.keys(Qi).sort()}function Le(e){let t=e.trim().toLowerCase();return t==="true"||t==="1"||t==="yes"||t==="on"?!0:t==="false"||t==="0"||t==="no"||t==="off"?!1:null}function pg(e){let t=e.trim();return(t.startsWith('"')&&t.endsWith('"')&&t.length>=2||t.startsWith("'")&&t.endsWith("'")&&t.length>=2)&&(t=t.slice(1,-1)),t}function mg(e){return I[e]}function Dn(e,t){let n=pg(t),o=S();switch(e){case"platformToken":return O({platformToken:n});case"platformDeviceId":return O({platformDeviceId:n});case"gatewayMode":{let r=En(n);if(!r)throw new Error('gatewayMode: "whatsapp", "telegram", or "both"');return o.gatewayMode=r,Be(o),o}case"telegramBotToken":if(!gt(n))throw new Error("telegramBotToken: invalid bot token format");return Ht(n);case"tunnelRelayUrl":{let r=n.trim();if(!r)throw new Error("tunnelRelayUrl: non-empty URL");let s;try{s=new URL(r)}catch{throw new Error("tunnelRelayUrl: invalid URL")}if(s.protocol!=="http:"&&s.protocol!=="https:")throw new Error("tunnelRelayUrl: use http:// or https://");return O({tunnelRelayUrl:r})}case"tunnelEnabled":{let r=Le(n);if(r===null)throw new Error("tunnelEnabled: true or false");return O({tunnelEnabled:r})}case"tunnelMaxActive":{let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<1||r>50)throw new Error("tunnelMaxActive: integer 1\u201350");return O({tunnelMaxActive:r})}case"clusterSenderBindings":{let r;try{r=JSON.parse(n)}catch{throw new Error("clusterSenderBindings: valid JSON object")}if(!r||typeof r!="object"||Array.isArray(r))throw new Error("clusterSenderBindings: JSON object required");return O({clusterSenderBindings:r})}default:break}if(e==="clusterEnabled"){let r=Le(n);if(r===null)throw new Error("clusterEnabled: true or false");o.clusterEnabled=r}else if(e==="clusterRole"){if(n!=="primary"&&n!=="secondary")throw new Error('clusterRole: "primary" or "secondary"');o.clusterRole=n}else if(e==="clusterLabel")o.clusterLabel=n.trim().slice(0,64);else if(e==="commandPrefix"){if(!n)throw new Error("commandPrefix: non-empty");o.commandPrefix=n}else if(e==="shell"){if(!n)throw new Error("shell: non-empty path");o.shell=n}else if(e==="syncTimeoutMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("syncTimeoutMs: positive integer");o.syncTimeoutMs=r}else if(e==="syncMaxBytes"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("syncMaxBytes: positive integer");o.syncMaxBytes=r}else if(e==="jobLogTailLines"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("jobLogTailLines: positive integer");o.jobLogTailLines=r}else if(e==="appsCols"||e==="appsRows"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<20)throw new Error(`${e}: integer >= 20`);o[e]=r}else if(e==="appsFlushMs"||e==="appsMinIntervalMs"||e==="appsMaxFlushBytes"||e==="appsMaxWaChars"||e==="appsLogTailLines"||e==="appsSubmitDelayMs"||e==="appsClearInputDelayMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<0)throw new Error(`${e}: non-negative integer`);o[e]=r}else if(e==="appsMaxSessions"||e==="appsMaxSessionsTotal"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<1)throw new Error(`${e}: positive integer`);o[e]=r}else if(e==="appsClearInput"){let r=Le(n);if(r===null)throw new Error("appsClearInput: true or false");o.appsClearInput=r}else if(e==="appsClearInputSequence")o.appsClearInputSequence=n;else if(e==="appsSkipClearOnPasswordPrompt"){let r=Le(n);if(r===null)throw new Error("appsSkipClearOnPasswordPrompt: true or false");o.appsSkipClearOnPasswordPrompt=r}else if(e==="appsPasswordPromptHint"){let r=Le(n);if(r===null)throw new Error("appsPasswordPromptHint: true or false");o.appsPasswordPromptHint=r}else if(e==="fileSendMaxBytes"||e==="fileReceiveMaxBytes"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<0)throw new Error(`${e}: non-negative integer`);o[e]=r}else if(e==="fileInboxSubdir")o.fileInboxSubdir=n.trim().slice(0,128);else if(e==="fileReceiveRootMode"){if(!new Set(["downloads","omnishData","sessionCwd","processCwd","fixed"]).has(n))throw new Error("fileReceiveRootMode: downloads|omnishData|sessionCwd|processCwd|fixed");o.fileReceiveRootMode=n}else if(e==="fileReceiveRootPath")o.fileReceiveRootPath=n.trim().slice(0,4096);else if(e==="recipesAllowDangerousBuiltins"){let r=Le(n);if(r===null)throw new Error("recipesAllowDangerousBuiltins: true or false");o.recipesAllowDangerousBuiltins=r}else if(e==="recipesMaxTaskChars"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<0)throw new Error("recipesMaxTaskChars: non-negative integer");o.recipesMaxTaskChars=r}else if(e==="recipesMacroDefaultCommand"){if(!n.includes("$OMNISH_TASK"))throw new Error('recipesMacroDefaultCommand: must include "$OMNISH_TASK"');o.recipesMacroDefaultCommand=n}else if(e==="recipesRunAttach"){let r=Le(n);if(r===null)throw new Error("recipesRunAttach: true or false");o.recipesRunAttach=r}else if(e==="serviceInstallFromChat"){let r=Le(n);if(r===null)throw new Error("serviceInstallFromChat: true or false");o.serviceInstallFromChat=r}else if(e==="updateCheckEnabled"){let r=Le(n);if(r===null)throw new Error("updateCheckEnabled: true or false");o.updateCheckEnabled=r}else if(e==="updateCheckIntervalMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<36e5)throw new Error("updateCheckIntervalMs: min 3600000");o.updateCheckIntervalMs=Math.min(6048e5,r)}else if(e==="updateCheckPackageName"){if(!n.trim())throw new Error("updateCheckPackageName: non-empty");o.updateCheckPackageName=n.trim().slice(0,214)}else if(e==="updateInfoUrl")o.updateInfoUrl=n.trim().slice(0,2048);else if(e==="chatLlmFallbackEnabled"){let r=Le(n);if(r===null)throw new Error("chatLlmFallbackEnabled: true or false");return O({chatLlmFallbackEnabled:r})}else{if(e==="chatLlmShellCommand")return O({chatLlmShellCommand:n});if(e==="chatLlmTimeoutMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("chatLlmTimeoutMs: positive integer");return O({chatLlmTimeoutMs:r})}else if(e==="chatLlmMaxInputChars"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("chatLlmMaxInputChars: positive integer");return O({chatLlmMaxInputChars:r})}else if(e==="chatLlmMaxOutputChars"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("chatLlmMaxOutputChars: positive integer");return O({chatLlmMaxOutputChars:r})}else if(e==="chatLlmNeedsTty"){let r=Le(n);if(r===null)throw new Error("chatLlmNeedsTty: true or false");return O({chatLlmNeedsTty:r})}else{if(e==="chatLlmWorkDir")return O({chatLlmWorkDir:n.trim()});if(e==="webhookEnabled"){let r=Le(n);if(r===null)throw new Error("webhookEnabled: true or false");return O({webhookEnabled:r})}else if(e==="webhookPort"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<0||r>65535)throw new Error("webhookPort: integer 0\u201365535");return O({webhookPort:r})}else if(e==="webhookHost"){if(!n.trim())throw new Error("webhookHost: non-empty");return O({webhookHost:n.trim().slice(0,256)})}else{if(e==="webhookToken")return O({webhookToken:n.trim()});if(e==="watchEnabled"){let r=Le(n);if(r===null)throw new Error("watchEnabled: true or false");return O({watchEnabled:r})}else if(e==="watchDebounceMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<500||r>6e4)throw new Error("watchDebounceMs: integer 500\u201360000");return O({watchDebounceMs:r})}else if(e==="watchMaxEventsPerMinute"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<1||r>120)throw new Error("watchMaxEventsPerMinute: integer 1\u2013120");return O({watchMaxEventsPerMinute:r})}else if(e==="watchAutoRestore"){let r=Le(n);if(r===null)throw new Error("watchAutoRestore: true or false");return O({watchAutoRestore:r})}else if(e==="pullEnabled"){let r=Le(n);if(r===null)throw new Error("pullEnabled: true or false");return O({pullEnabled:r})}else if(e==="pullInstallFromChat"){let r=Le(n);if(r===null)throw new Error("pullInstallFromChat: true or false");return O({pullInstallFromChat:r})}else if(e==="pullUrlAutoDetect"){let r=Le(n);if(r===null)throw new Error("pullUrlAutoDetect: true or false");return O({pullUrlAutoDetect:r})}else if(e==="pullDefaultMode"){if(!new Set(["video","audio","subs","transcript","all"]).has(n.trim()))throw new Error("pullDefaultMode: video|audio|subs|transcript|all");return O({pullDefaultMode:n.trim()})}else{if(e==="pullOutputDir")return O({pullOutputDir:n.trim()});if(e==="pullMaxBytes"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<0)throw new Error("pullMaxBytes: non-negative integer");return O({pullMaxBytes:r})}else if(e==="pullAutoSend"){let r=Le(n);if(r===null)throw new Error("pullAutoSend: true or false");return O({pullAutoSend:r})}else{if(e==="pullWhisperModel")return O({pullWhisperModel:n.trim()});if(e==="pullYtDlpPath")return O({pullYtDlpPath:n.trim()});if(e==="pullFfmpegPath")return O({pullFfmpegPath:n.trim()});if(e==="pullWhisperPath")return O({pullWhisperPath:n.trim()});throw new Error(`Unsupported key: ${e}`)}}}}}return Be(o),o}function Uu(e){let t=mg(e);if(e==="platformToken"||e==="platformDeviceId")return O({[e]:t});if(e==="tunnelRelayUrl")return O({tunnelRelayUrl:t});let n=S();return n[e]=t,Be(n),n}function hn(e,t){if(e==="platformToken"||e==="telegramBotToken"||e==="webhookToken"){let n=t.trim();return n?n.length<=8?"(set)":`${n.slice(0,4)}\u2026${n.slice(-4)}`:"(empty)"}return t}pe();G();import Un from"node:fs";import ea from"node:os";import $o from"node:path";import Gu from"node:crypto";var Xi="\u2063omnish/c v1",hg=/([nlra])=([^\s\]]*)/g;function ut(e){return e.replace(/-/g,"").slice(0,8)}function Vi(e){return e.replace(/[\s\]\r\n]/g,"_").slice(0,64)}function fg(e){let t=Vi(ut(e.nodeId)),n=Vi(e.label||""),o=e.role==="primary"?"p":"s",r=Vi(e.activeNodeId?ut(e.activeNodeId):"");return`${Xi} [n=${t} l=${n} r=${o} a=${r}]`}function Bu(e,t){let n=fg(t);if(e.includes(n))return e;let o=e.replace(/\s+$/,"");return o.length===0?n:`${o}
88
-
89
- ${n}`}function Hu(e){if(!e||!e.includes(Xi))return null;let t=e.indexOf(Xi),n=e.slice(t),o=n.indexOf("["),r=n.indexOf("]");if(o<0||r<0||r<o)return null;let s=n.slice(o+1,r),i={};for(let l of s.matchAll(hg))i[l[1]]=l[2]??"";if(!i.n)return null;let a=i.r==="p"?"primary":"secondary";return{nodeId:i.n,label:i.l??"",role:a,activeNodeId:i.a??""}}var qu=3,gg="node-id",yg="cluster-local.json",ju=8;function Ju(){return $o.join(W,yg)}function ta(){return $o.join(W,gg)}function nt(){B(W);let e=ta();try{if(Un.existsSync(e)){let n=Un.readFileSync(e,"utf8").trim();if(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(n))return n}}catch{}let t=Gu.randomUUID();return Un.writeFileSync(e,`${t}
90
- `,{mode:384}),t}function zu(){return{schemaVersion:qu,updatedAt:new Date().toISOString(),peers:[],senderBindings:{}}}function wg(e){if(!e||typeof e!="object")return{};let t={};for(let[n,o]of Object.entries(e)){if(!o||typeof o!="object")continue;let r=o;if(typeof r.nodeId!="string"||!r.nodeId)continue;let s=r.source==="config"?"config":"chat";t[n]={senderKey:typeof r.senderKey=="string"?r.senderKey:n,nodeId:r.nodeId,sinceIso:typeof r.sinceIso=="string"&&r.sinceIso?r.sinceIso:new Date(0).toISOString(),source:s}}return t}function be(){let e=Ju();try{let t=Un.readFileSync(e,"utf8"),n=JSON.parse(t),o=Array.isArray(n.peers)?n.peers.filter(s=>typeof s=="object"&&s!==null&&typeof s.nodeId=="string"&&typeof s.label=="string").map(s=>({nodeId:s.nodeId,label:s.label,role:s.role==="primary"?"primary":"secondary",lastSeenIso:typeof s.lastSeenIso=="string"?s.lastSeenIso:new Date(0).toISOString()})):[],r=wg(n.senderBindings);return{schemaVersion:qu,updatedAt:typeof n.updatedAt=="string"?n.updatedAt:new Date().toISOString(),peers:o,senderBindings:r}}catch{return zu()}}function bg(e,t){let n=$o.dirname(e);B(n);let o=`${JSON.stringify(t,null,2)}
91
- `,r=$o.join(n,`.${$o.basename(e)}.tmp.${process.pid}.${Gu.randomBytes(4).toString("hex")}`);Un.writeFileSync(r,o,{mode:384}),Un.renameSync(r,e)}function oa(e){let t=Ju(),n=zu();for(let o=0;o<ju;o++){n=be(),e(n),n.updatedAt=new Date().toISOString();try{return bg(t,n),n}catch{}}throw new Error(`Could not write cluster local state after ${ju} attempts: ${t}`)}function Ku(e){return(e.clusterLabel??"").trim()||ea.hostname()}function ke(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function Yu(e,t){let n=e.peers.findIndex(o=>o.nodeId===t.nodeId);n>=0?e.peers[n]=t:e.peers.push(t)}function Mo(e){return{nodeId:ut(nt()),label:Ku(e),role:e.clusterRole,lastSeenIso:new Date().toISOString()}}function Qu(e,t,n){let o=n.trim();if(!o)return{ok:!1,reason:"not-found"};let r=Mo(t),s=[r];for(let l of e.peers)l.nodeId!==r.nodeId&&s.push(l);let i=o.toLowerCase();if(/^[0-9a-f]{8}$/i.test(o)){let l=s.find(d=>d.nodeId.toLowerCase()===i);if(l)return{ok:!0,peer:l}}let a=s.filter(l=>l.label.toLowerCase()===i);return a.length===1?{ok:!0,peer:a[0]}:a.length>1?{ok:!1,reason:"ambiguous-label",matches:a}:{ok:!1,reason:"not-found"}}function It(e,t){let n=be(),o=n.senderBindings[t];if(o&&o.nodeId)return o;let r=e.clusterSenderBindings?.[t];if(typeof r=="string"&&r.trim()){let s=Qu(n,e,r);if(s.ok)return{senderKey:t,nodeId:s.peer.nodeId,sinceIso:new Date(0).toISOString(),source:"config"}}return null}function na(e,t,n="chat"){let o=S(),r=be(),s=Qu(r,o,t);if(!s.ok)return{state:r,resolved:s};let i=new Date().toISOString();return{state:oa(l=>{l.senderBindings[e]={senderKey:e,nodeId:s.peer.nodeId,sinceIso:i,source:n},Yu(l,{...s.peer,lastSeenIso:i})}),resolved:s}}function kg(e){let t=null;return{state:oa(o=>{o.senderBindings[e]&&(t=o.senderBindings[e]??null,delete o.senderBindings[e])}),removed:t}}function Vu(e,t){let n=ut(nt()),o=new Date().toISOString();return oa(r=>{if(Yu(r,{nodeId:e.nodeId,label:e.label||e.nodeId,role:e.role,lastSeenIso:o}),e.nodeId!==n&&t&&e.activeNodeId){let s=r.senderBindings[t];(!s||s.nodeId!==e.activeNodeId)&&(r.senderBindings[t]={senderKey:t,nodeId:e.activeNodeId,sinceIso:o,source:"chat"})}})}function Xu(e,t){let n=It(t,e);return n?n.nodeId===ut(nt()):!1}function vg(e,t){let n=Mo(t).nodeId,o=new Set([n]);for(let s of e.peers)o.add(s.nodeId);return[...o].sort()[0]??n}function Je(e,t){return vg(e,t)===ut(nt())}function Or(e,t,n){let o=ut(nt()),r=["*Computers*",""],s=n?It(t,n):null;n&&(s?r.push(`Your binding: \`${s.nodeId}\` (${s.source})`):r.push("Your binding: (none \u2014 send /c use <label-or-id>)"),r.push(""));let i=new Map;i.set(o,Mo(t));for(let l of e.peers)i.set(l.nodeId,l);let a=[...i.values()].sort((l,d)=>l.label.localeCompare(d.label));if(a.length===0)return r.push("(no peers observed yet \u2014 run /c status from this chat)"),r.join(`
92
- `);for(let l of a){let d=l.nodeId===o,c=[(s?l.nodeId===s.nodeId:!1)?"your binding":null,d?"you":null].filter(Boolean).join(", "),m=c?` (${c})`:"";r.push(`${Ae}*${l.label}*${m}
83
+ Reload failed: ${y.error}`}else l?u=`
84
+ Send /reload while omnish run is active (gatewayMode / Telegram).`:u=`
85
+ Send /reload to pick up changes where applicable.`;let m=c?`
86
+ \u26A0 shell changed \u2014 affects all commands run via omnish.`:"",h=d?`
87
+ telegramBotToken saved (not echoed).`:"",f=`Set *${i}* (saved).${h}${m}${u}`,g=`<b>Set ${$e(i)}</b> (saved).${h?"<br/>token saved (not echoed).":""}${c?"<br/>\u26A0 shell path changed.":""}${$e(u)}`;return we(f,g)}catch(l){return p(`Error: ${String(l)}`)}}return p("Unknown /config command. Try /config help or /config show")}pe();var ha=[...tn,"platformToken","platformDeviceId"],fa={platform_url:"tunnelRelayUrl",tunnel_relay_url:"tunnelRelayUrl",platform_token:"platformToken",token:"platformToken",omnish_token:"platformToken",platform_device_id:"platformDeviceId",device_id:"platformDeviceId"};for(let e of ha)fa[e]=e;function Gr(e){let t=e.trim().toLowerCase().replace(/-/g,"_");return fa[t]??null}function ld(){return Object.keys(fa).sort()}function Oe(e){let t=e.trim().toLowerCase();return t==="true"||t==="1"||t==="yes"||t==="on"?!0:t==="false"||t==="0"||t==="no"||t==="off"?!1:null}function ry(e){let t=e.trim();return(t.startsWith('"')&&t.endsWith('"')&&t.length>=2||t.startsWith("'")&&t.endsWith("'")&&t.length>=2)&&(t=t.slice(1,-1)),t}function sy(e){return I[e]}function Qn(e,t){let n=ry(t),o=S();switch(e){case"platformToken":return N({platformToken:n});case"platformDeviceId":return N({platformDeviceId:n});case"gatewayMode":{let r=Bn(n);if(!r)throw new Error('gatewayMode: "whatsapp", "telegram", or "both"');return o.gatewayMode=r,He(o),o}case"telegramBotToken":if(!wt(n))throw new Error("telegramBotToken: invalid bot token format");return qt(n);case"tunnelRelayUrl":{let r=n.trim();if(!r)throw new Error("tunnelRelayUrl: non-empty URL");let s;try{s=new URL(r)}catch{throw new Error("tunnelRelayUrl: invalid URL")}if(s.protocol!=="http:"&&s.protocol!=="https:")throw new Error("tunnelRelayUrl: use http:// or https://");return N({tunnelRelayUrl:r})}case"tunnelEnabled":{let r=Oe(n);if(r===null)throw new Error("tunnelEnabled: true or false");return N({tunnelEnabled:r})}case"tunnelMaxActive":{let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<1||r>50)throw new Error("tunnelMaxActive: integer 1\u201350");return N({tunnelMaxActive:r})}case"clusterSenderBindings":{let r;try{r=JSON.parse(n)}catch{throw new Error("clusterSenderBindings: valid JSON object")}if(!r||typeof r!="object"||Array.isArray(r))throw new Error("clusterSenderBindings: JSON object required");return N({clusterSenderBindings:r})}default:break}if(e==="clusterEnabled"){let r=Oe(n);if(r===null)throw new Error("clusterEnabled: true or false");o.clusterEnabled=r}else if(e==="clusterRole"){if(n!=="primary"&&n!=="secondary")throw new Error('clusterRole: "primary" or "secondary"');o.clusterRole=n}else if(e==="clusterLabel")o.clusterLabel=n.trim().slice(0,64);else if(e==="commandPrefix"){if(!n)throw new Error("commandPrefix: non-empty");o.commandPrefix=n}else if(e==="shell"){if(!n)throw new Error("shell: non-empty path");o.shell=n}else if(e==="syncTimeoutMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("syncTimeoutMs: positive integer");o.syncTimeoutMs=r}else if(e==="syncMaxBytes"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("syncMaxBytes: positive integer");o.syncMaxBytes=r}else if(e==="jobLogTailLines"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("jobLogTailLines: positive integer");o.jobLogTailLines=r}else if(e==="appsCols"||e==="appsRows"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<20)throw new Error(`${e}: integer >= 20`);o[e]=r}else if(e==="appsFlushMs"||e==="appsMinIntervalMs"||e==="appsMaxFlushBytes"||e==="appsMaxWaChars"||e==="appsLogTailLines"||e==="appsSubmitDelayMs"||e==="appsClearInputDelayMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<0)throw new Error(`${e}: non-negative integer`);o[e]=r}else if(e==="appsMaxSessions"||e==="appsMaxSessionsTotal"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<1)throw new Error(`${e}: positive integer`);o[e]=r}else if(e==="appsClearInput"){let r=Oe(n);if(r===null)throw new Error("appsClearInput: true or false");o.appsClearInput=r}else if(e==="appsClearInputSequence")o.appsClearInputSequence=n;else if(e==="appsSkipClearOnPasswordPrompt"){let r=Oe(n);if(r===null)throw new Error("appsSkipClearOnPasswordPrompt: true or false");o.appsSkipClearOnPasswordPrompt=r}else if(e==="appsPasswordPromptHint"){let r=Oe(n);if(r===null)throw new Error("appsPasswordPromptHint: true or false");o.appsPasswordPromptHint=r}else if(e==="fileSendMaxBytes"||e==="fileReceiveMaxBytes"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<0)throw new Error(`${e}: non-negative integer`);o[e]=r}else if(e==="fileInboxSubdir")o.fileInboxSubdir=n.trim().slice(0,128);else if(e==="fileReceiveRootMode"){if(!new Set(["downloads","omnishData","sessionCwd","processCwd","fixed"]).has(n))throw new Error("fileReceiveRootMode: downloads|omnishData|sessionCwd|processCwd|fixed");o.fileReceiveRootMode=n}else if(e==="fileReceiveRootPath")o.fileReceiveRootPath=n.trim().slice(0,4096);else if(e==="recipesAllowDangerousBuiltins"){let r=Oe(n);if(r===null)throw new Error("recipesAllowDangerousBuiltins: true or false");o.recipesAllowDangerousBuiltins=r}else if(e==="recipesMaxTaskChars"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<0)throw new Error("recipesMaxTaskChars: non-negative integer");o.recipesMaxTaskChars=r}else if(e==="recipesMacroDefaultCommand"){if(!n.includes("$OMNISH_TASK"))throw new Error('recipesMacroDefaultCommand: must include "$OMNISH_TASK"');o.recipesMacroDefaultCommand=n}else if(e==="recipesRunAttach"){let r=Oe(n);if(r===null)throw new Error("recipesRunAttach: true or false");o.recipesRunAttach=r}else if(e==="serviceInstallFromChat"){let r=Oe(n);if(r===null)throw new Error("serviceInstallFromChat: true or false");o.serviceInstallFromChat=r}else if(e==="updateCheckEnabled"){let r=Oe(n);if(r===null)throw new Error("updateCheckEnabled: true or false");o.updateCheckEnabled=r}else if(e==="updateCheckIntervalMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<36e5)throw new Error("updateCheckIntervalMs: min 3600000");o.updateCheckIntervalMs=Math.min(6048e5,r)}else if(e==="updateCheckPackageName"){if(!n.trim())throw new Error("updateCheckPackageName: non-empty");o.updateCheckPackageName=n.trim().slice(0,214)}else if(e==="updateInfoUrl")o.updateInfoUrl=n.trim().slice(0,2048);else if(e==="chatLlmFallbackEnabled"){let r=Oe(n);if(r===null)throw new Error("chatLlmFallbackEnabled: true or false");return N({chatLlmFallbackEnabled:r})}else{if(e==="chatLlmShellCommand")return N({chatLlmShellCommand:n});if(e==="chatLlmTimeoutMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("chatLlmTimeoutMs: positive integer");return N({chatLlmTimeoutMs:r})}else if(e==="chatLlmMaxInputChars"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("chatLlmMaxInputChars: positive integer");return N({chatLlmMaxInputChars:r})}else if(e==="chatLlmMaxOutputChars"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("chatLlmMaxOutputChars: positive integer");return N({chatLlmMaxOutputChars:r})}else if(e==="chatLlmNeedsTty"){let r=Oe(n);if(r===null)throw new Error("chatLlmNeedsTty: true or false");return N({chatLlmNeedsTty:r})}else{if(e==="chatLlmWorkDir")return N({chatLlmWorkDir:n.trim()});if(e==="webhookEnabled"){let r=Oe(n);if(r===null)throw new Error("webhookEnabled: true or false");return N({webhookEnabled:r})}else if(e==="webhookPort"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<0||r>65535)throw new Error("webhookPort: integer 0\u201365535");return N({webhookPort:r})}else if(e==="webhookHost"){if(!n.trim())throw new Error("webhookHost: non-empty");return N({webhookHost:n.trim().slice(0,256)})}else{if(e==="webhookToken")return N({webhookToken:n.trim()});if(e==="watchEnabled"){let r=Oe(n);if(r===null)throw new Error("watchEnabled: true or false");return N({watchEnabled:r})}else if(e==="watchDebounceMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<500||r>6e4)throw new Error("watchDebounceMs: integer 500\u201360000");return N({watchDebounceMs:r})}else if(e==="watchMaxEventsPerMinute"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<1||r>120)throw new Error("watchMaxEventsPerMinute: integer 1\u2013120");return N({watchMaxEventsPerMinute:r})}else if(e==="watchAutoRestore"){let r=Oe(n);if(r===null)throw new Error("watchAutoRestore: true or false");return N({watchAutoRestore:r})}else if(e==="mediaSendFiles"){let r=Oe(n);if(r===null)throw new Error("mediaSendFiles: true or false");return N({mediaSendFiles:r})}else if(e==="mediaInstallFromChat"){let r=Oe(n);if(r===null)throw new Error("mediaInstallFromChat: true or false");return N({mediaInstallFromChat:r})}else if(e==="mediaUrlAutoDl"){let r=Oe(n);if(r===null)throw new Error("mediaUrlAutoDl: true or false");return N({mediaUrlAutoDl:r})}else{if(e==="mediaOutputDir")return N({mediaOutputDir:n.trim()});if(e==="mediaMaxBytes"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<0)throw new Error("mediaMaxBytes: non-negative integer");return N({mediaMaxBytes:r})}else{if(e==="mediaWhisperModel")return N({mediaWhisperModel:n.trim()});if(e==="progressUpdates"){let r=Oe(n);if(r===null)throw new Error("progressUpdates: true or false");return N({progressUpdates:r})}else{if(e==="pullYtDlpPath")return N({pullYtDlpPath:n.trim()});if(e==="pullFfmpegPath")return N({pullFfmpegPath:n.trim()});if(e==="pullWhisperPath")return N({pullWhisperPath:n.trim()});throw new Error(`Unsupported key: ${e}`)}}}}}}return He(o),o}function cd(e){let t=sy(e);if(e==="platformToken"||e==="platformDeviceId")return N({[e]:t});if(e==="tunnelRelayUrl")return N({tunnelRelayUrl:t});let n=S();return n[e]=t,He(n),n}function vn(e,t){if(e==="platformToken"||e==="telegramBotToken"||e==="webhookToken"){let n=t.trim();return n?n.length<=8?"(set)":`${n.slice(0,4)}\u2026${n.slice(-4)}`:"(empty)"}return t}pe();q();import Vn from"node:fs";import ba from"node:os";import _o from"node:path";import md from"node:crypto";var ya="\u2063omnish/c v1",iy=/([nlra])=([^\s\]]*)/g;function ut(e){return e.replace(/-/g,"").slice(0,8)}function ga(e){return e.replace(/[\s\]\r\n]/g,"_").slice(0,64)}function ay(e){let t=ga(ut(e.nodeId)),n=ga(e.label||""),o=e.role==="primary"?"p":"s",r=ga(e.activeNodeId?ut(e.activeNodeId):"");return`${ya} [n=${t} l=${n} r=${o} a=${r}]`}function ud(e,t){let n=ay(t);if(e.includes(n))return e;let o=e.replace(/\s+$/,"");return o.length===0?n:`${o}
88
+
89
+ ${n}`}function dd(e){if(!e||!e.includes(ya))return null;let t=e.indexOf(ya),n=e.slice(t),o=n.indexOf("["),r=n.indexOf("]");if(o<0||r<0||r<o)return null;let s=n.slice(o+1,r),i={};for(let l of s.matchAll(iy))i[l[1]]=l[2]??"";if(!i.n)return null;let a=i.r==="p"?"primary":"secondary";return{nodeId:i.n,label:i.l??"",role:a,activeNodeId:i.a??""}}var hd=3,ly="node-id",cy="cluster-local.json",pd=8;function fd(){return _o.join(D,cy)}function ka(){return _o.join(D,ly)}function ot(){H(D);let e=ka();try{if(Vn.existsSync(e)){let n=Vn.readFileSync(e,"utf8").trim();if(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(n))return n}}catch{}let t=md.randomUUID();return Vn.writeFileSync(e,`${t}
90
+ `,{mode:384}),t}function gd(){return{schemaVersion:hd,updatedAt:new Date().toISOString(),peers:[],senderBindings:{}}}function uy(e){if(!e||typeof e!="object")return{};let t={};for(let[n,o]of Object.entries(e)){if(!o||typeof o!="object")continue;let r=o;if(typeof r.nodeId!="string"||!r.nodeId)continue;let s=r.source==="config"?"config":"chat";t[n]={senderKey:typeof r.senderKey=="string"?r.senderKey:n,nodeId:r.nodeId,sinceIso:typeof r.sinceIso=="string"&&r.sinceIso?r.sinceIso:new Date(0).toISOString(),source:s}}return t}function ve(){let e=fd();try{let t=Vn.readFileSync(e,"utf8"),n=JSON.parse(t),o=Array.isArray(n.peers)?n.peers.filter(s=>typeof s=="object"&&s!==null&&typeof s.nodeId=="string"&&typeof s.label=="string").map(s=>({nodeId:s.nodeId,label:s.label,role:s.role==="primary"?"primary":"secondary",lastSeenIso:typeof s.lastSeenIso=="string"?s.lastSeenIso:new Date(0).toISOString()})):[],r=uy(n.senderBindings);return{schemaVersion:hd,updatedAt:typeof n.updatedAt=="string"?n.updatedAt:new Date().toISOString(),peers:o,senderBindings:r}}catch{return gd()}}function dy(e,t){let n=_o.dirname(e);H(n);let o=`${JSON.stringify(t,null,2)}
91
+ `,r=_o.join(n,`.${_o.basename(e)}.tmp.${process.pid}.${md.randomBytes(4).toString("hex")}`);Vn.writeFileSync(r,o,{mode:384}),Vn.renameSync(r,e)}function Sa(e){let t=fd(),n=gd();for(let o=0;o<pd;o++){n=ve(),e(n),n.updatedAt=new Date().toISOString();try{return dy(t,n),n}catch{}}throw new Error(`Could not write cluster local state after ${pd} attempts: ${t}`)}function yd(e){return(e.clusterLabel??"").trim()||ba.hostname()}function Se(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function wd(e,t){let n=e.peers.findIndex(o=>o.nodeId===t.nodeId);n>=0?e.peers[n]=t:e.peers.push(t)}function Do(e){return{nodeId:ut(ot()),label:yd(e),role:e.clusterRole,lastSeenIso:new Date().toISOString()}}function bd(e,t,n){let o=n.trim();if(!o)return{ok:!1,reason:"not-found"};let r=Do(t),s=[r];for(let l of e.peers)l.nodeId!==r.nodeId&&s.push(l);let i=o.toLowerCase();if(/^[0-9a-f]{8}$/i.test(o)){let l=s.find(c=>c.nodeId.toLowerCase()===i);if(l)return{ok:!0,peer:l}}let a=s.filter(l=>l.label.toLowerCase()===i);return a.length===1?{ok:!0,peer:a[0]}:a.length>1?{ok:!1,reason:"ambiguous-label",matches:a}:{ok:!1,reason:"not-found"}}function Nt(e,t){let n=ve(),o=n.senderBindings[t];if(o&&o.nodeId)return o;let r=e.clusterSenderBindings?.[t];if(typeof r=="string"&&r.trim()){let s=bd(n,e,r);if(s.ok)return{senderKey:t,nodeId:s.peer.nodeId,sinceIso:new Date(0).toISOString(),source:"config"}}return null}function va(e,t,n="chat"){let o=S(),r=ve(),s=bd(r,o,t);if(!s.ok)return{state:r,resolved:s};let i=new Date().toISOString();return{state:Sa(l=>{l.senderBindings[e]={senderKey:e,nodeId:s.peer.nodeId,sinceIso:i,source:n},wd(l,{...s.peer,lastSeenIso:i})}),resolved:s}}function py(e){let t=null;return{state:Sa(o=>{o.senderBindings[e]&&(t=o.senderBindings[e]??null,delete o.senderBindings[e])}),removed:t}}function kd(e,t){let n=ut(ot()),o=new Date().toISOString();return Sa(r=>{if(wd(r,{nodeId:e.nodeId,label:e.label||e.nodeId,role:e.role,lastSeenIso:o}),e.nodeId!==n&&t&&e.activeNodeId){let s=r.senderBindings[t];(!s||s.nodeId!==e.activeNodeId)&&(r.senderBindings[t]={senderKey:t,nodeId:e.activeNodeId,sinceIso:o,source:"chat"})}})}function vd(e,t){let n=Nt(t,e);return n?n.nodeId===ut(ot()):!1}function my(e,t){let n=Do(t).nodeId,o=new Set([n]);for(let s of e.peers)o.add(s.nodeId);return[...o].sort()[0]??n}function qe(e,t){return my(e,t)===ut(ot())}function Jr(e,t,n){let o=ut(ot()),r=["*Computers*",""],s=n?Nt(t,n):null;n&&(s?r.push(`Your binding: \`${s.nodeId}\` (${s.source})`):r.push("Your binding: (none \u2014 send /c use <label-or-id>)"),r.push(""));let i=new Map;i.set(o,Do(t));for(let l of e.peers)i.set(l.nodeId,l);let a=[...i.values()].sort((l,c)=>l.label.localeCompare(c.label));if(a.length===0)return r.push("(no peers observed yet \u2014 run /c status from this chat)"),r.join(`
92
+ `);for(let l of a){let c=l.nodeId===o,u=[(s?l.nodeId===s.nodeId:!1)?"your binding":null,c?"you":null].filter(Boolean).join(", "),m=u?` (${u})`:"";r.push(`${Ae}*${l.label}*${m}
93
93
  id \`${l.nodeId}\` \xB7 role ${l.role}
94
94
  seen ${l.lastSeenIso}`)}return r.push(""),r.push(`Updated: ${e.updatedAt}`),r.join(`
95
- `)}function Zi(e,t,n){let o=ut(nt()),r=["<b>Computers</b>",""],s=n?It(t,n):null;n&&(s?r.push(`Your binding: <code>${ke(s.nodeId)}</code> (${ke(s.source)})`):r.push("Your binding: (none \u2014 send /c use &lt;label-or-id&gt;)"),r.push(""));let i=new Map;i.set(o,Mo(t));for(let l of e.peers)i.set(l.nodeId,l);let a=[...i.values()].sort((l,d)=>l.label.localeCompare(d.label));if(a.length===0)return r.push("(no peers observed yet \u2014 run /c status from this chat)"),r.join(`
96
- `);for(let l of a){let d=l.nodeId===o,c=[(s?l.nodeId===s.nodeId:!1)?"your binding":null,d?"you":null].filter(Boolean).join(", "),m=c?` (${ke(c)})`:"";r.push(`\u2022 <b>${ke(l.label)}</b>${m}<br/> id <code>${ke(l.nodeId)}</code> \xB7 role ${ke(l.role)}<br/> seen ${ke(l.lastSeenIso)}`)}return r.push(""),r.push(`Updated: ${ke(e.updatedAt)}`),r.join(`
97
- `)}function ra(e,t,n){return Or(e,t,n??null).replace(/\*([^*]+)\*/g,"$1").replace(/`([^`]+)`/g,"$1")}function sa(e,t){let n=nt(),o=ut(n),r=be(),s=e.clusterRole,i=Ku(e),a=t?It(e,t):null,l=a?a.nodeId===o:!1,d=t?a?`your binding: ${a.nodeId} (${a.source}${l?", here":""})`:"your binding: (none)":"",u=["*This computer*","",`label: ${i}`,`node id: \`${o}\` (full id in ${ta()})`,`host: ${ea.hostname()}`,`clusterRole: ${s}`,d,`peers known: ${r.peers.length}`,`senderBindings (chat): ${Object.keys(r.senderBindings).length}`].filter(Boolean).join(`
98
- `),c=["<b>This computer</b>","",`label: ${ke(i)}`,`node id: <code>${ke(o)}</code> (full id in ${ke(ta())})`,`host: ${ke(ea.hostname())}`,`clusterRole: ${ke(s)}`,t?ke(d):"",`peers known: ${r.peers.length}`,`senderBindings (chat): ${Object.keys(r.senderBindings).length}`].filter(Boolean).join(`
99
- `);return{wa:u,tg:c}}function Sg(e){let t=e.clusterRole,n=["*Computers* (per-sender bindings)","","Each allowlisted phone picks one machine to talk to. Other senders are not affected.","Selection persists; defaults can be set in config (clusterSenderBindings).","","Shorthand: /pcs \u2026 \xB7 /c \u2026","",`${Ae}/c use <label-or-id> \u2014 bind your messages to that machine`,`${Ae}/c here \u2014 bind your messages to THIS machine`,`${Ae}/c using \u2014 show your current binding`,`${Ae}/c unuse \u2014 clear your chat-set binding (config default still applies, if any)`,`${Ae}/c status \u2014 every online host replies with its own paragraph`,`${Ae}/c list \u2014 locally known roster (single responder)`,`${Ae}/c help \u2014 this help`,"",`clusterRole on this host: ${t}`].join(`
100
- `),o=["<b>Computers</b> (per-sender bindings)","","Each allowlisted phone picks one machine to talk to. Other senders are not affected.","Selection persists; defaults can be set in config (clusterSenderBindings).","","Shorthand: /pcs \u2026 \xB7 /c \u2026","","\u2022 /c use &lt;label-or-id&gt; \u2014 bind your messages to that machine","\u2022 /c here \u2014 bind your messages to THIS machine","\u2022 /c using \u2014 show your current binding","\u2022 /c unuse \u2014 clear your chat-set binding","\u2022 /c status \u2014 every online host replies with its own paragraph","\u2022 /c list \u2014 locally known roster (single responder)","\u2022 /c help \u2014 this help","",`clusterRole on this host: ${ke(t)}`].join(`
101
- `);return fe(n,o)}function xg(e,t){if(t.ok)return p("");if(t.reason==="ambiguous-label"){let n=(t.matches??[]).map(o=>`\`${o.nodeId}\` (${o.label})`).join(", ");return p(`Label "${e}" matches multiple machines: ${n}. Use the 8-character id with /c use <id>.`)}return p(`No machine matches "${e}". Send /c list to see ids and labels, or /c status to refresh the roster.`)}function Zu(e,t,n){let o=t.trim().split(/\s+/),r=o[0]?.toLowerCase()??"";if(!r||r==="help"){if(!e.clusterEnabled&&r!=="help"){let l=be();return Je(l,e)?p("Cluster is disabled. Enable with: /config set clusterEnabled true (then /c use <label-or-id>). /c help for the new commands."):null}let a=be();return Je(a,e)?Sg(e):null}let s=ut(nt());if(r==="use"||r==="bind"){let a=o.slice(1).join(" ").trim();if(!n){let y=be();return Je(y,e)?p("/c use is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}if(!a){let y=be();return Je(y,e)?p("Usage: /c use <label-or-id>"):null}e.clusterEnabled||yr(!0);let d=be().senderBindings[n]??null,{state:u,resolved:c}=na(n,a,"chat");if(!c.ok)return Je(u,e)?xg(a,c):null;let m=c.peer.nodeId===s,h=d?d.nodeId===s:!1;if(!m)return null;let f=[`*Bound to ${c.peer.label}*`,`id \`${c.peer.nodeId}\``,"","Your messages now route to this machine. Other senders are not affected.","",Or(u,e,n)].join(`
102
- `),g=[`<b>Bound to ${ke(c.peer.label)}</b>`,`id <code>${ke(c.peer.nodeId)}</code>`,"","Your messages now route to this machine. Other senders are not affected.","",Zi(u,e,n)].join(`
103
- `);return fe(f,g)}if(r==="here"||r==="take"){if(!n){let c=be();return Je(c,e)?p("/c here is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}e.clusterEnabled||yr(!0);let{state:a,resolved:l}=na(n,s,"chat");if(!l.ok)return Je(a,e)?p("Could not bind to this machine."):null;let d=["*Bound to this machine.*","","Your messages now route here. Other senders are not affected.","",Or(a,e,n)].join(`
104
- `),u=["<b>Bound to this machine.</b>","","Your messages now route here. Other senders are not affected.","",Zi(a,e,n)].join(`
105
- `);return fe(d,u)}if(r==="using"){if(!n){let d=be();return Je(d,e)?p("/c using is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}let a=It(e,n);if(a){if(a.nodeId!==s)return null;let c=[...be().peers,Mo(e)].find(f=>f.nodeId===a.nodeId)?.label??"(unknown label)",m=[`*Bound to ${c}*`,`id \`${a.nodeId}\` \xB7 source ${a.source}`,a.source==="chat"?`since ${a.sinceIso}`:"(from config)"].join(`
106
- `),h=[`<b>Bound to ${ke(c)}</b>`,`id <code>${ke(a.nodeId)}</code> \xB7 source ${ke(a.source)}`,a.source==="chat"?`since ${ke(a.sinceIso)}`:"(from config)"].join(`
107
- `);return fe(m,h)}let l=be();return Je(l,e)?p("You are not bound to any machine. Send /c use <label-or-id> to bind, or set a default in config.json (clusterSenderBindings)."):null}if(r==="unuse"||r==="unbind"||r==="release"){if(!n){let m=be();return Je(m,e)?p("/c unuse is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}let l=be().senderBindings[n]??null,{state:d}=kg(n);if(l){if(l.nodeId!==s)return null}else if(!Je(d,e))return null;let u=It(e,n),c=u?`
108
- Config default still applies: \`${u.nodeId}\`.`:`
109
- No config default is set; nobody will answer until you /c use <label-or-id>.`;return p(`Cleared your chat binding.${c}`)}if(r==="step-down"||r==="stepdown"){let a=be();return Je(a,e)?p("/c step-down is no longer used. The cluster is per-sender: send /c unuse to clear your binding."):null}if(r==="status"){let a=sa(e,n);return fe(a.wa,a.tg)}if(r==="list"){let a=be(),l=n?It(e,n):null;if(l){if(l.nodeId!==s)return null}else if(!Je(a,e))return null;return fe(Or(a,e,n??null),Zi(a,e,n??null))}let i=be();return Je(i,e)?p(`Unknown /c subcommand "${r}". Try /c help`):null}function ed(e,t){return yr(!0),na(e,t,"chat")}Xe();xe();function Cg(e){return`wa:${e}`}function ia(e){return`tg:${e}`}function td(e){return{peerKey:Cg(e.fromJid),text:e.text,waMessageId:e.messageId,mediaSavedPath:e.mediaSavedPath,mediaError:e.mediaError}}G();function Nr(e){let t=e.nodePath,n=e.scriptPath,o=e.omnishHome,r=["Linux (systemd --user), copy-paste on the host:","","mkdir -p ~/.config/systemd/user","# create ~/.config/systemd/user/omnish.service with ExecStart using:",`# ${t} ${n} run`,`# and Environment=OMNISH_HOME=${o}`,"# The generated unit uses Restart=on-failure and RestartSec=5 (tunable).","","systemctl --user daemon-reload","systemctl --user enable --now omnish.service","",'Optional (run without interactive login): loginctl enable-linger "$USER"',"","Full guide: docs/guides/background-and-boot.md \xB7 https://omnish.dev"].join(`
95
+ `)}function wa(e,t,n){let o=ut(ot()),r=["<b>Computers</b>",""],s=n?Nt(t,n):null;n&&(s?r.push(`Your binding: <code>${Se(s.nodeId)}</code> (${Se(s.source)})`):r.push("Your binding: (none \u2014 send /c use &lt;label-or-id&gt;)"),r.push(""));let i=new Map;i.set(o,Do(t));for(let l of e.peers)i.set(l.nodeId,l);let a=[...i.values()].sort((l,c)=>l.label.localeCompare(c.label));if(a.length===0)return r.push("(no peers observed yet \u2014 run /c status from this chat)"),r.join(`
96
+ `);for(let l of a){let c=l.nodeId===o,u=[(s?l.nodeId===s.nodeId:!1)?"your binding":null,c?"you":null].filter(Boolean).join(", "),m=u?` (${Se(u)})`:"";r.push(`\u2022 <b>${Se(l.label)}</b>${m}<br/> id <code>${Se(l.nodeId)}</code> \xB7 role ${Se(l.role)}<br/> seen ${Se(l.lastSeenIso)}`)}return r.push(""),r.push(`Updated: ${Se(e.updatedAt)}`),r.join(`
97
+ `)}function xa(e,t,n){return Jr(e,t,n??null).replace(/\*([^*]+)\*/g,"$1").replace(/`([^`]+)`/g,"$1")}function Ca(e,t){let n=ot(),o=ut(n),r=ve(),s=e.clusterRole,i=yd(e),a=t?Nt(e,t):null,l=a?a.nodeId===o:!1,c=t?a?`your binding: ${a.nodeId} (${a.source}${l?", here":""})`:"your binding: (none)":"",d=["*This computer*","",`label: ${i}`,`node id: \`${o}\` (full id in ${ka()})`,`host: ${ba.hostname()}`,`clusterRole: ${s}`,c,`peers known: ${r.peers.length}`,`senderBindings (chat): ${Object.keys(r.senderBindings).length}`].filter(Boolean).join(`
98
+ `),u=["<b>This computer</b>","",`label: ${Se(i)}`,`node id: <code>${Se(o)}</code> (full id in ${Se(ka())})`,`host: ${Se(ba.hostname())}`,`clusterRole: ${Se(s)}`,t?Se(c):"",`peers known: ${r.peers.length}`,`senderBindings (chat): ${Object.keys(r.senderBindings).length}`].filter(Boolean).join(`
99
+ `);return{wa:d,tg:u}}function hy(e){let t=e.clusterRole,n=["*Computers* (per-sender bindings)","","Each allowlisted phone picks one machine to talk to. Other senders are not affected.","Selection persists; defaults can be set in config (clusterSenderBindings).","","Shorthand: /pcs \u2026 \xB7 /c \u2026","",`${Ae}/c use <label-or-id> \u2014 bind your messages to that machine`,`${Ae}/c here \u2014 bind your messages to THIS machine`,`${Ae}/c using \u2014 show your current binding`,`${Ae}/c unuse \u2014 clear your chat-set binding (config default still applies, if any)`,`${Ae}/c status \u2014 every online host replies with its own paragraph`,`${Ae}/c list \u2014 locally known roster (single responder)`,`${Ae}/c help \u2014 this help`,"",`clusterRole on this host: ${t}`].join(`
100
+ `),o=["<b>Computers</b> (per-sender bindings)","","Each allowlisted phone picks one machine to talk to. Other senders are not affected.","Selection persists; defaults can be set in config (clusterSenderBindings).","","Shorthand: /pcs \u2026 \xB7 /c \u2026","","\u2022 /c use &lt;label-or-id&gt; \u2014 bind your messages to that machine","\u2022 /c here \u2014 bind your messages to THIS machine","\u2022 /c using \u2014 show your current binding","\u2022 /c unuse \u2014 clear your chat-set binding","\u2022 /c status \u2014 every online host replies with its own paragraph","\u2022 /c list \u2014 locally known roster (single responder)","\u2022 /c help \u2014 this help","",`clusterRole on this host: ${Se(t)}`].join(`
101
+ `);return we(n,o)}function fy(e,t){if(t.ok)return p("");if(t.reason==="ambiguous-label"){let n=(t.matches??[]).map(o=>`\`${o.nodeId}\` (${o.label})`).join(", ");return p(`Label "${e}" matches multiple machines: ${n}. Use the 8-character id with /c use <id>.`)}return p(`No machine matches "${e}". Send /c list to see ids and labels, or /c status to refresh the roster.`)}function Sd(e,t,n){let o=t.trim().split(/\s+/),r=o[0]?.toLowerCase()??"";if(!r||r==="help"){if(!e.clusterEnabled&&r!=="help"){let l=ve();return qe(l,e)?p("Cluster is disabled. Enable with: /config set clusterEnabled true (then /c use <label-or-id>). /c help for the new commands."):null}let a=ve();return qe(a,e)?hy(e):null}let s=ut(ot());if(r==="use"||r==="bind"){let a=o.slice(1).join(" ").trim();if(!n){let y=ve();return qe(y,e)?p("/c use is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}if(!a){let y=ve();return qe(y,e)?p("Usage: /c use <label-or-id>"):null}e.clusterEnabled||Mr(!0);let c=ve().senderBindings[n]??null,{state:d,resolved:u}=va(n,a,"chat");if(!u.ok)return qe(d,e)?fy(a,u):null;let m=u.peer.nodeId===s,h=c?c.nodeId===s:!1;if(!m)return null;let f=[`*Bound to ${u.peer.label}*`,`id \`${u.peer.nodeId}\``,"","Your messages now route to this machine. Other senders are not affected.","",Jr(d,e,n)].join(`
102
+ `),g=[`<b>Bound to ${Se(u.peer.label)}</b>`,`id <code>${Se(u.peer.nodeId)}</code>`,"","Your messages now route to this machine. Other senders are not affected.","",wa(d,e,n)].join(`
103
+ `);return we(f,g)}if(r==="here"||r==="take"){if(!n){let u=ve();return qe(u,e)?p("/c here is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}e.clusterEnabled||Mr(!0);let{state:a,resolved:l}=va(n,s,"chat");if(!l.ok)return qe(a,e)?p("Could not bind to this machine."):null;let c=["*Bound to this machine.*","","Your messages now route here. Other senders are not affected.","",Jr(a,e,n)].join(`
104
+ `),d=["<b>Bound to this machine.</b>","","Your messages now route here. Other senders are not affected.","",wa(a,e,n)].join(`
105
+ `);return we(c,d)}if(r==="using"){if(!n){let c=ve();return qe(c,e)?p("/c using is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}let a=Nt(e,n);if(a){if(a.nodeId!==s)return null;let u=[...ve().peers,Do(e)].find(f=>f.nodeId===a.nodeId)?.label??"(unknown label)",m=[`*Bound to ${u}*`,`id \`${a.nodeId}\` \xB7 source ${a.source}`,a.source==="chat"?`since ${a.sinceIso}`:"(from config)"].join(`
106
+ `),h=[`<b>Bound to ${Se(u)}</b>`,`id <code>${Se(a.nodeId)}</code> \xB7 source ${Se(a.source)}`,a.source==="chat"?`since ${Se(a.sinceIso)}`:"(from config)"].join(`
107
+ `);return we(m,h)}let l=ve();return qe(l,e)?p("You are not bound to any machine. Send /c use <label-or-id> to bind, or set a default in config.json (clusterSenderBindings)."):null}if(r==="unuse"||r==="unbind"||r==="release"){if(!n){let m=ve();return qe(m,e)?p("/c unuse is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}let l=ve().senderBindings[n]??null,{state:c}=py(n);if(l){if(l.nodeId!==s)return null}else if(!qe(c,e))return null;let d=Nt(e,n),u=d?`
108
+ Config default still applies: \`${d.nodeId}\`.`:`
109
+ No config default is set; nobody will answer until you /c use <label-or-id>.`;return p(`Cleared your chat binding.${u}`)}if(r==="step-down"||r==="stepdown"){let a=ve();return qe(a,e)?p("/c step-down is no longer used. The cluster is per-sender: send /c unuse to clear your binding."):null}if(r==="status"){let a=Ca(e,n);return we(a.wa,a.tg)}if(r==="list"){let a=ve(),l=n?Nt(e,n):null;if(l){if(l.nodeId!==s)return null}else if(!qe(a,e))return null;return we(Jr(a,e,n??null),wa(a,e,n??null))}let i=ve();return qe(i,e)?p(`Unknown /c subcommand "${r}". Try /c help`):null}function xd(e,t){return Mr(!0),va(e,t,"chat")}Ze();Ce();function gy(e){return`wa:${e}`}function Ra(e){return`tg:${e}`}function Cd(e){return{peerKey:gy(e.fromJid),text:e.text,waMessageId:e.messageId,mediaSavedPath:e.mediaSavedPath,mediaError:e.mediaError}}q();function qr(e){let t=e.nodePath,n=e.scriptPath,o=e.omnishHome,r=["Linux (systemd --user), copy-paste on the host:","","mkdir -p ~/.config/systemd/user","# create ~/.config/systemd/user/omnish.service with ExecStart using:",`# ${t} ${n} run`,`# and Environment=OMNISH_HOME=${o}`,"# The generated unit uses Restart=on-failure and RestartSec=5 (tunable).","","systemctl --user daemon-reload","systemctl --user enable --now omnish.service","",'Optional (run without interactive login): loginctl enable-linger "$USER"',"","Full guide: docs/guides/background-and-boot.md \xB7 https://omnish.dev"].join(`
110
110
  `),s=["macOS (LaunchAgent), copy-paste on the host:","","# Use contrib/dev.omnish.gateway.plist from the repo with paths filled as:",`# Node: ${t}`,`# Script: ${n}`,`# OMNISH_HOME: ${o}`,"# Generated plist uses KeepAlive for crash restart.","","cp \u2026/dev.omnish.gateway.plist ~/Library/LaunchAgents/","launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.omnish.gateway.plist","launchctl kickstart -k gui/$(id -u)/dev.omnish.gateway","","https://omnish.dev"].join(`
111
111
  `),i=["Windows: Task Scheduler \u2014 Action = Start a program:","",`Program: ${t}`,`Arguments: "${n}" run`,"","Set user env OMNISH_HOME if needed. Optional XML: contrib/omnish-windows-task.xml","","https://omnish.dev"].join(`
112
112
  `);return process.platform==="linux"?r:process.platform==="darwin"?s:process.platform==="win32"?i:[r,"","---","",s].join(`
113
- `)}pe();import Lt from"node:process";G();function Fr(){let e=Pn,t=at,n=["Linux (package manager or omnish pull install):",""," omnish pull install"," omnish pull install --whisper # needs python3 + venv","","Or manually:"," pipx install yt-dlp # or: curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o ~/.local/bin/yt-dlp && chmod +x ~/.local/bin/yt-dlp"," sudo apt install ffmpeg # or dnf install ffmpeg",` python3 -m venv ${t}`,` ${t}/bin/pip install -U openai-whisper`,"",`Bundled path (after install): ${e}`,"Docs: docs/features/media-pull.md"].join(`
113
+ `)}pe();import mt from"node:process";q();function zr(){let e=Un,t=at,n=["Linux (package manager or omnish pull install):",""," omnish pull install"," omnish pull install --whisper # needs python3 + venv","","Or manually:"," pipx install yt-dlp # or: curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o ~/.local/bin/yt-dlp && chmod +x ~/.local/bin/yt-dlp"," sudo apt install ffmpeg # or dnf install ffmpeg",` python3 -m venv ${t}`,` ${t}/bin/pip install -U openai-whisper`,"",`Bundled path (after install): ${e}`,"Docs: docs/features/media-commands.md"].join(`
114
114
  `),o=["macOS:",""," omnish pull install"," omnish pull install --whisper","","Or: brew install yt-dlp ffmpeg python",` python3 -m venv ${t} && ${t}/bin/pip install -U openai-whisper`,"",`Bundled path: ${e}`].join(`
115
115
  `),r=["Windows:",""," omnish pull install"," omnish pull install --whisper","","Or: winget install yt-dlp \xB7 winget install Gyan.FFmpeg"," py -3 -m venv %USERPROFILE%\\.omnish\\venvs\\whisper"," %USERPROFILE%\\.omnish\\venvs\\whisper\\Scripts\\pip install -U openai-whisper","",`Bundled path: ${e}`].join(`
116
116
  `);return process.platform==="linux"?n:process.platform==="darwin"?o:process.platform==="win32"?r:[n,"","---","",o,"","---","",r].join(`
117
- `)}pe();G();import ld from"node:fs";import Lo from"node:path";import{spawn as Rg}from"node:child_process";import _r from"node:fs";import Po from"node:path";function nd(e){return Po.join(e,"%(title).200B.%(ext)s")}function aa(e,t,n){return new Promise((o,r)=>{let s=Rg(e,t,{windowsHide:!0}),i="",a="";s.stdout?.on("data",d=>{i+=String(d)}),s.stderr?.on("data",d=>{a+=String(d)});let l=setTimeout(()=>{s.kill("SIGTERM"),r(new Error(`yt-dlp timed out after ${n}ms`))},n);s.on("error",d=>{clearTimeout(l),r(d)}),s.on("close",d=>{clearTimeout(l),o({code:d,stdout:i,stderr:a})})})}function od(e,t){let n=[],o=r=>{let s;try{s=_r.readdirSync(r,{withFileTypes:!0})}catch{return}for(let i of s){let a=Po.join(r,i.name);if(i.isDirectory())o(a);else if(i.isFile())try{_r.statSync(a).mtimeMs>=t-2e3&&n.push(a)}catch{}}};return o(e),n.sort()}async function Tg(e,t){let n=await aa(e,["--no-download","--print","title",t],6e4);if(n.code===0)return n.stdout.trim().split(`
118
- `)[0]?.trim()||void 0}async function la(e){let{cfg:t,tools:n,url:o,mode:r,outputDir:s}=e,i=n.ytDlp;if(!i)throw new Error("yt-dlp not found. Run: omnish pull install");_r.mkdirSync(s,{recursive:!0});let a=Date.now(),l=nd(s),d=Math.min(9e5,Math.max(6e4,t.syncTimeoutMs*3)),u=await Tg(i,o),c=[],m=[],h=[];t.pullMaxBytes>0&&h.push("--max-filesize",String(Math.floor(t.pullMaxBytes)));let f=async b=>{let k=await aa(i,b,d);if(m.push(k.stderr.trim()||k.stdout.trim()),k.code!==0)throw new Error(`yt-dlp failed (exit ${k.code}):
119
- ${(k.stderr||k.stdout).slice(-2e3)}`)},g=r==="all"?["video","audio","subs"]:[r];for(let b of g){let k=Date.now();if(b==="video"){if(!n.ffmpeg)throw new Error("ffmpeg required for video. Run: omnish pull install");await f(["-f","bv*+ba/b","--merge-output-format","mp4","--ffmpeg-location",Po.dirname(n.ffmpeg),...h,"-o",l,o])}else if(b==="audio"){let M=n.ffmpeg?["--ffmpeg-location",Po.dirname(n.ffmpeg)]:[];await f(["-x","--audio-format","m4a",...M,...h,"-o",l,o])}else b==="subs"&&await f(["--skip-download","--write-subs","--write-auto-subs","--sub-langs","en.*,en","-o",l,o]);c.push(...od(s,k))}let y=[...new Set(c)];return{title:u,outputDir:s,files:y,log:m.join(`
117
+ `)}pe();import Xy from"node:path";q();import yy from"node:net";import wy from"node:fs";function by(){try{let e=wy.readFileSync(Dn,"utf8"),t=JSON.parse(e);return typeof t.host!="string"||typeof t.port!="number"||typeof t.token!="string"||!Number.isFinite(t.port)?null:{host:t.host,port:t.port,token:t.token}}catch{return null}}async function Sn(e){let t=by();if(!t)return"No gateway control endpoint \u2014 is `omnish run` active? (control metadata missing.)";let n={...e,token:t.token},o=`${JSON.stringify(n)}
118
+ `;return new Promise(r=>{let s=!1,i="";function a(c){s||(s=!0,r(c))}let l=yy.connect({host:t.host,port:t.port},()=>{l.write(o)});l.setTimeout(6e5),l.on("data",c=>{i+=c.toString("utf8");let d=i.indexOf(`
119
+ `);if(d>=0){let u=i.slice(0,d).trim();try{let m=JSON.parse(u);m.ok?a(null):a(m.error||"Unknown error from gateway control.")}catch{a("Invalid response from gateway control.")}l.destroy()}}),l.on("error",c=>{a(`Control connection failed: ${String(c)}`)}),l.on("timeout",()=>{l.destroy(),a("Gateway control timed out.")}),l.on("close",()=>{!s&&i.trim()===""&&a("Gateway closed the control connection without a response.")})})}function Rd(e,t,n){return`Step ${e+1}/${t}: ${n}\u2026`}function nn(){return{async stepStart(){},async stepFail(){}}}async function Td(e,t){let n=e.peerKey?.trim();if(n){if(e.sendToPeer){try{await e.sendToPeer(n,t)}catch{}return}Sn({op:"sendPeerText",peerKey:n,text:t}).catch(()=>{})}}function Kr(e){return!e.enabled||!e.peerKey?.trim()?nn():{async stepStart(t,n,o){await Td(e,Rd(t,n,o.label))},async stepFail(t){await Td(e,`Step failed: ${String(t)}`)}}}function $d(){return process.env.OMNISH_PEER_KEY?.trim()||null}import{spawn as ky}from"node:child_process";import Yr from"node:fs";import Uo from"node:path";function Md(e){return Uo.join(e,"%(title).200B.%(ext)s")}function Ta(e,t,n){return new Promise((o,r)=>{let s=ky(e,t,{windowsHide:!0}),i="",a="";s.stdout?.on("data",c=>{i+=String(c)}),s.stderr?.on("data",c=>{a+=String(c)});let l=setTimeout(()=>{s.kill("SIGTERM"),r(new Error(`yt-dlp timed out after ${n}ms`))},n);s.on("error",c=>{clearTimeout(l),r(c)}),s.on("close",c=>{clearTimeout(l),o({code:c,stdout:i,stderr:a})})})}function Pd(e,t){let n=[],o=r=>{let s;try{s=Yr.readdirSync(r,{withFileTypes:!0})}catch{return}for(let i of s){let a=Uo.join(r,i.name);if(i.isDirectory())o(a);else if(i.isFile())try{Yr.statSync(a).mtimeMs>=t-2e3&&n.push(a)}catch{}}};return o(e),n.sort()}async function vy(e,t){let n=await Ta(e,["--no-download","--print","title",t],6e4);if(n.code===0)return n.stdout.trim().split(`
120
+ `)[0]?.trim()||void 0}async function Ft(e){let{cfg:t,tools:n,url:o,mode:r,outputDir:s}=e,i=n.ytDlp;if(!i)throw new Error("yt-dlp not found. Run: omnish pull install");Yr.mkdirSync(s,{recursive:!0});let a=Date.now(),l=Md(s),c=Math.min(9e5,Math.max(6e4,t.syncTimeoutMs*3)),d=await vy(i,o),u=[],m=[],h=[];t.mediaMaxBytes>0&&h.push("--max-filesize",String(Math.floor(t.mediaMaxBytes)));let f=async b=>{let k=await Ta(i,b,c);if(m.push(k.stderr.trim()||k.stdout.trim()),k.code!==0)throw new Error(`yt-dlp failed (exit ${k.code}):
121
+ ${(k.stderr||k.stdout).slice(-2e3)}`)},g=r==="all"?["video","audio","subs"]:[r];for(let b of g){let k=Date.now();if(b==="video"){if(!n.ffmpeg)throw new Error("ffmpeg required for video. Run: omnish pull install");await f(["-f","bv*+ba/b","--merge-output-format","mp4","--ffmpeg-location",Uo.dirname(n.ffmpeg),...h,"-o",l,o])}else if(b==="audio"){let R=n.ffmpeg?["--ffmpeg-location",Uo.dirname(n.ffmpeg)]:[];await f(["-x","--audio-format","m4a",...R,...h,"-o",l,o])}else b==="subs"&&await f(["--skip-download","--write-subs","--write-auto-subs","--sub-langs","en.*,en","-o",l,o]);u.push(...Pd(s,k))}let y=[...new Set(u)];return{title:d,outputDir:s,files:y,log:m.join(`
120
122
  ---
121
- `)}}async function rd(e){let{tools:t,url:n,outputDir:o,timeoutMs:r}=e,s=t.ytDlp;if(!s)throw new Error("yt-dlp not found.");_r.mkdirSync(o,{recursive:!0});let i=nd(o),a=Date.now(),l=t.ffmpeg?["--ffmpeg-location",Po.dirname(t.ffmpeg)]:[],d=await aa(s,["-f","ba/b","-x","--audio-format","m4a",...l,"-o",i,n],r);if(d.code!==0)throw new Error(`yt-dlp audio failed: ${(d.stderr||d.stdout).slice(-1500)}`);let u=od(o,a).filter(c=>/\.(m4a|mp3|opus|webm|wav)$/i.test(c));if(u.length===0)throw new Error("No audio file produced.");return u[0]}import{spawnSync as $g}from"node:child_process";import Mg from"node:fs";import ca from"node:path";async function ua(e){let{cfg:t,tools:n,url:o,outputDir:r}=e,s=n.whisper;if(!s)throw new Error("whisper not found. Run: omnish pull install --whisper");let i=Math.min(9e5,Math.max(12e4,t.syncTimeoutMs*4)),a=await rd({tools:n,url:o,outputDir:r,timeoutMs:i}),l=t.pullWhisperModel.trim()||"small",d=$g(s,[a,"--model",l,"--output_dir",r,"--output_format","all"],{encoding:"utf8",timeout:i,windowsHide:!0});if(d.status!==0)throw new Error(`whisper failed:
122
- ${(d.stderr||d.stdout).slice(-2e3)}`);let u=ca.basename(a,ca.extname(a));return{files:[".txt",".srt",".vtt",".json"].map(h=>ca.join(r,u+h)).filter(h=>Mg.existsSync(h)),log:d.stderr||d.stdout||""}}G();import{spawnSync as ma}from"node:child_process";import ge from"node:fs";import Pg from"node:os";import ze from"node:path";var Eg="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp",Ag="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe";function id(){return process.platform==="win32"?"yt-dlp.exe":"yt-dlp"}function ad(){return process.platform==="win32"?"ffmpeg.exe":"ffmpeg"}function da(){return process.platform==="win32"?"whisper.exe":"whisper"}function Hn(e){return ze.join(Pn,e)}function pa(){return process.platform==="win32"?ze.join(at,"Scripts","whisper.exe"):ze.join(at,"bin","whisper")}function fn(e){try{return!ge.existsSync(e)||!ge.statSync(e).isFile()?!1:(process.platform==="win32"||ge.accessSync(e,ge.constants.X_OK),!0)}catch{return!1}}function Bn(e){let t=process.platform==="win32"?"where":"which",n=ma(t,[e],{encoding:"utf8",timeout:8e3,windowsHide:!0});if(n.status!==0||!n.stdout?.trim())return null;let o=n.stdout.trim().split(/\r?\n/)[0]?.trim();return o&&fn(o)?o:null}function sd(e,t,n){let o=e.trim();if(o&&fn(o))return o;let r=Hn(t);if(fn(r))return r;for(let s of n){let i=Bn(s);if(i)return i}return null}function Ao(e){let t=sd(e.pullYtDlpPath,id(),["yt-dlp","yt-dlp.exe"]),n=sd(e.pullFfmpegPath,ad(),["ffmpeg","ffmpeg.exe"]),o=e.pullWhisperPath.trim(),r=null;o&&fn(o)?r=o:fn(Hn(da()))?r=Hn(da()):fn(pa())?r=pa():r=Bn(process.platform==="win32"?"whisper.exe":"whisper");let s=process.platform==="win32"?Bn("python")??Bn("py"):Bn("python3")??Bn("python");return{ytDlp:t,ffmpeg:n,whisper:r,python:s}}function Dr(e){let t=Ao(e),n=[{name:"yt-dlp",ok:!!t.ytDlp,detail:t.ytDlp??"missing \u2014 run: omnish pull install"},{name:"ffmpeg",ok:!!t.ffmpeg,detail:t.ffmpeg??"missing \u2014 run: omnish pull install (needed for video merge / audio extract)"},{name:"python",ok:!!t.python,detail:t.python??"missing \u2014 required for Whisper (omnish pull install --whisper)"},{name:"whisper",ok:!!t.whisper,detail:t.whisper??"missing \u2014 optional: omnish pull install --whisper"},{name:"pullEnabled",ok:e.pullEnabled,detail:e.pullEnabled?"on":"off \u2014 /config set pullEnabled true"},{name:"data dir",ok:!0,detail:W}],o=n.map(r=>`${r.ok?"\u2713":"\u2717"} ${r.name}: ${r.detail}`).join(`
123
- `);return{lines:n,text:o}}async function Wr(e,t){B(ze.dirname(t));let n=await fetch(e,{redirect:"follow"});if(!n.ok)throw new Error(`Download failed (${n.status}): ${e}`);let o=Buffer.from(await n.arrayBuffer());ge.writeFileSync(t,o),process.platform!=="win32"&&ge.chmodSync(t,493)}function Eo(e,t,n={}){if(ma(e,t,{stdio:"inherit",cwd:n.cwd,timeout:6e5,windowsHide:!0}).status!==0)throw new Error(`Command failed: ${e} ${t.join(" ")}`)}async function Ig(){let e=Hn(id()),t=process.platform==="win32"?Ag:Eg;return await Wr(t,e),e}async function Lg(){let e=Hn(ad());B(Pn);let t=ze.join(W,"tmp","pull-install");if(B(t),process.platform==="win32"){let l="https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip",d=ze.join(t,"ffmpeg.zip");await Wr(l,d),Eo("powershell",["-NoProfile","-Command",`Expand-Archive -Force -Path '${d.replace(/'/g,"''")}' -DestinationPath '${t.replace(/'/g,"''")}'`],{cwd:t});let c=ge.readdirSync(t,{withFileTypes:!0}).find(h=>h.isDirectory()&&h.name.startsWith("ffmpeg"))?.name;if(!c)throw new Error("Could not find ffmpeg folder in archive.");let m=ze.join(t,c,"bin","ffmpeg.exe");if(!ge.existsSync(m))throw new Error("ffmpeg.exe not found in archive.");return ge.copyFileSync(m,e),e}if(process.platform==="darwin"){let l="https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip",d=ze.join(t,"ffmpeg.zip");await Wr(l,d),Eo("unzip",["-o",d,"-d",t],{cwd:t});let u=ze.join(t,"ffmpeg");if(!ge.existsSync(u))throw new Error("ffmpeg binary not found after unzip.");return ge.copyFileSync(u,e),ge.chmodSync(e,493),e}let n=Pg.arch();if(n!=="x64"&&n!=="amd64")throw new Error(`Automatic ffmpeg install is not supported on linux/${n}. Install ffmpeg via your package manager and ensure it is on PATH.`);let o="https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz",r=ze.join(t,"ffmpeg.tar.xz");await Wr(o,r),Eo("tar",["-xf",r,"-C",t],{cwd:t});let i=ge.readdirSync(t).filter(l=>l.startsWith("ffmpeg"))[0];if(!i)throw new Error("Could not find ffmpeg directory in tarball.");let a=ze.join(t,i,"ffmpeg");if(!ge.existsSync(a))throw new Error("ffmpeg binary not found in tarball.");return ge.copyFileSync(a,e),ge.chmodSync(e,493),e}function Og(){let e=process.platform==="win32"?[{cmd:"py",args:["-3"]},{cmd:"python",args:[]},{cmd:"python3",args:[]}]:[{cmd:"python3",args:[]},{cmd:"python",args:[]}];for(let t of e)if(ma(t.cmd,[...t.args,"--version"],{encoding:"utf8",timeout:15e3,windowsHide:!0}).status===0)return t;throw new Error("Python 3 not found. Install Python 3.10+ then run: omnish pull install --whisper")}async function Ng(){B(ze.dirname(at));let e=Og();if(ge.existsSync(at))try{ge.rmSync(at,{recursive:!0,force:!0})}catch{}Eo(e.cmd,[...e.args,"-m","venv",at]);let t=process.platform==="win32"?ze.join(at,"Scripts","pip.exe"):ze.join(at,"bin","pip");Eo(t,["install","-U","pip","openai-whisper"]);let n=pa();if(!fn(n))throw new Error("Whisper install finished but whisper executable was not found.");let o=Hn(da());try{ge.existsSync(o)&&ge.unlinkSync(o),ge.symlinkSync(n,o)}catch{ge.copyFileSync(n,o),process.platform!=="win32"&&ge.chmodSync(o,493)}return n}async function Ur(e){let t=[],n=await Ig();t.push(`yt-dlp \u2192 ${n}`);let o;try{o=await Lg(),t.push(`ffmpeg \u2192 ${o}`)}catch(s){t.push(`ffmpeg: ${String(s)} (install manually; video merge needs it)`)}let r;return e.whisper&&(r=await Ng(),t.push(`whisper \u2192 ${r}`)),{ytDlp:n,ffmpeg:o,whisper:r,messages:t}}var Fg=/^https?:\/\/\S+$/i;function ha(e){let t=e.trim();if(!Fg.test(t))return!1;try{return Io(t),!0}catch{return!1}}function Io(e){let t;try{t=new URL(e.trim())}catch{throw new Error("Invalid URL.")}if(t.protocol!=="http:"&&t.protocol!=="https:")throw new Error("Only http:// and https:// URLs are supported.");let n=t.hostname.toLowerCase();if(n==="localhost"||n.endsWith(".localhost")||n==="127.0.0.1"||n==="::1"||n==="0.0.0.0"||n.startsWith("127.")||n.startsWith("10.")||n.startsWith("192.168.")||/^172\.(1[6-9]|2\d|3[01])\./.test(n)||n.startsWith("169.254.")||n==="[::1]")throw new Error("Private or local URLs are not allowed.");return t}function _g(e,t){let n=e.pullOutputDir.trim(),o=new Date().toISOString().slice(0,10),r;n?r=Lo.isAbsolute(n)?n:Lo.resolve(t,n):r=Lo.join(t,".omnish-pull");let s=Lo.join(r,o);return ld.mkdirSync(s,{recursive:!0}),s}async function fa(e){Io(e.url);let t=Ao(e.cfg),n=_g(e.cfg,e.sessionCwd),o=[],r;if(e.mode==="transcript"){let a=await ua({cfg:e.cfg,tools:t,url:e.url,outputDir:n});o.push(...a.files)}else if(e.mode==="all"){let a=await la({cfg:e.cfg,tools:t,url:e.url,mode:"all",outputDir:n});r=a.title,o.push(...a.files);try{let l=await ua({cfg:e.cfg,tools:t,url:e.url,outputDir:n});o.push(...l.files)}catch{}}else{let a=await la({cfg:e.cfg,tools:t,url:e.url,mode:e.mode,outputDir:n});r=a.title,o.push(...a.files)}let s=o.filter(a=>typeof a=="string"&&ld.existsSync(a)),i=[r?`Title: ${r}`:"",`Saved under: ${n}`,s.length?"Files:":"(no files listed)",...s.map(a=>` ${a}`)].filter(Boolean);return{title:r,outputDir:n,files:s,summary:i.join(`
124
- `)}}async function cd(e){let[t,n,o]=e,r=t??"audio",s=S(),i=o?.trim()?Lo.resolve(o):gi,a=await fa({cfg:s,mode:r,url:n??"",sessionCwd:i});process.stdout.write(a.summary+`
125
- `)}function ga(){let e=Lt.stdout;console.log(Ce(e,"omnish pull"),w(e,"\u2014 download media from URLs (yt-dlp + ffmpeg + optional Whisper)")),console.log(""),console.log(q(e,"Usage:")),console.log(` ${v(e,"omnish pull doctor")}`),console.log(` ${v(e,"omnish pull install")} ${w(e,"[--whisper]")}`),console.log(` ${v(e,"omnish pull instructions")}`),console.log(` ${v(e,"omnish pull-exec <mode> <url> <cwd>")} ${Q(e,"(internal / background)")}`),console.log(""),console.log(w(e,"Chat: /pull help \u2014 enable pullEnabled in config.json first.")),console.log(Q(e,"Docs: docs/features/media-pull.md"))}async function ud(e){let t=(e[0]??"").toLowerCase(),n=e.slice(1);if(!t||t==="help"||t==="-h"||t==="--help"){ga();return}if(t==="doctor"){let o=S(),{text:r}=Dr(o);console.log(D(Lt.stdout,r));return}if(t==="instructions"||t==="setup"){console.log(Fr());return}if(t==="install"){let o=n.some(s=>s==="--whisper"),r=Lt.stdout;console.log(D(r,"Installing pull tools into ~/.omnish/bin \u2026"));try{let s=await Ur({whisper:o});for(let i of s.messages)console.log(D(r,i))}catch(s){console.error(T(Lt.stderr,String(s))),Lt.exitCode=1}return}console.error(T(Lt.stderr,`Unknown subcommand "${t}". Try: omnish pull help`)),Lt.exitCode=1}async function dd(e){try{await cd(e)}catch(t){console.error(T(Lt.stderr,String(t))),Lt.exitCode=1}}G();import{execFileSync as Xt}from"node:child_process";import jn from"node:fs";import No from"node:os";import dt from"node:path";import He from"node:process";function Ot(){let e=He.execPath,t=He.argv[1];if(!t||!t.trim())return{nodePath:e,scriptPath:"",omnishHome:W,error:"Cannot resolve gateway entry script (missing argv[1]). Run omnish from its CLI entry."};let n=dt.resolve(t),o=He.env.OMNISH_HOME?.trim(),r=o?dt.resolve(o):W;return{nodePath:e,scriptPath:n,omnishHome:r}}function pd(e){return/[ "'\\\s]/.test(e)?`"${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`:e}function Wg(e){return`Environment="OMNISH_HOME=${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}function Dg(e){return`[Unit]
123
+ `)}}async function Ed(e){let{tools:t,url:n,outputDir:o,timeoutMs:r}=e,s=t.ytDlp;if(!s)throw new Error("yt-dlp not found.");Yr.mkdirSync(o,{recursive:!0});let i=Md(o),a=Date.now(),l=t.ffmpeg?["--ffmpeg-location",Uo.dirname(t.ffmpeg)]:[],c=await Ta(s,["-f","ba/b","-x","--audio-format","m4a",...l,"-o",i,n],r);if(c.code!==0)throw new Error(`yt-dlp audio failed: ${(c.stderr||c.stdout).slice(-1500)}`);let d=Pd(o,a).filter(u=>/\.(m4a|mp3|opus|webm|wav)$/i.test(u));if(d.length===0)throw new Error("No audio file produced.");return d[0]}q();import{spawnSync as Pa}from"node:child_process";import be from"node:fs";import Sy from"node:os";import ze from"node:path";var xy="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp",Cy="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe";function Id(){return process.platform==="win32"?"yt-dlp.exe":"yt-dlp"}function Ld(){return process.platform==="win32"?"ffmpeg.exe":"ffmpeg"}function $a(){return process.platform==="win32"?"whisper.exe":"whisper"}function Zn(e){return ze.join(Un,e)}function Ma(){return process.platform==="win32"?ze.join(at,"Scripts","whisper.exe"):ze.join(at,"bin","whisper")}function xn(e){try{return!be.existsSync(e)||!be.statSync(e).isFile()?!1:(process.platform==="win32"||be.accessSync(e,be.constants.X_OK),!0)}catch{return!1}}function Xn(e){let t=process.platform==="win32"?"where":"which",n=Pa(t,[e],{encoding:"utf8",timeout:8e3,windowsHide:!0});if(n.status!==0||!n.stdout?.trim())return null;let o=n.stdout.trim().split(/\r?\n/)[0]?.trim();return o&&xn(o)?o:null}function Ad(e,t,n){let o=e.trim();if(o&&xn(o))return o;let r=Zn(t);if(xn(r))return r;for(let s of n){let i=Xn(s);if(i)return i}return null}function dt(e){let t=Ad(e.pullYtDlpPath,Id(),["yt-dlp","yt-dlp.exe"]),n=Ad(e.pullFfmpegPath,Ld(),["ffmpeg","ffmpeg.exe"]),o=e.pullWhisperPath.trim(),r=null;o&&xn(o)?r=o:xn(Zn($a()))?r=Zn($a()):xn(Ma())?r=Ma():r=Xn(process.platform==="win32"?"whisper.exe":"whisper");let s=process.platform==="win32"?Xn("python")??Xn("py"):Xn("python3")??Xn("python");return{ytDlp:t,ffmpeg:n,whisper:r,python:s}}function Vr(e){let t=dt(e),n=[{name:"yt-dlp",ok:!!t.ytDlp,detail:t.ytDlp??"missing \u2014 run: omnish pull install"},{name:"ffmpeg",ok:!!t.ffmpeg,detail:t.ffmpeg??"missing \u2014 run: omnish pull install (needed for video merge / audio extract)"},{name:"python",ok:!!t.python,detail:t.python??"missing \u2014 required for Whisper (omnish pull install --whisper)"},{name:"whisper",ok:!!t.whisper,detail:t.whisper??"missing \u2014 optional: omnish pull install --whisper"},{name:"mediaSendFiles",ok:e.mediaSendFiles,detail:e.mediaSendFiles?"on (files sent to chat)":"off \u2014 paths only; /config set mediaSendFiles true"},{name:"data dir",ok:!0,detail:D}],o=n.map(r=>`${r.ok?"\u2713":"\u2717"} ${r.name}: ${r.detail}`).join(`
124
+ `);return{lines:n,text:o}}async function Qr(e,t){H(ze.dirname(t));let n=await fetch(e,{redirect:"follow"});if(!n.ok)throw new Error(`Download failed (${n.status}): ${e}`);let o=Buffer.from(await n.arrayBuffer());be.writeFileSync(t,o),process.platform!=="win32"&&be.chmodSync(t,493)}function Bo(e,t,n={}){if(Pa(e,t,{stdio:"inherit",cwd:n.cwd,timeout:6e5,windowsHide:!0}).status!==0)throw new Error(`Command failed: ${e} ${t.join(" ")}`)}async function Ry(){let e=Zn(Id()),t=process.platform==="win32"?Cy:xy;return await Qr(t,e),e}async function Ty(){let e=Zn(Ld());H(Un);let t=ze.join(D,"tmp","pull-install");if(H(t),process.platform==="win32"){let l="https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip",c=ze.join(t,"ffmpeg.zip");await Qr(l,c),Bo("powershell",["-NoProfile","-Command",`Expand-Archive -Force -Path '${c.replace(/'/g,"''")}' -DestinationPath '${t.replace(/'/g,"''")}'`],{cwd:t});let u=be.readdirSync(t,{withFileTypes:!0}).find(h=>h.isDirectory()&&h.name.startsWith("ffmpeg"))?.name;if(!u)throw new Error("Could not find ffmpeg folder in archive.");let m=ze.join(t,u,"bin","ffmpeg.exe");if(!be.existsSync(m))throw new Error("ffmpeg.exe not found in archive.");return be.copyFileSync(m,e),e}if(process.platform==="darwin"){let l="https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip",c=ze.join(t,"ffmpeg.zip");await Qr(l,c),Bo("unzip",["-o",c,"-d",t],{cwd:t});let d=ze.join(t,"ffmpeg");if(!be.existsSync(d))throw new Error("ffmpeg binary not found after unzip.");return be.copyFileSync(d,e),be.chmodSync(e,493),e}let n=Sy.arch();if(n!=="x64"&&n!=="amd64")throw new Error(`Automatic ffmpeg install is not supported on linux/${n}. Install ffmpeg via your package manager and ensure it is on PATH.`);let o="https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz",r=ze.join(t,"ffmpeg.tar.xz");await Qr(o,r),Bo("tar",["-xf",r,"-C",t],{cwd:t});let i=be.readdirSync(t).filter(l=>l.startsWith("ffmpeg"))[0];if(!i)throw new Error("Could not find ffmpeg directory in tarball.");let a=ze.join(t,i,"ffmpeg");if(!be.existsSync(a))throw new Error("ffmpeg binary not found in tarball.");return be.copyFileSync(a,e),be.chmodSync(e,493),e}function $y(){let e=process.platform==="win32"?[{cmd:"py",args:["-3"]},{cmd:"python",args:[]},{cmd:"python3",args:[]}]:[{cmd:"python3",args:[]},{cmd:"python",args:[]}];for(let t of e)if(Pa(t.cmd,[...t.args,"--version"],{encoding:"utf8",timeout:15e3,windowsHide:!0}).status===0)return t;throw new Error("Python 3 not found. Install Python 3.10+ then run: omnish pull install --whisper")}async function My(){H(ze.dirname(at));let e=$y();if(be.existsSync(at))try{be.rmSync(at,{recursive:!0,force:!0})}catch{}Bo(e.cmd,[...e.args,"-m","venv",at]);let t=process.platform==="win32"?ze.join(at,"Scripts","pip.exe"):ze.join(at,"bin","pip");Bo(t,["install","-U","pip","openai-whisper"]);let n=Ma();if(!xn(n))throw new Error("Whisper install finished but whisper executable was not found.");let o=Zn($a());try{be.existsSync(o)&&be.unlinkSync(o),be.symlinkSync(n,o)}catch{be.copyFileSync(n,o),process.platform!=="win32"&&be.chmodSync(o,493)}return n}async function Xr(e){let t=[],n=e.whisper?3:2,o=0;e.progress&&await e.progress.stepStart(o++,n,"Installing yt-dlp");let r=await Ry();t.push(`yt-dlp \u2192 ${r}`);let s;e.progress&&await e.progress.stepStart(o++,n,"Installing ffmpeg");try{s=await Ty(),t.push(`ffmpeg \u2192 ${s}`)}catch(a){t.push(`ffmpeg: ${String(a)} (install manually; video merge needs it)`)}let i;return e.whisper&&(e.progress&&await e.progress.stepStart(o++,n,"Installing whisper"),i=await My(),t.push(`whisper \u2192 ${i}`)),{ytDlp:r,ffmpeg:s,whisper:i,messages:t}}var Py=/^https?:\/\/\S+$/i;function Ho(e){let t=e.trim();if(!Py.test(t))return!1;try{return pt(t),!0}catch{return!1}}function pt(e){let t;try{t=new URL(e.trim())}catch{throw new Error("Invalid URL.")}if(t.protocol!=="http:"&&t.protocol!=="https:")throw new Error("Only http:// and https:// URLs are supported.");let n=t.hostname.toLowerCase();if(n==="localhost"||n.endsWith(".localhost")||n==="127.0.0.1"||n==="::1"||n==="0.0.0.0"||n.startsWith("127.")||n.startsWith("10.")||n.startsWith("192.168.")||/^172\.(1[6-9]|2\d|3[01])\./.test(n)||n.startsWith("169.254.")||n==="[::1]")throw new Error("Private or local URLs are not allowed.");return t}Ve();import ts from"node:fs";import Na from"node:path";import Wd from"node:fs";var Od=Number(process.env.PLATFORM_INBOUND_MEDIA_MAX_BYTES)>0?Number(process.env.PLATFORM_INBOUND_MEDIA_MAX_BYTES):54525952,Nd=50;function Fd(e){try{return JSON.parse(e)}catch{return null}}function rn(e){return e.fileSendMaxBytes<=0?8388608:Math.min(e.fileSendMaxBytes,8388608)}function La(e){if(Wd.statSync(e.absPath).size>8388608)throw new Error(`File too large for attached mode (max ${8388608} bytes).`);let n=Wd.readFileSync(e.absPath).toString("base64");return{name:e.displayName,mimetype:e.mimetype,category:e.category,dataBase64:n,...e.caption?{caption:e.caption}:{}}}function es(e,t,n,o){if(t.kind==="texts")return{body:t.bodies.map(s=>me(s,o).text).filter(s=>s.trim()).join(`
125
+
126
+ `),...n?{messageId:n}:{}};if(t.kind==="text")return{body:me(t.body,o).text,...n?{messageId:n}:{}};if(t.kind==="bundle"){let r=(t.texts??[]).map(i=>me(i,o).text).filter(i=>i.trim()),s=(t.files??[]).map(La);return{body:r.length?r.join(`
127
+
128
+ `):void 0,...n?{messageId:n}:{},...s.length?{files:s}:{}}}return t.kind==="file"?{...n?{messageId:n}:{},files:[La(t.spec)]}:{...n?{messageId:n}:{},files:t.specs.map(La)}}var Iy=new Set(["jpg","jpeg","png","gif","webp","bmp","tif","tiff","heic","avif","svg","mp4","mov","webm","mkv","avi","m4v","3gp","mp3","ogg","opus","wav","m4a","flac","aac","wma","pdf","zip","gz","tgz","tar","bz2","xz","7z","rar","doc","docx","xls","xlsx","ppt","pptx","txt","md","json","xml","csv","epub","apk","dmg","iso","deb","rpm","exe","msi","bin"]),Ly=new Set(["html","htm","php","asp","aspx","jsp","cgi"]);function Oa(e){let t=e.pathname.split("/").pop()??"",n=t.lastIndexOf(".");if(n<=0||n===t.length-1)return null;let o=t.slice(n+1).toLowerCase();return!o||!/^[a-z0-9]{1,12}$/i.test(o)?null:o}function _d(e){let t=typeof e=="string"?pt(e):e,n=Oa(t);return!n||Ly.has(n)?!1:Iy.has(n)}var Oy="omnish/1.0 (+https://omnish.dev)";function Ny(e){let t=[];return e.mediaMaxBytes>0&&t.push(e.mediaMaxBytes),e.mediaSendFiles&&(e.fileSendMaxBytes>0&&t.push(e.fileSendMaxBytes),re()&&t.push(rn(e))),t.length===0?0:Math.min(...t)}function ns(e){let n=Na.basename(e.replace(/\\/g,"/")).replace(/[^\w.\-()+@]/g,"_").replace(/^\.+/,"");return!n||n==="_"?"":n.slice(0,200)}function Fy(e){if(!e)return null;let t=/filename\*\s*=\s*[^']*'[^']*'([^;]+)/i.exec(e);if(t?.[1])try{return ns(decodeURIComponent(t[1].trim()))}catch{return ns(t[1].trim())}let n=/filename\s*=\s*("([^"]+)"|([^;\s]+))/i.exec(e),o=n?.[2]??n?.[3];return o?ns(o.trim()):null}function Wy(e,t){let n=Fy(t);if(n)return n;let o=ns(decodeURIComponent(e.pathname.split("/").pop()??""));if(o)return o;let r=Oa(e);return`download-${Date.now()}${r?`.${r}`:""}`}async function Dd(e){let t=pt(e.url),n=Ny(e.cfg),o=await fetch(t.href,{redirect:"follow",headers:{"User-Agent":Oy,Accept:"*/*"}});if(!o.ok)throw new Error(`Download failed (HTTP ${o.status}): ${e.url}`);let r=o.headers.get("content-length");if(n>0&&r){let u=Number(r);if(Number.isFinite(u)&&u>n)throw new Error(`File too large (${u} bytes; max ${n}).`)}let s=Wy(t,o.headers.get("content-disposition"));ts.mkdirSync(e.outputDir,{recursive:!0});let i=Na.join(e.outputDir,s),a=o.body;if(!a)throw new Error("Empty response body.");let l=ts.createWriteStream(i,{mode:384}),c=a.getReader(),d=0;try{for(;;){let{done:u,value:m}=await c.read();if(u)break;if(m?.length){if(d+=m.length,n>0&&d>n)throw await c.cancel().catch(()=>{}),new Error(`File too large (max ${n} bytes).`);await new Promise((h,f)=>{l.write(Buffer.from(m),g=>g?f(g):h())})}}}catch(u){l.close();try{ts.unlinkSync(i)}catch{}throw u}if(await new Promise((u,m)=>{l.end(h=>h?m(h):u())}),d===0){try{ts.unlinkSync(i)}catch{}throw new Error("Download returned no data.")}return Na.resolve(i)}async function sn(e,t){let n=[],o=t.length;for(let r=0;r<t.length;r++){let s=t[r];await e.stepStart(r,o,{label:s.label});try{n.push(await s.run())}catch(i){throw e.stepFail&&await e.stepFail(i),i}}return n}import Ud from"node:fs";import Bd from"node:path";var _y=new Set(["jpg","jpeg","png","gif","webp","bmp","tif","tiff","heic","avif"]),Dy=new Set(["mp4","mov","webm","mkv","avi","m4v","3gp"]),Uy=new Set(["mp3","ogg","opus","wav","m4a","flac","aac","wma"]),os={jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",bmp:"image/bmp",tif:"image/tiff",tiff:"image/tiff",heic:"image/heic",avif:"image/avif",mp4:"video/mp4",mov:"video/quicktime",webm:"video/webm",mkv:"video/x-matroska",avi:"video/x-msvideo",m4v:"video/x-m4v","3gp":"video/3gpp",mp3:"audio/mpeg",ogg:"audio/ogg",opus:"audio/opus",wav:"audio/wav",m4a:"audio/mp4",flac:"audio/flac",aac:"audio/aac",wma:"audio/x-ms-wma",pdf:"application/pdf",zip:"application/zip",gz:"application/gzip",txt:"text/plain",json:"application/json",xml:"application/xml",csv:"text/csv"};function By(e){let t=e.replace(/^\./,"").toLowerCase();return _y.has(t)?{category:"image",mimetype:os[t]??"image/jpeg"}:Dy.has(t)?{category:"video",mimetype:os[t]??"video/mp4"}:Uy.has(t)?{category:"audio",mimetype:os[t]??"audio/mpeg"}:{category:"document",mimetype:os[t]??"application/octet-stream"}}function xt(e,t){let n;try{n=Ud.realpathSync(e)}catch{return{error:"File not found or unreadable."}}let o;try{o=Ud.lstatSync(n)}catch{return{error:"Cannot stat file."}}if(!o.isFile())return{error:"Not a regular file (directories and special files are not supported)."};if(t>0&&o.size>t)return{error:`File too large (max ${t} bytes).`};let r=Bd.basename(n),s=Bd.extname(n).slice(1),{category:i,mimetype:a}=By(s);return{absPath:n,category:i,mimetype:a,displayName:r}}Ve();function Hy(e,t){let n=re()?rn(e):e.fileSendMaxBytes,o=[];for(let r of t){let s=xt(r,n);"error"in s||o.push(s)}return o}function Cn(e){let{cfg:t,files:n,textLines:o,skippedNote:r}=e,s=n.filter(d=>typeof d=="string"&&d.length>0),i=[...o];if(r&&i.push(r),!t.mediaSendFiles){let d=s.length?s.map(u=>` ${u}`):[" (no files)"];return{kind:"text",body:p([...i,"Files:",...d].filter(Boolean).join(`
129
+ `))}}let a=Hy(t,s),l=s.length-a.length,c=l>0?`(${l} file(s) over size cap \u2014 paths only in text above)`:"";return c&&i.push(c),a.length===0?{kind:"text",body:p(i.join(`
130
+
131
+ `)||"(done)")}:{kind:"bundle",texts:i.length?i.map(d=>p(d)):void 0,files:a}}import jy from"node:fs";import rs from"node:path";function no(e,t){let n=e.mediaOutputDir.trim(),o=new Date().toISOString().slice(0,10),r;n?r=rs.isAbsolute(n)?n:rs.resolve(t,n):r=rs.join(t,".omnish-media");let s=rs.join(r,o);return jy.mkdirSync(s,{recursive:!0}),s}async function Hd(e){pt(e.url);let t=no(e.cfg,e.sessionCwd),n=e.reporter??nn();if(_d(e.url)){let[l]=await sn(n,[{label:"Downloading file",run:async()=>Dd({cfg:e.cfg,url:e.url,outputDir:t})}]);return Cn({cfg:e.cfg,files:[l],textLines:[`Saved: ${l}`,`Output: ${t}`]})}let o=dt(e.cfg);if(!o.ytDlp)return{kind:"text",body:p("yt-dlp missing. Run: omnish pull install")};if(!o.ffmpeg)return{kind:"text",body:p("ffmpeg missing. Run: omnish pull install")};let[r]=await sn(n,[{label:"Downloading video",run:async()=>Ft({cfg:e.cfg,tools:o,url:e.url,mode:"video",outputDir:t})}]),s=r.files.filter(l=>/\.(mp4|mkv|webm|mov)$/i.test(l)),i=s.length?s:r.files,a=[r.title?`Title: ${r.title}`:"",`Saved under: ${t}`].filter(Boolean);return Cn({cfg:e.cfg,files:i,textLines:a})}import{spawnSync as Gy}from"node:child_process";import Jy from"node:fs";import oo from"node:path";import jd from"node:fs";import Gd from"node:path";function ss(e,t){let n=e.trim();if(!n)throw new Error("URL or file path required.");if(/^https?:\/\//i.test(n))return pt(n),{kind:"url",url:n};let o=Gd.isAbsolute(n)?n:Gd.resolve(t,n);if(!jd.existsSync(o))throw new Error(`File not found: ${o}`);if(!jd.statSync(o).isFile())throw new Error(`Not a file: ${o}`);return{kind:"file",absPath:o}}function Fa(e){let t=e.trim();if(!t)throw new Error("Empty time value.");if(/^\d+(\.\d+)?$/.test(t))return Number.parseFloat(t);let n=t.split(":").map(o=>Number.parseFloat(o));if(n.some(o=>!Number.isFinite(o)))throw new Error(`Invalid time: ${e}`);if(n.length===2)return n[0]*60+n[1];if(n.length===3)return n[0]*3600+n[1]*60+n[2];throw new Error(`Invalid time: ${e}`)}function Jd(e){let t=e.trim(),n={fromSec:null,toSec:null,durationSec:null,format:null,audioOnly:!1},o=!0;for(;o;){o=!1;let r=t.trim(),s=r.match(/^(?:--from|--start|-ss)\s+(\S+)(?:\s+([\s\S]+))?$/i);if(s){n.fromSec=Fa(s[1]),t=(s[2]??"").trim(),o=!0;continue}let i=r.match(/^(?:--to|--end)\s+(\S+)(?:\s+([\s\S]+))?$/i);if(i){n.toSec=Fa(i[1]),t=(i[2]??"").trim(),o=!0;continue}let a=r.match(/^(?:-t|--duration)\s+(\S+)(?:\s+([\s\S]+))?$/i);if(a){n.durationSec=Fa(a[1]),t=(a[2]??"").trim(),o=!0;continue}let l=r.match(/^(?:--format|-f)\s+(\S+)(?:\s+([\s\S]+))?$/i);if(l){n.format=l[1].replace(/^\./,"").toLowerCase(),t=(l[2]??"").trim(),o=!0;continue}/^--audio-only\b/i.test(r)&&(n.audioOnly=!0,t=r.replace(/^--audio-only\s*/i,"").trim(),o=!0)}if(!t)throw new Error("Missing URL or file path.");return{targetRaw:t,opts:n}}function qy(e,t){return t.format?t.format.replace(/^\./,""):t.audioOnly?"m4a":oo.extname(e).replace(/^\./,"")||"mp4"}function zy(e,t,n,o,r){let s=oo.extname(t).toLowerCase(),i=oo.extname(n).toLowerCase(),a=["-y"];o.fromSec!==null&&o.fromSec>0&&a.push("-ss",String(o.fromSec)),a.push("-i",t),o.durationSec!==null?a.push("-t",String(o.durationSec)):o.toSec!==null&&(o.fromSec!==null?a.push("-t",String(Math.max(.1,o.toSec-o.fromSec))):a.push("-to",String(o.toSec))),o.audioOnly||[".mp3",".m4a",".wav",".opus"].includes(i)?(a.push("-vn"),i===".mp3"?a.push("-acodec","libmp3lame"):i===".wav"&&a.push("-acodec","pcm_s16le")):s===i&&!o.audioOnly&&o.format===null&&a.push("-c","copy"),a.push(n);let l=Gy(e,a,{encoding:"utf8",timeout:r,windowsHide:!0});if(l.status!==0)throw new Error(`ffmpeg failed:
132
+ ${(l.stderr||l.stdout).slice(-2e3)}`);if(!Jy.existsSync(n))throw new Error("ffmpeg did not produce output file.")}async function qd(e){let t=dt(e.cfg),n=t.ffmpeg;if(!n)return{kind:"text",body:p("ffmpeg missing. Run: omnish pull install")};let{targetRaw:o,opts:r}=Jd(e.args),s=no(e.cfg,e.sessionCwd),i=e.reporter??nn(),a=ss(o,e.sessionCwd),l=Math.min(9e5,Math.max(6e4,e.cfg.syncTimeoutMs*3)),c;if(a.kind==="url"){if(!t.ytDlp)throw new Error("yt-dlp and ffmpeg required for URL input. Run: omnish pull install");let[f]=await sn(i,[{label:"Downloading",run:async()=>{let g=await Ft({cfg:e.cfg,tools:t,url:a.url,mode:"video",outputDir:s}),y=g.files.find(b=>/\.(mp4|mkv|webm|mov|m4a|mp3)$/i.test(b))??g.files[0];if(!y)throw new Error("Download produced no file.");return y}}]);c=f}else c=a.absPath;let d=qy(c,r),u=oo.basename(c,oo.extname(c)),m=oo.join(s,`${u}-edit.${d}`),[h]=await sn(i,[{label:"Editing",run:async()=>(zy(n,c,m,r,l),m)}]);return Cn({cfg:e.cfg,files:[h],textLines:[`Edited: ${h}`]})}import Yy from"node:fs";import{spawnSync as Wa}from"node:child_process";import zd from"node:fs";import Go from"node:path";function Ky(e,t){let n=Go.basename(t,Go.extname(t));return[".txt",".srt",".vtt",".json"].map(r=>Go.join(e,n+r)).filter(r=>zd.existsSync(r))}async function _a(e){let{cfg:t,tools:n,inputPath:o,outputDir:r}=e,s=n.whisper;if(!s)throw new Error("whisper not found. Run: omnish pull install --whisper");if(!zd.existsSync(o))throw new Error(`File not found: ${o}`);let i=Math.min(9e5,Math.max(12e4,t.syncTimeoutMs*4)),a=t.mediaWhisperModel.trim()||"small",l=Wa(s,[o,"--model",a,"--output_dir",r,"--output_format","all"],{encoding:"utf8",timeout:i,windowsHide:!0});if(l.status!==0&&n.ffmpeg){let c=Go.join(r,`_whisper_audio${Go.extname(o)||".m4a"}`);Wa(n.ffmpeg,["-y","-i",o,"-vn","-ar","16000","-ac","1",c],{encoding:"utf8",timeout:i,windowsHide:!0}).status===0&&(l=Wa(s,[c,"--model",a,"--output_dir",r,"--output_format","all"],{encoding:"utf8",timeout:i,windowsHide:!0}))}if(l.status!==0)throw new Error(`whisper failed:
133
+ ${(l.stderr||l.stdout).slice(-2e3)}`);return{files:Ky(r,o),log:l.stderr||l.stdout||""}}async function Da(e){let{cfg:t,tools:n,url:o,outputDir:r}=e,s=Math.min(9e5,Math.max(12e4,t.syncTimeoutMs*4)),i=await Ed({tools:n,url:o,outputDir:r,timeoutMs:s});return _a({cfg:t,tools:n,inputPath:i,outputDir:r})}function Qy(e){try{return Yy.readFileSync(e,"utf8").trim()}catch{return""}}function Vy(e,t){if(e.length<=t)return[e];let n=[];for(let o=0;o<e.length;o+=t)n.push(e.slice(o,o+t));return n}async function Kd(e){let t=dt(e.cfg);if(!t.whisper)return{kind:"text",body:p("whisper missing. Run: omnish pull install --whisper")};let n=ss(e.targetRaw,e.sessionCwd),o=no(e.cfg,e.sessionCwd),r=e.reporter??nn(),s="",i=null,a=[];if(n.kind==="url"){if(!t.ytDlp)return{kind:"text",body:p("yt-dlp missing. Run: omnish pull install")};if(!t.ffmpeg)return{kind:"text",body:p("ffmpeg missing. Run: omnish pull install")};a.push({label:"Downloading video",run:async()=>{let y=await Ft({cfg:e.cfg,tools:t,url:n.url,mode:"video",outputDir:o});if(i=y.files.filter(k=>/\.(mp4|mkv|webm|mov)$/i.test(k))[0]??y.files[0]??null,!i)throw new Error("No video file downloaded.");return s=i,y}})}else s=n.absPath;a.push({label:"Transcribing",run:async()=>_a({cfg:e.cfg,tools:t,inputPath:s,outputDir:o})}),a.push({label:"Preparing results",run:async()=>{}});let l=await sn(r,a),c=l[l.length-2],d=c.files.find(y=>y.endsWith(".txt")),u=c.files.find(y=>y.endsWith(".srt")||y.endsWith(".vtt")),m=d?Qy(d):"",h=Math.min(e.cfg.appsMaxWaChars,e.cfg.syncMaxBytes)||3500,f=m?Vy(m,h):["(no transcript text produced)"],g=[];return u&&g.push(u),n.kind==="url"&&i&&g.push(i),Cn({cfg:e.cfg,files:g,textLines:f})}async function Ua(e){if(e.kind==="text"){process.stdout.write(`${e.body.wa}
134
+ `);return}if(e.kind==="bundle"){for(let t of e.texts??[])process.stdout.write(`${t.wa}
135
+
136
+ `);for(let t of e.files??[])process.stdout.write(`FILE: ${t.absPath}
137
+ `);return}if(e.kind==="file"){process.stdout.write(`FILE: ${e.spec.absPath}
138
+ `);return}if(e.kind==="files")for(let t of e.specs)process.stdout.write(`FILE: ${t.absPath}
139
+ `)}async function Ba(e){let[t,n,o]=e,r=(t??"").toLowerCase(),s=S(),i=o?.trim()?Xy.resolve(o):process.cwd(),a=$d(),l=Kr({peerKey:a,enabled:s.progressUpdates});if(r==="dl"){await Ua(await Hd({cfg:s,url:n??"",sessionCwd:i,reporter:l}));return}if(r==="tr"){await Ua(await Kd({cfg:s,targetRaw:n??"",sessionCwd:i,reporter:l}));return}if(r==="edit"){await Ua(await qd({cfg:s,args:n??"",sessionCwd:i,reporter:l}));return}process.stderr.write(`Unknown media-exec command: ${r}
140
+ `),process.exitCode=1}pe();q();import Yd from"node:fs";import Jo from"node:path";function Zy(e,t){let n=e.mediaOutputDir.trim(),o=new Date().toISOString().slice(0,10),r;n?r=Jo.isAbsolute(n)?n:Jo.resolve(t,n):r=Jo.join(t,".omnish-media");let s=Jo.join(r,o);return Yd.mkdirSync(s,{recursive:!0}),s}async function ew(e){pt(e.url);let t=dt(e.cfg),n=Zy(e.cfg,e.sessionCwd),o=[],r;if(e.mode==="transcript"){let a=await Da({cfg:e.cfg,tools:t,url:e.url,outputDir:n});o.push(...a.files)}else if(e.mode==="all"){let a=await Ft({cfg:e.cfg,tools:t,url:e.url,mode:"all",outputDir:n});r=a.title,o.push(...a.files);try{let l=await Da({cfg:e.cfg,tools:t,url:e.url,outputDir:n});o.push(...l.files)}catch{}}else{let a=await Ft({cfg:e.cfg,tools:t,url:e.url,mode:e.mode,outputDir:n});r=a.title,o.push(...a.files)}let s=o.filter(a=>typeof a=="string"&&Yd.existsSync(a)),i=[r?`Title: ${r}`:"",`Saved under: ${n}`,s.length?"Files:":"(no files listed)",...s.map(a=>` ${a}`)].filter(Boolean);return{title:r,outputDir:n,files:s,summary:i.join(`
141
+ `)}}async function Qd(e){let[t,n,o]=e,r=t??"audio",s=S(),i=o?.trim()?Jo.resolve(o):Ni,a=await ew({cfg:s,mode:r,url:n??"",sessionCwd:i});process.stdout.write(a.summary+`
142
+ `)}function Ha(){let e=mt.stdout;console.log(Re(e,"omnish pull"),w(e,"\u2014 download media from URLs (yt-dlp + ffmpeg + optional Whisper)")),console.log(""),console.log(z(e,"Usage:")),console.log(` ${v(e,"omnish pull doctor")}`),console.log(` ${v(e,"omnish pull install")} ${w(e,"[--whisper]")}`),console.log(` ${v(e,"omnish pull instructions")}`),console.log(` ${v(e,"omnish media-exec <dl|tr|edit> <payload> <cwd>")} ${X(e,"(internal / background)")}`),console.log(""),console.log(w(e,"Chat: /dl help \xB7 /tr \xB7 /edit (/pull aliases older commands)")),console.log(X(e,"Docs: docs/features/media-commands.md"))}async function Vd(e){let t=(e[0]??"").toLowerCase(),n=e.slice(1);if(!t||t==="help"||t==="-h"||t==="--help"){Ha();return}if(t==="doctor"){let o=S(),{text:r}=Vr(o);console.log(U(mt.stdout,r));return}if(t==="instructions"||t==="setup"){console.log(zr());return}if(t==="install"){let o=n.some(s=>s==="--whisper"),r=mt.stdout;console.log(U(r,"Installing pull tools into ~/.omnish/bin \u2026"));try{let s=await Xr({whisper:o});for(let i of s.messages)console.log(U(r,i))}catch(s){console.error(C(mt.stderr,String(s))),mt.exitCode=1}return}console.error(C(mt.stderr,`Unknown subcommand "${t}". Try: omnish pull help`)),mt.exitCode=1}async function Xd(e){try{let t=(e[0]??"").toLowerCase();if(t==="dl"||t==="tr"||t==="edit"){await Ba(e);return}await Qd(e)}catch(t){console.error(C(mt.stderr,String(t))),mt.exitCode=1}}async function Zd(e){try{await Ba(e)}catch(t){console.error(C(mt.stderr,String(t))),mt.exitCode=1}}q();import{execFileSync as an}from"node:child_process";import ro from"node:fs";import zo from"node:os";import ht from"node:path";import je from"node:process";function Wt(){let e=je.execPath,t=je.argv[1];if(!t||!t.trim())return{nodePath:e,scriptPath:"",omnishHome:D,error:"Cannot resolve gateway entry script (missing argv[1]). Run omnish from its CLI entry."};let n=ht.resolve(t),o=je.env.OMNISH_HOME?.trim(),r=o?ht.resolve(o):D;return{nodePath:e,scriptPath:n,omnishHome:r}}function ep(e){return/[ "'\\\s]/.test(e)?`"${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`:e}function tw(e){return`Environment="OMNISH_HOME=${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}function nw(e){return`[Unit]
126
143
  Description=omnish gateway (WhatsApp/Telegram)
127
144
  After=network-online.target
128
145
  Wants=network-online.target
129
146
 
130
147
  [Service]
131
148
  Type=simple
132
- ExecStart=${`${pd(e.nodePath)} ${pd(e.scriptPath)} run`}
133
- ${Wg(e.omnishHome)}
149
+ ExecStart=${`${ep(e.nodePath)} ${ep(e.scriptPath)} run`}
150
+ ${tw(e.omnishHome)}
134
151
  Restart=on-failure
135
152
  RestartSec=5
136
153
 
137
154
  [Install]
138
155
  WantedBy=default.target
139
- `}function Oo(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function Ug(e){let t=No.homedir(),n=dt.join(e.omnishHome,"logs","launchd-stdout.log"),o=dt.join(e.omnishHome,"logs","launchd-stderr.log");B(dt.dirname(n));let r=Oo(e.nodePath),s=Oo(e.scriptPath),i=Oo(e.omnishHome),a=Oo(n),l=Oo(o);return`<?xml version="1.0" encoding="UTF-8"?>
156
+ `}function qo(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function ow(e){let t=zo.homedir(),n=ht.join(e.omnishHome,"logs","launchd-stdout.log"),o=ht.join(e.omnishHome,"logs","launchd-stderr.log");H(ht.dirname(n));let r=qo(e.nodePath),s=qo(e.scriptPath),i=qo(e.omnishHome),a=qo(n),l=qo(o);return`<?xml version="1.0" encoding="UTF-8"?>
140
157
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
141
158
  <plist version="1.0">
142
159
  <dict>
@@ -163,39 +180,40 @@ WantedBy=default.target
163
180
  <string>${l}</string>
164
181
  </dict>
165
182
  </plist>
166
- `}function Br(){let e=Ot();if(e.error)return{ok:!1,detail:e.error};if(He.platform==="win32")return{ok:!1,detail:"Automatic install is not supported on Windows from chat. Use Task Scheduler (see /service instructions) or omnish-windows-task.xml in the repo contrib folder."};if(He.platform==="darwin")try{let t=dt.join(No.homedir(),"Library/LaunchAgents/dev.omnish.gateway.plist");B(dt.dirname(t));let n=Ug(e);jn.writeFileSync(t,n,{mode:384});let o=typeof He.getuid=="function"?He.getuid():null;if(o===null||o<0)return{ok:!1,detail:"Could not read user id for launchctl."};let r=`gui/${o}`;try{Xt("launchctl",["bootout",r,t],{stdio:"pipe"})}catch{}return Xt("launchctl",["bootstrap",r,t],{stdio:"inherit"}),Xt("launchctl",["kickstart","-k",`${r}/dev.omnish.gateway`],{stdio:"inherit"}),{ok:!0,detail:"LaunchAgent installed: ~/Library/LaunchAgents/dev.omnish.gateway.plist (label dev.omnish.gateway)."}}catch(t){return{ok:!1,detail:String(t)}}if(He.platform==="linux")try{let t=dt.join(No.homedir(),".config","systemd","user");B(t);let n=dt.join(t,"omnish.service"),o=Dg(e);return jn.writeFileSync(n,o,{mode:420}),Xt("systemctl",["--user","daemon-reload"],{stdio:"inherit"}),Xt("systemctl",["--user","enable","--now","omnish.service"],{stdio:"inherit"}),{ok:!0,detail:`User systemd unit installed: ${n} \u2014 enabled and started.`}}catch(t){return{ok:!1,detail:String(t)}}return{ok:!1,detail:`Unsupported platform: ${He.platform}`}}function Hr(){if(He.platform==="win32")return{ok:!0,detail:"Windows: remove the omnish task from Task Scheduler manually on this PC. See /service instructions."};if(He.platform==="darwin")try{let e=dt.join(No.homedir(),"Library/LaunchAgents/dev.omnish.gateway.plist"),n=`gui/${typeof He.getuid=="function"?He.getuid():0}`;if(jn.existsSync(e)){try{Xt("launchctl",["bootout",n,e],{stdio:"pipe"})}catch{}jn.unlinkSync(e)}return{ok:!0,detail:"LaunchAgent dev.omnish.gateway removed (if it was present)."}}catch(e){return{ok:!1,detail:String(e)}}if(He.platform==="linux")try{let e=dt.join(No.homedir(),".config","systemd","user","omnish.service");try{Xt("systemctl",["--user","disable","--now","omnish.service"],{stdio:"pipe"})}catch{}jn.existsSync(e)&&jn.unlinkSync(e);try{Xt("systemctl",["--user","daemon-reload"],{stdio:"pipe"})}catch{}return{ok:!0,detail:"User systemd unit omnish.service removed (if it was present)."}}catch(e){return{ok:!1,detail:String(e)}}return{ok:!1,detail:`Unsupported platform: ${He.platform}`}}import md from"node:fs";var jr=48e3;function Gr(e,t){try{if(!md.existsSync(e))return"(no log file yet \u2014 start with `omnish run -d` or the systemd/LaunchAgent service.)";let n=md.readFileSync(e),r=(n.length>jr?n.subarray(n.length-jr):n).toString("utf8");return n.length>jr&&(r=`\u2026(truncated to last ${jr} bytes)
183
+ `}function is(){let e=Wt();if(e.error)return{ok:!1,detail:e.error};if(je.platform==="win32")return{ok:!1,detail:"Automatic install is not supported on Windows from chat. Use Task Scheduler (see /service instructions) or omnish-windows-task.xml in the repo contrib folder."};if(je.platform==="darwin")try{let t=ht.join(zo.homedir(),"Library/LaunchAgents/dev.omnish.gateway.plist");H(ht.dirname(t));let n=ow(e);ro.writeFileSync(t,n,{mode:384});let o=typeof je.getuid=="function"?je.getuid():null;if(o===null||o<0)return{ok:!1,detail:"Could not read user id for launchctl."};let r=`gui/${o}`;try{an("launchctl",["bootout",r,t],{stdio:"pipe"})}catch{}return an("launchctl",["bootstrap",r,t],{stdio:"inherit"}),an("launchctl",["kickstart","-k",`${r}/dev.omnish.gateway`],{stdio:"inherit"}),{ok:!0,detail:"LaunchAgent installed: ~/Library/LaunchAgents/dev.omnish.gateway.plist (label dev.omnish.gateway)."}}catch(t){return{ok:!1,detail:String(t)}}if(je.platform==="linux")try{let t=ht.join(zo.homedir(),".config","systemd","user");H(t);let n=ht.join(t,"omnish.service"),o=nw(e);return ro.writeFileSync(n,o,{mode:420}),an("systemctl",["--user","daemon-reload"],{stdio:"inherit"}),an("systemctl",["--user","enable","--now","omnish.service"],{stdio:"inherit"}),{ok:!0,detail:`User systemd unit installed: ${n} \u2014 enabled and started.`}}catch(t){return{ok:!1,detail:String(t)}}return{ok:!1,detail:`Unsupported platform: ${je.platform}`}}function as(){if(je.platform==="win32")return{ok:!0,detail:"Windows: remove the omnish task from Task Scheduler manually on this PC. See /service instructions."};if(je.platform==="darwin")try{let e=ht.join(zo.homedir(),"Library/LaunchAgents/dev.omnish.gateway.plist"),n=`gui/${typeof je.getuid=="function"?je.getuid():0}`;if(ro.existsSync(e)){try{an("launchctl",["bootout",n,e],{stdio:"pipe"})}catch{}ro.unlinkSync(e)}return{ok:!0,detail:"LaunchAgent dev.omnish.gateway removed (if it was present)."}}catch(e){return{ok:!1,detail:String(e)}}if(je.platform==="linux")try{let e=ht.join(zo.homedir(),".config","systemd","user","omnish.service");try{an("systemctl",["--user","disable","--now","omnish.service"],{stdio:"pipe"})}catch{}ro.existsSync(e)&&ro.unlinkSync(e);try{an("systemctl",["--user","daemon-reload"],{stdio:"pipe"})}catch{}return{ok:!0,detail:"User systemd unit omnish.service removed (if it was present)."}}catch(e){return{ok:!1,detail:String(e)}}return{ok:!1,detail:`Unsupported platform: ${je.platform}`}}import tp from"node:fs";var ls=48e3;function cs(e,t){try{if(!tp.existsSync(e))return"(no log file yet \u2014 start with `omnish run -d` or the systemd/LaunchAgent service.)";let n=tp.readFileSync(e),r=(n.length>ls?n.subarray(n.length-ls):n).toString("utf8");return n.length>ls&&(r=`\u2026(truncated to last ${ls} bytes)
167
184
  ${r}`),r.split(/\r?\n/).slice(-t).join(`
168
- `).trimEnd()||"(empty)"}catch(n){return`Could not read log: ${String(n)}`}}import Zt from"node:fs";import ay from"node:path";G();import Bg from"node:crypto";import ba from"node:fs";import Hg from"node:path";var ka=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,qn="__omnish_recipes_global__",va=new Set(["help","list","show","add","set","remove","rm","del","run","r","online"]),Gn=new Map,hd=!1;function jg(){try{let e=ba.readFileSync(lr,"utf8"),t=JSON.parse(e);if(t&&typeof t=="object")for(let[n,o]of Object.entries(t)){if(!o||typeof o!="object")continue;let r={};for(let[s,i]of Object.entries(o)){let a=String(s).toLowerCase();i&&typeof i=="object"&&typeof i.command=="string"&&(r[a]=Ke(i))}Gn.set(n,r)}}catch{}}function Gg(e){if(!Array.isArray(e)||e.length===0)return;let t=[];for(let n of e)if(typeof n=="string"&&n.trim())t.push({cmd:n.trim()});else if(n&&typeof n=="object"){let o=n,r=typeof o.cmd=="string"?o.cmd.trim():"";if(!r)continue;t.push({cmd:r,label:typeof o.label=="string"?o.label.trim().slice(0,80):void 0,continueOnFail:typeof o.continueOnFail=="boolean"?o.continueOnFail:void 0})}return t.length>0?t:void 0}function Ke(e){let t=typeof e.taskEnv=="string"&&/^[A-Za-z_][A-Za-z0-9_]*$/.test(e.taskEnv)?e.taskEnv:"OMNISH_TASK",n=typeof e.promptTemplate=="string"&&e.promptTemplate.trim().length>0?e.promptTemplate.trim():void 0,o=Gg(e.steps),r={command:String(e.command).trim(),taskEnv:t,label:typeof e.label=="string"?e.label.trim().slice(0,80):void 0,description:typeof e.description=="string"?e.description.trim().slice(0,200):void 0,category:typeof e.category=="string"?e.category.trim().slice(0,40):void 0,featured:typeof e.featured=="boolean"?e.featured:void 0,dangerous:typeof e.dangerous=="boolean"?e.dangerous:void 0};return n!==void 0&&(r.promptTemplate=n),o!==void 0&&(r.steps=o),r}function Jr(){B(Hg.dirname(lr));let e={};for(let[t,n]of Gn)Object.entries(n).length>0&&(e[t]={...n});ba.writeFileSync(lr,JSON.stringify(e,null,2)+`
169
- `,{mode:384})}function zr(){hd||(jg(),hd=!0)}function fd(e){return zr(),Gn.get(e)??{}}function ya(e){zr();let t=Gn.get(e);return t||(t={},Gn.set(e,t)),t}function qg(e,t){let n=e.taskEnv??"OMNISH_TASK",o=t.recipesMacroDefaultCommand.trim();return gn(o,n).ok?Ke({...e,command:o,promptTemplate:e.command}):e}function Jg(e,t){if(e.promptTemplate||gn(e.command,t).ok||!Jn(e.command,t))return!1;let n=e.command;return n.includes("```")||n.length>2e3||/^"Create\b/i.test(n)||n.includes("<<<")||n.includes(`
170
- ### `)}function Sa(e,t){let n=Ke(e),o=n.taskEnv??"OMNISH_TASK";return!n.promptTemplate&&Jg(n,o)&&(n=qg(n,t)),n}function zg(e){try{let t=ba.readFileSync(ar,"utf8"),n=JSON.parse(t);if(!n||typeof n!="object")return{};let o={};for(let[r,s]of Object.entries(n)){let i=r.trim().toLowerCase();if(!(!ka.test(i)||va.has(i))&&s&&typeof s=="object"&&typeof s.command=="string"){let l=Sa(s,e),d=yn(l);if(!d.ok){console.warn(`[omnish] recipes.json: skipping "${i}": ${d.error}`);continue}o[i]=l}}return o}catch{return{}}}function Kg(e){return{claude:{command:'claude -p "$OMNISH_TASK" --allowedTools all --dangerously-skip-permissions',taskEnv:"OMNISH_TASK",label:"Claude Code",description:"Anthropic Claude Code CLI (requires `claude` on PATH)",category:"agents",featured:!0,dangerous:e.recipesAllowDangerousBuiltins},codex:{command:'codex "$OMNISH_TASK"',taskEnv:"OMNISH_TASK",label:"Codex CLI",description:"OpenAI Codex CLI if installed; override in recipes.json",category:"agents",featured:!1},gemini:{command:'gemini -p "$OMNISH_TASK"',taskEnv:"OMNISH_TASK",label:"Gemini CLI",description:"Google Gemini CLI if installed; override in recipes.json",category:"agents",featured:!1},cursor:{command:"agent --yolo --force -p ```Before writing any code:\n1. Analyze the codebase and state your full implementation plan\n2. List every file you will touch\n3. Identify any risks or ambiguities\n4. Then execute the plan step by step, <task> $OMNISH_TASK </task>```",taskEnv:"OMNISH_TASK",label:"Cursor Agent",description:"Cursor Agent if installed; override in recipes.json",category:"agents",featured:!0}}}function qr(e,t,n){for(let[o,r]of Object.entries(t)){let s=o.toLowerCase();!ka.test(s)||va.has(s)||r.command.trim()&&e.set(s,{...r,name:s,source:n})}}function wa(e,t){let n=fd(e),o={};for(let[r,s]of Object.entries(n)){let i=Sa(s,t);yn(i).ok&&(o[r]=i)}return o}function gd(e,t){let n=new Map;return qr(n,Kg(t),"builtin"),qr(n,zg(t),"global"),qr(n,wa(qn,t),"shared"),qr(n,wa(e,t),"peer"),n}function Ye(e,t,n){let o=n.trim().toLowerCase();return gd(e,t).get(o)}function yd(e){let t=e.trim();if(/^--global$/i.test(t))return{mode:"global",remainder:""};if(/^-g$/i.test(t))return{mode:"global",remainder:""};if(/^--chat$/i.test(t))return{mode:"chat",remainder:""};if(/^-p$/i.test(t))return{mode:"chat",remainder:""};let n=/^--global\s+/i.exec(t);if(n)return{mode:"global",remainder:t.slice(n[0].length).trimStart()};let o=/^-g\s+/i.exec(t);if(o)return{mode:"global",remainder:t.slice(o[0].length).trimStart()};let r=/^--chat\s+/i.exec(t);if(r)return{mode:"chat",remainder:t.slice(r[0].length).trimStart()};let s=/^-p\s+/i.exec(t);return s?{mode:"chat",remainder:t.slice(s[0].length).trimStart()}:{mode:"resolved",remainder:t}}function xa(e){let t=e.trim();if(/^--global$/i.test(t))return{scope:"global",remainder:"",explicit:!0};if(/^-g$/i.test(t))return{scope:"global",remainder:"",explicit:!0};if(/^--chat$/i.test(t))return{scope:"chat",remainder:"",explicit:!0};if(/^-p$/i.test(t))return{scope:"chat",remainder:"",explicit:!0};let n=/^--global\s+([\s\S]*)$/i.exec(t);if(n?.[1]!==void 0)return{scope:"global",remainder:n[1].trimStart(),explicit:!0};let o=/^-g\s+([\s\S]*)$/i.exec(t);if(o?.[1]!==void 0)return{scope:"global",remainder:o[1].trimStart(),explicit:!0};let r=/^--chat\s+([\s\S]*)$/i.exec(t);if(r?.[1]!==void 0)return{scope:"chat",remainder:r[1].trimStart(),explicit:!0};let s=/^-p\s+([\s\S]*)$/i.exec(t);return s?.[1]!==void 0?{scope:"chat",remainder:s[1].trimStart(),explicit:!0}:{scope:"chat",remainder:t,explicit:!1}}function Ca(e){let t=xa(e);return{scope:t.scope,remainder:t.remainder}}function wd(e){let t=e.trim();return!t||/^-+$/i.test(t)?{filter:"merged"}:/^(?:--global|-g)$/i.test(t)?{filter:"global"}:/^(?:--chat|-p)$/i.test(t)?{filter:"chat"}:{filter:"merged",bad:t}}function Yg(e){let t=e.trim().toLowerCase();if(t==="--global"||t==="-g")return"global";if(t==="--chat"||t==="-p")return"chat"}function bd(e){let t=e.trim().match(/^(\S+)\s+(--global|-g|--chat|-p)\s*$/i);if(!t?.[1]||!t[2])return;let n=Yg(t[2]);if(n)return{name:t[1],target:n}}function Nt(e,t,n,o){let r=o.trim().toLowerCase(),s=e==="global"?qn:t,i=fd(s)[r];if(i===void 0)return;let a=Sa(i,n);if(yn(a).ok)return{...a,name:r,source:e==="global"?"shared":"peer"}}function kd(e,t,n){if(n==="merged")return[];let o=n==="global"?qn:e,r=n==="global"?"shared":"peer",s=wa(o,t);return Object.entries(s).map(([i,a])=>({...a,name:i,source:r})).sort((i,a)=>i.name.localeCompare(a.name))}function vt(e){let t=e.trim();if(!t)return{ok:!1,error:"Name is empty."};let n=t.toLowerCase();return ka.test(n)?va.has(n)?{ok:!1,error:`Reserved name: ${n}`}:{ok:!0,normalized:n}:{ok:!1,error:`Invalid name (use letters, digits, _ or -; max 32 chars): ${t}`}}function Kr(e,t){let n=e.replace(/\r\n/g,`
171
- `).trim();return n?t>0&&n.length>t?{ok:!1,error:`Task too long (max ${t} characters).`}:{ok:!0,task:n}:{ok:!1,error:"Task is empty."}}function Jn(e,t){return e.includes("$")?e.includes(t):!1}function gn(e,t){let n=e.trim();if(!n)return{ok:!1,error:"Command is empty."};if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(t))return{ok:!1,error:"Invalid taskEnv (use letters, digits, _)."};if(!Jn(n,t))return{ok:!1,error:`Command must reference "$${t}" so the task is passed via the environment.`};let o=`"$${t}"`;return n.includes(o)?n.includes("```")?{ok:!1,error:"Recipe command must not contain markdown code fences (```). Use promptTemplate or split with a --- line (see /run help)."}:{ok:!0}:{ok:!1,error:`Command must include ${o} (quoted).`}}function yn(e){let t=e.taskEnv??"OMNISH_TASK",n=gn(e.command,t);return n.ok?e.promptTemplate!==void 0&&e.promptTemplate.trim().length===0?{ok:!1,error:"promptTemplate cannot be empty."}:{ok:!0}:n}function Qg(e){let t=e,n=null,o=!1,r=-1,s=-1;for(let l=0;l<t.length;l+=1){let d=t[l];if(o){o=!1;continue}if(d==="\\"){o=!0;continue}if(n){d===n&&(n=null);continue}if(d==='"'||d==="'"){n=d;continue}if(d!=="-"||t[l+1]!=="-")continue;let u=l===0?"":t[l-1];if(u&&!/\s/.test(u))continue;let c=t.slice(l+2),m=/^(template|tamplate)\b/i.exec(c);if(!m)continue;let h=l+2+m[0].length,f=t[h]??"";if(!f||!/\s/.test(f))continue;r=l;let g=h;for(;g<t.length&&/\s/.test(t[g]);)g+=1;s=g;break}if(r<0||s<0)return{command:t.trim()};let i=t.slice(0,r).trim(),a=t.slice(s).trim();return(a.startsWith('"')&&a.endsWith('"')&&a.length>=2||a.startsWith("'")&&a.endsWith("'")&&a.length>=2)&&(a=a.slice(1,-1)),{command:i,template:a}}function Ra(e,t){let n=e.replace(/\r\n/g,`
172
- `).trim();if(!n)throw new Error("Recipe body is empty.");let o=n.match(/^--steps\s+([\s\S]+)$/i);if(o){let m=o[1].trim().split(/\s*;\s*|\n/).filter(f=>f.trim());if(m.length===0)throw new Error("No steps found. Separate steps with ; or newlines.");let h=m.map(f=>{let g=f.startsWith("+"),y=g?f.slice(1).trim():f.trim();if(!y)throw new Error("Empty step in recipe.");return{cmd:y,...g?{continueOnFail:!0}:{}}});return Ke({command:h[0].cmd,steps:h})}let r="OMNISH_TASK",{command:s,template:i}=Qg(n);if(i!==void 0){if(!s)throw new Error("Command part (before --template) is empty.");if(!i.trim())throw new Error("Template part (after --template) is empty.");let c=gn(s,r);if(!c.ok)throw new Error(c.error);return Ke({command:s,promptTemplate:i})}let a=/\n---\n/,l=n.search(a);if(l>=0){let c=n.slice(0,l).trim(),m=n.slice(l).replace(/^\n---\n/,"").trim();if(!c)throw new Error("Command part (before ---) is empty.");if(!m)throw new Error("Template part (after ---) is empty.");let h=gn(c,r);if(!h.ok)throw new Error(h.error);return Ke({command:c,promptTemplate:m})}if(gn(n,r).ok)return Ke({command:n});let d=t.trim(),u=gn(d,r);if(!u.ok)throw new Error(`recipesMacroDefaultCommand invalid (${u.error}). Fix config or paste runner then --- then template.`);return Ke({command:d,promptTemplate:n})}function Yr(e,t,n){let o="`".repeat(3),r=`<<<${o}$${t}${o}>>>`,s=e;s.includes(r)&&(s=s.split(r).join(n));let i=`<<<${t}>>>`;s.includes(i)&&(s=s.split(i).join(n));let a=new RegExp(`\\$${t}\\b`,"g");return s.replace(a,n)}function zn(e,t,n,o="chat",r){let s=vt(t);if(!s.ok)throw new Error(s.error);let i=Ke({...n,command:n.command});if(r?.skipCommandValidation){if(!i.command.trim())throw new Error("Command is empty.")}else{let d=yn(i);if(!d.ok)throw new Error(d.error)}let a=o==="global"?qn:e,l=ya(a);l[s.normalized]=i,Jr()}function vd(e,t,n="chat"){let o=t.trim().toLowerCase(),r=n==="global"?qn:e;zr();let s=Gn.get(r);return!s||!(o in s)?!1:(delete s[o],Jr(),!0)}function Ta(e,t,n,o){let r=vt(t);if(!r.ok)return{ok:!1,error:r.error};let s=r.normalized,i=e,a=qn;zr();let l=ya(i),d=ya(a),u=l[s],c=d[s],m=h=>{let f=Ke({...h}),g=yn(f);if(!g.ok)throw new Error(g.error);return f};try{if(n==="global"){if(u!==void 0){let g=m(u);return d[s]=g,delete l[s],Jr(),{ok:!0,kind:"moved",target:"global",name:s}}if(c!==void 0)return{ok:!0,kind:"noop",message:`Recipe "${s}" is already gateway-shared.`};let f=Ye(e,o,s);return f?.source==="builtin"||f?.source==="global"?{ok:!1,error:`Recipe "${s}" is built-in or from host recipes.json \u2014 not moved via /run set. Edit recipes.json or use a different name.`}:{ok:!1,error:`No user recipe "${s}" in this chat to promote. Add with /run add ${s} \u2026 or demote from global with /run set -p ${s}.`}}if(c!==void 0){let f=m(c);return l[s]=f,delete d[s],Jr(),{ok:!0,kind:"moved",target:"chat",name:s}}if(u!==void 0)return{ok:!0,kind:"noop",message:`Recipe "${s}" is already stored for this chat only.`};let h=Ye(e,o,s);return h?.source==="builtin"||h?.source==="global"?{ok:!1,error:`Recipe "${s}" is built-in or from host recipes.json \u2014 not moved via /run set.`}:{ok:!1,error:`No gateway-shared user recipe "${s}" to demote. Add with /run add --global ${s} \u2026 or promote from this chat with /run set -g ${s}.`}}catch(h){return{ok:!1,error:String(h)}}}function Sd(e,t){let n=[...gd(e,t).values()],o=n.filter(a=>a.featured).sort((a,l)=>a.name.localeCompare(l.name)),r=n.filter(a=>a.source==="peer").sort((a,l)=>a.name.localeCompare(l.name)),s=n.filter(a=>a.source==="shared").sort((a,l)=>a.name.localeCompare(l.name)),i=n.filter(a=>!a.featured&&a.source!=="peer"&&a.source!=="shared").sort((a,l)=>a.name.localeCompare(l.name));return{featured:o,shared:s,yours:r,more:i}}function Qr(e){let t=Bg.randomBytes(4).toString("hex");return`r-${e.replace(/[^a-zA-Z0-9_-]/g,"").slice(0,12)}-${t}`.slice(0,32)}function xd(e,t){let n=[`runbook: ${e}`];for(let r of t){let s=r.skipped?"skip":r.exitCode===0?"ok":"FAIL",i=r.label||`step ${r.index+1}`;if(r.skipped)n.push(`[${s}] ${i}`);else{n.push(`[${s}] ${i}${r.timedOut?" (timeout)":r.exitCode!==0?` (exit ${r.exitCode})`:""}`);let a=r.output.trim();if(a){let l=a.length>300?a.slice(0,300)+"\u2026":a;n.push(l)}}}let o=t.every(r=>r.skipped||r.exitCode===0);return n.push(o?"All steps completed successfully.":"Runbook stopped on failure."),n.join(`
173
- `)}G();var Cd={enter:"\r",cr:"\r",lf:`
174
- `,return:"\r",tab:" ",esc:"\x1B",escape:"\x1B",space:" ",backspace:"\x7F",bs:"\x7F",delete:"\x1B[3~",up:"\x1B[A",down:"\x1B[B",right:"\x1B[C",left:"\x1B[D",home:"\x1B[H",end:"\x1B[F",pageup:"\x1B[5~",pagedown:"\x1B[6~","ctrl+c":"","ctrl+d":"","ctrl+z":"","ctrl+l":"\f","ctrl+u":"","ctrl+k":"\v"};function Vg(e){let t="";for(let n=0;n<e.length;n++){let o=e[n];if(o==="\\"&&n+1<e.length){let r=e[n+1];if(r==="x"&&n+3<e.length){let s=e.slice(n+2,n+4);if(/^[0-9a-fA-F]{2}$/.test(s)){t+=String.fromCharCode(Number.parseInt(s,16)),n+=3;continue}}if(r==="r"){t+="\r",n++;continue}if(r==="n"){t+=`
175
- `,n++;continue}if(r==="t"){t+=" ",n++;continue}if(r==="\\"){t+="\\",n++;continue}}t+=o}return t}function Xg(e){let t=e.trim();if(!t)return"";if(t.startsWith("\\"))return Vg(t);let n=t.toLowerCase();if(Cd[n])return Cd[n];let o=t.match(/^\^([A-Za-z])$/);if(o){let s=o[1].toUpperCase().charCodeAt(0);if(s>=64&&s<=95)return String.fromCharCode(s-64)}let r=n.match(/^ctrl\+(.+)$/);if(r){let s=r[1];if(s.length===1){let i=s.toUpperCase().charCodeAt(0);if(i>=64&&i<=95)return String.fromCharCode(i-64)}}return t}function Vr(e){let t=e.split(",").map(n=>n.trim()).filter(Boolean);return t.length===0?"":t.map(n=>Xg(n)).join("")}var Fo="\x1B";function Xr(e){let t=e;return t=t.replace(new RegExp(`${Fo}\\[[\\d;?]*[ -/]*[@-~]`,"g"),""),t=t.replace(new RegExp(`${Fo}\\][^${Fo}\\u0007]*(?:\\u0007|${Fo}\\\\)`,"g"),""),t=t.replace(new RegExp(`${Fo}[@-Z\\\\-_]`,"g"),""),t=t.replace(/\u0007/g,""),t}function Zg(e,t){if(e.length<=t)return e?[e]:[];let n=[];for(let o=0;o<e.length;o+=t)n.push(e.slice(o,o+t));return n}function ey(e,t){return`${e}${t}`}var Zr=class{constructor(t){this.opts=t}pending=new Map;timers=new Map;lastFlushEnd=new Map;flushing=new Set;closed=!1;dispose(){this.closed=!0;for(let t of this.timers.values())clearTimeout(t);this.timers.clear(),this.pending.clear(),this.lastFlushEnd.clear(),this.flushing.clear()}push(t,n,o){if(this.closed||this.opts.isMuted(t,n))return;let r=ey(t,n);this.pending.set(r,(this.pending.get(r)??"")+o);let s=this.timers.get(r);s&&clearTimeout(s);let i=this.opts.getCfg().appsFlushMs,a=setTimeout(()=>{this.timers.delete(r),this.flushNow(r,t,n)},i);this.timers.set(r,a)}async flushNow(t,n,o){if(!(this.closed||this.flushing.has(t))){this.flushing.add(t);try{let r=this.opts.getCfg(),s=r.appsMinIntervalMs,i=this.lastFlushEnd.get(t)??0,a=Math.max(0,i+s-Date.now());if(a>0&&await new Promise(h=>setTimeout(h,a)),this.closed)return;let l=this.pending.get(t)??"";if(this.pending.delete(t),!l||(this.opts.isRaw(n,o)||(l=Xr(l)),!l.trim()))return;let d=r.appsMaxFlushBytes;if(l.length>d){let h=l.slice(d);l=l.slice(0,d)+`
176
- [\u2026+${h.length} chars; /apps since ${o} to read more]`,this.pending.set(t,(this.pending.get(t)??"")+h)}let c=(this.opts.getRunningCount(n)>1?`[${o}] `:"")+l,m=Zg(c,r.appsMaxWaChars);for(let h of m){if(this.closed)break;try{await this.opts.send(n,h)}catch{break}}this.lastFlushEnd.set(t,Date.now())}finally{this.flushing.delete(t),!this.closed&&(this.pending.get(t)??"").length>0&&queueMicrotask(()=>void this.flushNow(t,n,o))}}}};var es=4096,ty=/\[sudo\]\s+password\s+for\s+/i,ny=/passphrase\s+for\s+/i,oy=/(?:^|\n)\s*(?:Password|password):\s*$/;function ts(e){let n=Xr(e).replace(/\r/g,"").slice(-512);return!!(ty.test(n)||ny.test(n)||oy.test(n))}G();import ry from"node:fs";import sy from"node:path";import*as Rd from"node-pty";var iy=1024*1024,ns=class e{peerKey;name;command;cwd;envKeysCount;logPath;term=null;logStream=null;disposeData=null;disposeExit=null;exited=!1;ringChunks=[];ringBytes=0;constructor(t){this.peerKey=t.peerKey,this.name=t.name,this.command=t.command,this.cwd=t.cwd,this.envKeysCount=t.envKeysCount,this.logPath=t.logPath}appendRing(t){for(this.ringChunks.push(t),this.ringBytes+=t.length;this.ringBytes>iy&&this.ringChunks.length>0;){let n=this.ringChunks.shift();this.ringBytes-=n.length}}static start(t){B(yo(t.peerKey));let n=sy.join(yo(t.peerKey),`${t.name}.log`),o=ry.createWriteStream(n,{flags:"a",mode:384}),r={...process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...t.cwd?{PWD:t.cwd}:{},...t.extraEnv??{}},s=Rd.spawn(t.shell,["-lc",t.command],{name:"xterm-256color",cols:t.cols,rows:t.rows,cwd:t.cwd,env:r}),i=new e({...t,logPath:n});return i.term=s,i.logStream=o,i.disposeData=s.onData(a=>{let l=Buffer.from(a,"utf8");i.appendRing(l),o.write(a),t.onOutput?.(a),t.router.push(t.peerKey,t.name,a)}),i.disposeExit=s.onExit(a=>{i.exited||(i.exited=!0,i.cleanupTerm(),t.onExit({exitCode:a.exitCode,signal:a.signal}))}),i}get alive(){return this.term!==null&&!this.exited}get ringByteCount(){return this.ringBytes}recentOutputTail(t=4096){if(this.ringBytes===0)return"";let n=Math.max(1,t),o=[],r=Math.min(n,this.ringBytes);for(let s=this.ringChunks.length-1;s>=0&&r>0;s--){let i=this.ringChunks[s];i.length<=r?(o.unshift(i),r-=i.length):(o.unshift(i.subarray(i.length-r)),r=0)}return Buffer.concat(o).toString("utf8")}write(t){this.term?.write(t)}resize(t,n){this.term?.resize(t,n)}kill(t){if(this.term)try{this.term.kill(t)}catch{}}cleanupTerm(){this.disposeData?.dispose(),this.disposeData=null,this.disposeExit?.dispose(),this.disposeExit=null,this.term=null,this.logStream?.end(),this.logStream=null}destroy(){if(this.exited){this.cleanupTerm();return}this.exited=!0,this.kill("SIGHUP"),this.cleanupTerm()}};function $a(e){return new Promise(t=>setTimeout(t,e))}async function os(e,t,n){let o=n.appsSubmitDelayMs,r=n.appsClearInputDelayMs,s=n.appsClearInput!==!1,a=n.appsSkipClearOnPasswordPrompt!==!1&&ts(e.recentOutputTail(es)),l=s&&!a?Vr(n.appsClearInputSequence||"^A,^K"):"",u=t.replace(/\r\n/g,`
185
+ `).trimEnd()||"(empty)"}catch(n){return`Could not read log: ${String(n)}`}}import ln from"node:fs";import Cw from"node:path";q();import rw from"node:crypto";import Ja from"node:fs";import sw from"node:path";var qa=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,io="__omnish_recipes_global__",za=new Set(["help","list","show","add","set","remove","rm","del","run","r","online"]),so=new Map,np=!1;function iw(){try{let e=Ja.readFileSync(br,"utf8"),t=JSON.parse(e);if(t&&typeof t=="object")for(let[n,o]of Object.entries(t)){if(!o||typeof o!="object")continue;let r={};for(let[s,i]of Object.entries(o)){let a=String(s).toLowerCase();i&&typeof i=="object"&&typeof i.command=="string"&&(r[a]=Ke(i))}so.set(n,r)}}catch{}}function aw(e){if(!Array.isArray(e)||e.length===0)return;let t=[];for(let n of e)if(typeof n=="string"&&n.trim())t.push({cmd:n.trim()});else if(n&&typeof n=="object"){let o=n,r=typeof o.cmd=="string"?o.cmd.trim():"";if(!r)continue;t.push({cmd:r,label:typeof o.label=="string"?o.label.trim().slice(0,80):void 0,continueOnFail:typeof o.continueOnFail=="boolean"?o.continueOnFail:void 0})}return t.length>0?t:void 0}function Ke(e){let t=typeof e.taskEnv=="string"&&/^[A-Za-z_][A-Za-z0-9_]*$/.test(e.taskEnv)?e.taskEnv:"OMNISH_TASK",n=typeof e.promptTemplate=="string"&&e.promptTemplate.trim().length>0?e.promptTemplate.trim():void 0,o=aw(e.steps),r={command:String(e.command).trim(),taskEnv:t,label:typeof e.label=="string"?e.label.trim().slice(0,80):void 0,description:typeof e.description=="string"?e.description.trim().slice(0,200):void 0,category:typeof e.category=="string"?e.category.trim().slice(0,40):void 0,featured:typeof e.featured=="boolean"?e.featured:void 0,dangerous:typeof e.dangerous=="boolean"?e.dangerous:void 0};return n!==void 0&&(r.promptTemplate=n),o!==void 0&&(r.steps=o),r}function ds(){H(sw.dirname(br));let e={};for(let[t,n]of so)Object.entries(n).length>0&&(e[t]={...n});Ja.writeFileSync(br,JSON.stringify(e,null,2)+`
186
+ `,{mode:384})}function ps(){np||(iw(),np=!0)}function op(e){return ps(),so.get(e)??{}}function ja(e){ps();let t=so.get(e);return t||(t={},so.set(e,t)),t}function lw(e,t){let n=e.taskEnv??"OMNISH_TASK",o=t.recipesMacroDefaultCommand.trim();return Rn(o,n).ok?Ke({...e,command:o,promptTemplate:e.command}):e}function cw(e,t){if(e.promptTemplate||Rn(e.command,t).ok||!ao(e.command,t))return!1;let n=e.command;return n.includes("```")||n.length>2e3||/^"Create\b/i.test(n)||n.includes("<<<")||n.includes(`
187
+ ### `)}function Ka(e,t){let n=Ke(e),o=n.taskEnv??"OMNISH_TASK";return!n.promptTemplate&&cw(n,o)&&(n=lw(n,t)),n}function uw(e){try{let t=Ja.readFileSync(wr,"utf8"),n=JSON.parse(t);if(!n||typeof n!="object")return{};let o={};for(let[r,s]of Object.entries(n)){let i=r.trim().toLowerCase();if(!(!qa.test(i)||za.has(i))&&s&&typeof s=="object"&&typeof s.command=="string"){let l=Ka(s,e),c=Tn(l);if(!c.ok){console.warn(`[omnish] recipes.json: skipping "${i}": ${c.error}`);continue}o[i]=l}}return o}catch{return{}}}function dw(e){return{claude:{command:'claude -p "$OMNISH_TASK" --allowedTools all --dangerously-skip-permissions',taskEnv:"OMNISH_TASK",label:"Claude Code",description:"Anthropic Claude Code CLI (requires `claude` on PATH)",category:"agents",featured:!0,dangerous:e.recipesAllowDangerousBuiltins},codex:{command:'codex "$OMNISH_TASK"',taskEnv:"OMNISH_TASK",label:"Codex CLI",description:"OpenAI Codex CLI if installed; override in recipes.json",category:"agents",featured:!1},gemini:{command:'gemini -p "$OMNISH_TASK"',taskEnv:"OMNISH_TASK",label:"Gemini CLI",description:"Google Gemini CLI if installed; override in recipes.json",category:"agents",featured:!1},cursor:{command:"agent --yolo --force -p ```Before writing any code:\n1. Analyze the codebase and state your full implementation plan\n2. List every file you will touch\n3. Identify any risks or ambiguities\n4. Then execute the plan step by step, <task> $OMNISH_TASK </task>```",taskEnv:"OMNISH_TASK",label:"Cursor Agent",description:"Cursor Agent if installed; override in recipes.json",category:"agents",featured:!0}}}function us(e,t,n){for(let[o,r]of Object.entries(t)){let s=o.toLowerCase();!qa.test(s)||za.has(s)||r.command.trim()&&e.set(s,{...r,name:s,source:n})}}function Ga(e,t){let n=op(e),o={};for(let[r,s]of Object.entries(n)){let i=Ka(s,t);Tn(i).ok&&(o[r]=i)}return o}function rp(e,t){let n=new Map;return us(n,dw(t),"builtin"),us(n,uw(t),"global"),us(n,Ga(io,t),"shared"),us(n,Ga(e,t),"peer"),n}function Ye(e,t,n){let o=n.trim().toLowerCase();return rp(e,t).get(o)}function sp(e){let t=e.trim();if(/^--global$/i.test(t))return{mode:"global",remainder:""};if(/^-g$/i.test(t))return{mode:"global",remainder:""};if(/^--chat$/i.test(t))return{mode:"chat",remainder:""};if(/^-p$/i.test(t))return{mode:"chat",remainder:""};let n=/^--global\s+/i.exec(t);if(n)return{mode:"global",remainder:t.slice(n[0].length).trimStart()};let o=/^-g\s+/i.exec(t);if(o)return{mode:"global",remainder:t.slice(o[0].length).trimStart()};let r=/^--chat\s+/i.exec(t);if(r)return{mode:"chat",remainder:t.slice(r[0].length).trimStart()};let s=/^-p\s+/i.exec(t);return s?{mode:"chat",remainder:t.slice(s[0].length).trimStart()}:{mode:"resolved",remainder:t}}function Ya(e){let t=e.trim();if(/^--global$/i.test(t))return{scope:"global",remainder:"",explicit:!0};if(/^-g$/i.test(t))return{scope:"global",remainder:"",explicit:!0};if(/^--chat$/i.test(t))return{scope:"chat",remainder:"",explicit:!0};if(/^-p$/i.test(t))return{scope:"chat",remainder:"",explicit:!0};let n=/^--global\s+([\s\S]*)$/i.exec(t);if(n?.[1]!==void 0)return{scope:"global",remainder:n[1].trimStart(),explicit:!0};let o=/^-g\s+([\s\S]*)$/i.exec(t);if(o?.[1]!==void 0)return{scope:"global",remainder:o[1].trimStart(),explicit:!0};let r=/^--chat\s+([\s\S]*)$/i.exec(t);if(r?.[1]!==void 0)return{scope:"chat",remainder:r[1].trimStart(),explicit:!0};let s=/^-p\s+([\s\S]*)$/i.exec(t);return s?.[1]!==void 0?{scope:"chat",remainder:s[1].trimStart(),explicit:!0}:{scope:"chat",remainder:t,explicit:!1}}function Qa(e){let t=Ya(e);return{scope:t.scope,remainder:t.remainder}}function ip(e){let t=e.trim();return!t||/^-+$/i.test(t)?{filter:"merged"}:/^(?:--global|-g)$/i.test(t)?{filter:"global"}:/^(?:--chat|-p)$/i.test(t)?{filter:"chat"}:{filter:"merged",bad:t}}function pw(e){let t=e.trim().toLowerCase();if(t==="--global"||t==="-g")return"global";if(t==="--chat"||t==="-p")return"chat"}function ap(e){let t=e.trim().match(/^(\S+)\s+(--global|-g|--chat|-p)\s*$/i);if(!t?.[1]||!t[2])return;let n=pw(t[2]);if(n)return{name:t[1],target:n}}function _t(e,t,n,o){let r=o.trim().toLowerCase(),s=e==="global"?io:t,i=op(s)[r];if(i===void 0)return;let a=Ka(i,n);if(Tn(a).ok)return{...a,name:r,source:e==="global"?"shared":"peer"}}function lp(e,t,n){if(n==="merged")return[];let o=n==="global"?io:e,r=n==="global"?"shared":"peer",s=Ga(o,t);return Object.entries(s).map(([i,a])=>({...a,name:i,source:r})).sort((i,a)=>i.name.localeCompare(a.name))}function Ct(e){let t=e.trim();if(!t)return{ok:!1,error:"Name is empty."};let n=t.toLowerCase();return qa.test(n)?za.has(n)?{ok:!1,error:`Reserved name: ${n}`}:{ok:!0,normalized:n}:{ok:!1,error:`Invalid name (use letters, digits, _ or -; max 32 chars): ${t}`}}function ms(e,t){let n=e.replace(/\r\n/g,`
188
+ `).trim();return n?t>0&&n.length>t?{ok:!1,error:`Task too long (max ${t} characters).`}:{ok:!0,task:n}:{ok:!1,error:"Task is empty."}}function ao(e,t){return e.includes("$")?e.includes(t):!1}function Rn(e,t){let n=e.trim();if(!n)return{ok:!1,error:"Command is empty."};if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(t))return{ok:!1,error:"Invalid taskEnv (use letters, digits, _)."};if(!ao(n,t))return{ok:!1,error:`Command must reference "$${t}" so the task is passed via the environment.`};let o=`"$${t}"`;return n.includes(o)?n.includes("```")?{ok:!1,error:"Recipe command must not contain markdown code fences (```). Use promptTemplate or split with a --- line (see /run help)."}:{ok:!0}:{ok:!1,error:`Command must include ${o} (quoted).`}}function Tn(e){let t=e.taskEnv??"OMNISH_TASK",n=Rn(e.command,t);return n.ok?e.promptTemplate!==void 0&&e.promptTemplate.trim().length===0?{ok:!1,error:"promptTemplate cannot be empty."}:{ok:!0}:n}function mw(e){let t=e,n=null,o=!1,r=-1,s=-1;for(let l=0;l<t.length;l+=1){let c=t[l];if(o){o=!1;continue}if(c==="\\"){o=!0;continue}if(n){c===n&&(n=null);continue}if(c==='"'||c==="'"){n=c;continue}if(c!=="-"||t[l+1]!=="-")continue;let d=l===0?"":t[l-1];if(d&&!/\s/.test(d))continue;let u=t.slice(l+2),m=/^(template|tamplate)\b/i.exec(u);if(!m)continue;let h=l+2+m[0].length,f=t[h]??"";if(!f||!/\s/.test(f))continue;r=l;let g=h;for(;g<t.length&&/\s/.test(t[g]);)g+=1;s=g;break}if(r<0||s<0)return{command:t.trim()};let i=t.slice(0,r).trim(),a=t.slice(s).trim();return(a.startsWith('"')&&a.endsWith('"')&&a.length>=2||a.startsWith("'")&&a.endsWith("'")&&a.length>=2)&&(a=a.slice(1,-1)),{command:i,template:a}}function Va(e,t){let n=e.replace(/\r\n/g,`
189
+ `).trim();if(!n)throw new Error("Recipe body is empty.");let o=n.match(/^--steps\s+([\s\S]+)$/i);if(o){let m=o[1].trim().split(/\s*;\s*|\n/).filter(f=>f.trim());if(m.length===0)throw new Error("No steps found. Separate steps with ; or newlines.");let h=m.map(f=>{let g=f.startsWith("+"),y=g?f.slice(1).trim():f.trim();if(!y)throw new Error("Empty step in recipe.");return{cmd:y,...g?{continueOnFail:!0}:{}}});return Ke({command:h[0].cmd,steps:h})}let r="OMNISH_TASK",{command:s,template:i}=mw(n);if(i!==void 0){if(!s)throw new Error("Command part (before --template) is empty.");if(!i.trim())throw new Error("Template part (after --template) is empty.");let u=Rn(s,r);if(!u.ok)throw new Error(u.error);return Ke({command:s,promptTemplate:i})}let a=/\n---\n/,l=n.search(a);if(l>=0){let u=n.slice(0,l).trim(),m=n.slice(l).replace(/^\n---\n/,"").trim();if(!u)throw new Error("Command part (before ---) is empty.");if(!m)throw new Error("Template part (after ---) is empty.");let h=Rn(u,r);if(!h.ok)throw new Error(h.error);return Ke({command:u,promptTemplate:m})}if(Rn(n,r).ok)return Ke({command:n});let c=t.trim(),d=Rn(c,r);if(!d.ok)throw new Error(`recipesMacroDefaultCommand invalid (${d.error}). Fix config or paste runner then --- then template.`);return Ke({command:c,promptTemplate:n})}function hs(e,t,n){let o="`".repeat(3),r=`<<<${o}$${t}${o}>>>`,s=e;s.includes(r)&&(s=s.split(r).join(n));let i=`<<<${t}>>>`;s.includes(i)&&(s=s.split(i).join(n));let a=new RegExp(`\\$${t}\\b`,"g");return s.replace(a,n)}function lo(e,t,n,o="chat",r){let s=Ct(t);if(!s.ok)throw new Error(s.error);let i=Ke({...n,command:n.command});if(r?.skipCommandValidation){if(!i.command.trim())throw new Error("Command is empty.")}else{let c=Tn(i);if(!c.ok)throw new Error(c.error)}let a=o==="global"?io:e,l=ja(a);l[s.normalized]=i,ds()}function cp(e,t,n="chat"){let o=t.trim().toLowerCase(),r=n==="global"?io:e;ps();let s=so.get(r);return!s||!(o in s)?!1:(delete s[o],ds(),!0)}function Xa(e,t,n,o){let r=Ct(t);if(!r.ok)return{ok:!1,error:r.error};let s=r.normalized,i=e,a=io;ps();let l=ja(i),c=ja(a),d=l[s],u=c[s],m=h=>{let f=Ke({...h}),g=Tn(f);if(!g.ok)throw new Error(g.error);return f};try{if(n==="global"){if(d!==void 0){let g=m(d);return c[s]=g,delete l[s],ds(),{ok:!0,kind:"moved",target:"global",name:s}}if(u!==void 0)return{ok:!0,kind:"noop",message:`Recipe "${s}" is already gateway-shared.`};let f=Ye(e,o,s);return f?.source==="builtin"||f?.source==="global"?{ok:!1,error:`Recipe "${s}" is built-in or from host recipes.json \u2014 not moved via /run set. Edit recipes.json or use a different name.`}:{ok:!1,error:`No user recipe "${s}" in this chat to promote. Add with /run add ${s} \u2026 or demote from global with /run set -p ${s}.`}}if(u!==void 0){let f=m(u);return l[s]=f,delete c[s],ds(),{ok:!0,kind:"moved",target:"chat",name:s}}if(d!==void 0)return{ok:!0,kind:"noop",message:`Recipe "${s}" is already stored for this chat only.`};let h=Ye(e,o,s);return h?.source==="builtin"||h?.source==="global"?{ok:!1,error:`Recipe "${s}" is built-in or from host recipes.json \u2014 not moved via /run set.`}:{ok:!1,error:`No gateway-shared user recipe "${s}" to demote. Add with /run add --global ${s} \u2026 or promote from this chat with /run set -g ${s}.`}}catch(h){return{ok:!1,error:String(h)}}}function up(e,t){let n=[...rp(e,t).values()],o=n.filter(a=>a.featured).sort((a,l)=>a.name.localeCompare(l.name)),r=n.filter(a=>a.source==="peer").sort((a,l)=>a.name.localeCompare(l.name)),s=n.filter(a=>a.source==="shared").sort((a,l)=>a.name.localeCompare(l.name)),i=n.filter(a=>!a.featured&&a.source!=="peer"&&a.source!=="shared").sort((a,l)=>a.name.localeCompare(l.name));return{featured:o,shared:s,yours:r,more:i}}function fs(e){let t=rw.randomBytes(4).toString("hex");return`r-${e.replace(/[^a-zA-Z0-9_-]/g,"").slice(0,12)}-${t}`.slice(0,32)}function dp(e,t,n,o){let r=t.skipped?"skip":t.exitCode===0?"ok":"FAIL",s=t.label||`step ${n+1}`,i=`[${r}] ${e} ${n+1}/${o}: ${s}`;if(t.skipped)return i;let a=t.timedOut?" (timeout)":t.exitCode!==0?` (exit ${t.exitCode})`:"",l=t.output.trim();if(!l)return`${i}${a}`;let c=l.length>300?l.slice(0,300)+"\u2026":l;return`${i}${a}
190
+ ${c}`}function pp(e,t){let n=[`runbook: ${e}`];for(let r of t){let s=r.skipped?"skip":r.exitCode===0?"ok":"FAIL",i=r.label||`step ${r.index+1}`;if(r.skipped)n.push(`[${s}] ${i}`);else{n.push(`[${s}] ${i}${r.timedOut?" (timeout)":r.exitCode!==0?` (exit ${r.exitCode})`:""}`);let a=r.output.trim();if(a){let l=a.length>300?a.slice(0,300)+"\u2026":a;n.push(l)}}}let o=t.every(r=>r.skipped||r.exitCode===0);return n.push(o?"All steps completed successfully.":"Runbook stopped on failure."),n.join(`
191
+ `)}q();var mp={enter:"\r",cr:"\r",lf:`
192
+ `,return:"\r",tab:" ",esc:"\x1B",escape:"\x1B",space:" ",backspace:"\x7F",bs:"\x7F",delete:"\x1B[3~",up:"\x1B[A",down:"\x1B[B",right:"\x1B[C",left:"\x1B[D",home:"\x1B[H",end:"\x1B[F",pageup:"\x1B[5~",pagedown:"\x1B[6~","ctrl+c":"","ctrl+d":"","ctrl+z":"","ctrl+l":"\f","ctrl+u":"","ctrl+k":"\v"};function hw(e){let t="";for(let n=0;n<e.length;n++){let o=e[n];if(o==="\\"&&n+1<e.length){let r=e[n+1];if(r==="x"&&n+3<e.length){let s=e.slice(n+2,n+4);if(/^[0-9a-fA-F]{2}$/.test(s)){t+=String.fromCharCode(Number.parseInt(s,16)),n+=3;continue}}if(r==="r"){t+="\r",n++;continue}if(r==="n"){t+=`
193
+ `,n++;continue}if(r==="t"){t+=" ",n++;continue}if(r==="\\"){t+="\\",n++;continue}}t+=o}return t}function fw(e){let t=e.trim();if(!t)return"";if(t.startsWith("\\"))return hw(t);let n=t.toLowerCase();if(mp[n])return mp[n];let o=t.match(/^\^([A-Za-z])$/);if(o){let s=o[1].toUpperCase().charCodeAt(0);if(s>=64&&s<=95)return String.fromCharCode(s-64)}let r=n.match(/^ctrl\+(.+)$/);if(r){let s=r[1];if(s.length===1){let i=s.toUpperCase().charCodeAt(0);if(i>=64&&i<=95)return String.fromCharCode(i-64)}}return t}function gs(e){let t=e.split(",").map(n=>n.trim()).filter(Boolean);return t.length===0?"":t.map(n=>fw(n)).join("")}var Ko="\x1B";function ys(e){let t=e;return t=t.replace(new RegExp(`${Ko}\\[[\\d;?]*[ -/]*[@-~]`,"g"),""),t=t.replace(new RegExp(`${Ko}\\][^${Ko}\\u0007]*(?:\\u0007|${Ko}\\\\)`,"g"),""),t=t.replace(new RegExp(`${Ko}[@-Z\\\\-_]`,"g"),""),t=t.replace(/\u0007/g,""),t}function gw(e,t){if(e.length<=t)return e?[e]:[];let n=[];for(let o=0;o<e.length;o+=t)n.push(e.slice(o,o+t));return n}function yw(e,t){return`${e}${t}`}var ws=class{constructor(t){this.opts=t}pending=new Map;timers=new Map;lastFlushEnd=new Map;flushing=new Set;closed=!1;dispose(){this.closed=!0;for(let t of this.timers.values())clearTimeout(t);this.timers.clear(),this.pending.clear(),this.lastFlushEnd.clear(),this.flushing.clear()}push(t,n,o){if(this.closed||this.opts.isMuted(t,n))return;let r=yw(t,n);this.pending.set(r,(this.pending.get(r)??"")+o);let s=this.timers.get(r);s&&clearTimeout(s);let i=this.opts.getCfg().appsFlushMs,a=setTimeout(()=>{this.timers.delete(r),this.flushNow(r,t,n)},i);this.timers.set(r,a)}async flushNow(t,n,o){if(!(this.closed||this.flushing.has(t))){this.flushing.add(t);try{let r=this.opts.getCfg(),s=r.appsMinIntervalMs,i=this.lastFlushEnd.get(t)??0,a=Math.max(0,i+s-Date.now());if(a>0&&await new Promise(h=>setTimeout(h,a)),this.closed)return;let l=this.pending.get(t)??"";if(this.pending.delete(t),!l||(this.opts.isRaw(n,o)||(l=ys(l)),!l.trim()))return;let c=r.appsMaxFlushBytes;if(l.length>c){let h=l.slice(c);l=l.slice(0,c)+`
194
+ [\u2026+${h.length} chars; /apps since ${o} to read more]`,this.pending.set(t,(this.pending.get(t)??"")+h)}let u=(this.opts.getRunningCount(n)>1?`[${o}] `:"")+l,m=gw(u,r.appsMaxWaChars);for(let h of m){if(this.closed)break;try{await this.opts.send(n,h)}catch{break}}this.lastFlushEnd.set(t,Date.now())}finally{this.flushing.delete(t),!this.closed&&(this.pending.get(t)??"").length>0&&queueMicrotask(()=>void this.flushNow(t,n,o))}}}};var bs=4096,ww=/\[sudo\]\s+password\s+for\s+/i,bw=/passphrase\s+for\s+/i,kw=/(?:^|\n)\s*(?:Password|password):\s*$/;function ks(e){let n=ys(e).replace(/\r/g,"").slice(-512);return!!(ww.test(n)||bw.test(n)||kw.test(n))}q();import vw from"node:fs";import Sw from"node:path";import*as hp from"node-pty";var xw=1024*1024,vs=class e{peerKey;name;command;cwd;envKeysCount;logPath;term=null;logStream=null;disposeData=null;disposeExit=null;exited=!1;ringChunks=[];ringBytes=0;constructor(t){this.peerKey=t.peerKey,this.name=t.name,this.command=t.command,this.cwd=t.cwd,this.envKeysCount=t.envKeysCount,this.logPath=t.logPath}appendRing(t){for(this.ringChunks.push(t),this.ringBytes+=t.length;this.ringBytes>xw&&this.ringChunks.length>0;){let n=this.ringChunks.shift();this.ringBytes-=n.length}}static start(t){H(Mo(t.peerKey));let n=Sw.join(Mo(t.peerKey),`${t.name}.log`),o=vw.createWriteStream(n,{flags:"a",mode:384}),r={...process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...t.cwd?{PWD:t.cwd}:{},...t.extraEnv??{}},s=hp.spawn(t.shell,["-lc",t.command],{name:"xterm-256color",cols:t.cols,rows:t.rows,cwd:t.cwd,env:r}),i=new e({...t,logPath:n});return i.term=s,i.logStream=o,i.disposeData=s.onData(a=>{let l=Buffer.from(a,"utf8");i.appendRing(l),o.write(a),t.onOutput?.(a),t.router.push(t.peerKey,t.name,a)}),i.disposeExit=s.onExit(a=>{i.exited||(i.exited=!0,i.cleanupTerm(),t.onExit({exitCode:a.exitCode,signal:a.signal}))}),i}get alive(){return this.term!==null&&!this.exited}get ringByteCount(){return this.ringBytes}recentOutputTail(t=4096){if(this.ringBytes===0)return"";let n=Math.max(1,t),o=[],r=Math.min(n,this.ringBytes);for(let s=this.ringChunks.length-1;s>=0&&r>0;s--){let i=this.ringChunks[s];i.length<=r?(o.unshift(i),r-=i.length):(o.unshift(i.subarray(i.length-r)),r=0)}return Buffer.concat(o).toString("utf8")}write(t){this.term?.write(t)}resize(t,n){this.term?.resize(t,n)}kill(t){if(this.term)try{this.term.kill(t)}catch{}}cleanupTerm(){this.disposeData?.dispose(),this.disposeData=null,this.disposeExit?.dispose(),this.disposeExit=null,this.term=null,this.logStream?.end(),this.logStream=null}destroy(){if(this.exited){this.cleanupTerm();return}this.exited=!0,this.kill("SIGHUP"),this.cleanupTerm()}};function Za(e){return new Promise(t=>setTimeout(t,e))}async function Ss(e,t,n){let o=n.appsSubmitDelayMs,r=n.appsClearInputDelayMs,s=n.appsClearInput!==!1,a=n.appsSkipClearOnPasswordPrompt!==!1&&ks(e.recentOutputTail(bs)),l=s&&!a?gs(n.appsClearInputSequence||"^A,^K"):"",d=t.replace(/\r\n/g,`
177
195
  `).replace(/\r/g,"").split(`
178
- `);l&&(r>0&&await $a(r),e.write(l));for(let c of u)c.length>0&&e.write(c),o>0&&await $a(o),e.write("\r"),l&&(r>0&&await $a(r),e.write(l))}var ly=/^[a-zA-Z0-9_-]{1,32}$/;function je(e){return ly.test(e)?null:"Session name must be 1\u201332 chars: letters, digits, _ or -."}function Fe(e,t){return`${e}:${t}`}function cy(e){let t=e.lastIndexOf(":");return t<=0?null:{peerKey:e.slice(0,t),name:e.slice(t+1)}}function uy(e){return/\bstarted\b/i.test(e)&&!/^Session "/.test(e)&&!/^Per-chat app/.test(e)&&!/^Global app/.test(e)}function Ma(e,t){let n=e??t.recipesRunAttach;return{attach:n,mute:!n}}function dy(e,t){return e===0&&(t===0||t==null)}function py(e,t){try{let o=Zt.readFileSync(e,"utf8").split(/\r?\n/);return o.slice(Math.max(0,o.length-t)).join(`
179
- `).trimEnd()}catch{return""}}var en=class{constructor(t,n){this.getCfg=t;this.send=n,this.router=new Zr({getCfg:()=>this.getCfg(),send:n,isMuted:(o,r)=>this.muted.has(Fe(o,r)),isRaw:(o,r)=>this.rawMode.has(Fe(o,r)),getRunningCount:o=>this.countPeerRunning(o)})}sessions=new Map;focus=new Map;muted=new Set;rawMode=new Set;router;killTimers=new Set;send;runQueue=new Map;activeQueuedRunHead=new Map;runQueuePaused=new Map;passwordHintSent=new Set;countPeerRunning(t){let n=`${t}:`,o=0;for(let[r,s]of this.sessions)r.startsWith(n)&&s.alive&&o++;return o}countTotalRunning(){let t=0;for(let n of this.sessions.values())n.alive&&t++;return t}dispose(){for(let t of this.killTimers)clearTimeout(t);this.killTimers.clear(),this.stopAll(),this.router.dispose()}stopAll(){for(let t of this.sessions.values())t.destroy();this.sessions.clear(),this.focus.clear(),this.runQueue.clear(),this.activeQueuedRunHead.clear(),this.runQueuePaused.clear()}getFocus(t){return this.focus.get(t)??null}logPath(t,n){return ay.join(yo(t),`${n}.log`)}getSession(t,n){return this.sessions.get(Fe(t,n))}removeSessionRecord(t,n){let o=Fe(t,n);this.sessions.delete(o),this.passwordHintSent.delete(o),this.focus.get(t)===n&&this.focus.set(t,null)}onSessionOutput(t,n){let o=Fe(t,n),r=this.getSession(t,n);if(!r?.alive)return;let s=this.getCfg(),i=r.recentOutputTail(es);if(!ts(i)){this.passwordHintSent.delete(o);return}if(s.appsPasswordPromptHint===!1||this.passwordHintSent.has(o))return;this.passwordHintSent.add(o);let l=`Password prompt in app "${n}" \u2014 reply with your password only (no line-clear keys). It is not echoed in chat but is written to the session log.`;this.send(t,l).catch(()=>{})}async writeFocusedLine(t,n){let o=this.focus.get(t);if(!o)return!1;let r=this.getSession(t,o);return r?.alive?(await os(r,n,this.getCfg()),!0):!1}async writeNamedLine(t,n,o){let r=je(n);if(r)return r;let s=this.getSession(t,n);return s?.alive?(await os(s,o,this.getCfg()),null):`No app session "${n}". /apps list`}enqueueQueuedRun(t,n,o){let r=this.runQueue.get(t)??[];r.push(n),this.runQueue.set(t,r);let s=r.length,i=this.runQueuePaused.get(t)??!1,a=this.activeQueuedRunHead.get(t)??null,l=a?.sessionName??null;if(l?this.getSession(t,l)?.alive??!1:!1){let u=a?.recipeLabel?` (recipe: ${a.recipeLabel})`:"",c=s-1,m=c>0?`${c} other job(s) in line before this one.`:"Next in line after the active run finishes.";return`Queued "${n.recipeLabel}" (wait slot ${s} behind active ${l}${u}). ${m}`}return i?`Queued "${n.recipeLabel}" (position ${s}). Run queue is paused \u2014 /run queue resume to continue.`:this.drainNextQueuedRun(t,o)}resumeRunQueue(t,n){this.runQueuePaused.set(t,!1);let o=this.activeQueuedRunHead.get(t)??null,r=o?.sessionName??null;if(r?this.getSession(t,r)?.alive??!1:!1){let a=o?.recipeLabel?` (recipe: ${o.recipeLabel})`:"";return`Run queue: session "${r}"${a} is still running. Wait for exit code=0 and signal=0 before the next starts.`}return(this.runQueue.get(t)??[]).length===0?"Run queue is empty.":(this.activeQueuedRunHead.set(t,null),this.drainNextQueuedRun(t,n))}runQueueStatus(t){let n=this.runQueue.get(t)??[],o=this.activeQueuedRunHead.get(t)??null,r=o?.sessionName??null,s=this.runQueuePaused.get(t)??!1,i=r?this.getSession(t,r)?.alive??!1:!1,a=["Run queue (this chat)"];if(r&&i){let l=o?.recipeLabel?` \xB7 recipe: ${o.recipeLabel}`:"";a.push(`Active: ${r}${l}`)}else r?a.push(`Active: ${r} (exiting or stale)`):a.push("Active: (none)");if(a.push(`Pending: ${n.length} (waiting only \u2014 not counting the active run above)`),a.push(`Paused: ${s}`),n.length>0){a.push("Waiting (FIFO order):");for(let d=0;d<Math.min(n.length,20);d++)a.push(`${d+1}) ${n[d].recipeLabel}`);n.length>20&&a.push(`\u2026 and ${n.length-20} more`)}return a.push("Next auto-starts only after exit code=0 and signal=0."),a.push("Commands: /run queue resume"),a.join(`
180
- `)}drainNextQueuedRun(t,n){let o=this.runQueue.get(t);if(!o||o.length===0)return"";let r=o.shift();o.length===0?this.runQueue.delete(t):this.runQueue.set(t,o);let s=Qr(r.recipeLabel),i=this.runQueue.get(t)?.length??0,a=r.startOptions??Ma(null,n),l=this.start(t,s,r.command,n,r.extraEnv,a);if(!uy(l))return o.unshift(r),this.runQueue.set(t,o),this.runQueuePaused.set(t,!0),this.activeQueuedRunHead.set(t,null),`${l}
181
- Run queue paused; fix the issue, then /run queue resume.`;this.activeQueuedRunHead.set(t,{sessionName:s,recipeLabel:r.recipeLabel});let u=i>0?`
196
+ `);l&&(r>0&&await Za(r),e.write(l));for(let u of d)u.length>0&&e.write(u),o>0&&await Za(o),e.write("\r"),l&&(r>0&&await Za(r),e.write(l))}var Rw=/^[a-zA-Z0-9_-]{1,32}$/;function Ge(e){return Rw.test(e)?null:"Session name must be 1\u201332 chars: letters, digits, _ or -."}function Fe(e,t){return`${e}:${t}`}function Tw(e){let t=e.lastIndexOf(":");return t<=0?null:{peerKey:e.slice(0,t),name:e.slice(t+1)}}function $w(e){return/\bstarted\b/i.test(e)&&!/^Session "/.test(e)&&!/^Per-chat app/.test(e)&&!/^Global app/.test(e)}function el(e,t){let n=e??t.recipesRunAttach;return{attach:n,mute:!n}}function Mw(e,t){return e===0&&(t===0||t==null)}function Pw(e,t){try{let o=ln.readFileSync(e,"utf8").split(/\r?\n/);return o.slice(Math.max(0,o.length-t)).join(`
197
+ `).trimEnd()}catch{return""}}var cn=class{constructor(t,n){this.getCfg=t;this.send=n,this.router=new ws({getCfg:()=>this.getCfg(),send:n,isMuted:(o,r)=>this.muted.has(Fe(o,r)),isRaw:(o,r)=>this.rawMode.has(Fe(o,r)),getRunningCount:o=>this.countPeerRunning(o)})}sessions=new Map;focus=new Map;muted=new Set;rawMode=new Set;router;killTimers=new Set;send;runQueue=new Map;activeQueuedRunHead=new Map;runQueuePaused=new Map;passwordHintSent=new Set;countPeerRunning(t){let n=`${t}:`,o=0;for(let[r,s]of this.sessions)r.startsWith(n)&&s.alive&&o++;return o}countTotalRunning(){let t=0;for(let n of this.sessions.values())n.alive&&t++;return t}dispose(){for(let t of this.killTimers)clearTimeout(t);this.killTimers.clear(),this.stopAll(),this.router.dispose()}stopAll(){for(let t of this.sessions.values())t.destroy();this.sessions.clear(),this.focus.clear(),this.runQueue.clear(),this.activeQueuedRunHead.clear(),this.runQueuePaused.clear()}getFocus(t){return this.focus.get(t)??null}logPath(t,n){return Cw.join(Mo(t),`${n}.log`)}getSession(t,n){return this.sessions.get(Fe(t,n))}removeSessionRecord(t,n){let o=Fe(t,n);this.sessions.delete(o),this.passwordHintSent.delete(o),this.focus.get(t)===n&&this.focus.set(t,null)}onSessionOutput(t,n){let o=Fe(t,n),r=this.getSession(t,n);if(!r?.alive)return;let s=this.getCfg(),i=r.recentOutputTail(bs);if(!ks(i)){this.passwordHintSent.delete(o);return}if(s.appsPasswordPromptHint===!1||this.passwordHintSent.has(o))return;this.passwordHintSent.add(o);let l=`Password prompt in app "${n}" \u2014 reply with your password only (no line-clear keys). It is not echoed in chat but is written to the session log.`;this.send(t,l).catch(()=>{})}async writeFocusedLine(t,n){let o=this.focus.get(t);if(!o)return!1;let r=this.getSession(t,o);return r?.alive?(await Ss(r,n,this.getCfg()),!0):!1}async writeNamedLine(t,n,o){let r=Ge(n);if(r)return r;let s=this.getSession(t,n);return s?.alive?(await Ss(s,o,this.getCfg()),null):`No app session "${n}". /apps list`}enqueueQueuedRun(t,n,o){let r=this.runQueue.get(t)??[];r.push(n),this.runQueue.set(t,r);let s=r.length,i=this.runQueuePaused.get(t)??!1,a=this.activeQueuedRunHead.get(t)??null,l=a?.sessionName??null;if(l?this.getSession(t,l)?.alive??!1:!1){let d=a?.recipeLabel?` (recipe: ${a.recipeLabel})`:"",u=s-1,m=u>0?`${u} other job(s) in line before this one.`:"Next in line after the active run finishes.";return`Queued "${n.recipeLabel}" (wait slot ${s} behind active ${l}${d}). ${m}`}return i?`Queued "${n.recipeLabel}" (position ${s}). Run queue is paused \u2014 /run queue resume to continue.`:this.drainNextQueuedRun(t,o)}resumeRunQueue(t,n){this.runQueuePaused.set(t,!1);let o=this.activeQueuedRunHead.get(t)??null,r=o?.sessionName??null;if(r?this.getSession(t,r)?.alive??!1:!1){let a=o?.recipeLabel?` (recipe: ${o.recipeLabel})`:"";return`Run queue: session "${r}"${a} is still running. Wait for exit code=0 and signal=0 before the next starts.`}return(this.runQueue.get(t)??[]).length===0?"Run queue is empty.":(this.activeQueuedRunHead.set(t,null),this.drainNextQueuedRun(t,n))}runQueueStatus(t){let n=this.runQueue.get(t)??[],o=this.activeQueuedRunHead.get(t)??null,r=o?.sessionName??null,s=this.runQueuePaused.get(t)??!1,i=r?this.getSession(t,r)?.alive??!1:!1,a=["Run queue (this chat)"];if(r&&i){let l=o?.recipeLabel?` \xB7 recipe: ${o.recipeLabel}`:"";a.push(`Active: ${r}${l}`)}else r?a.push(`Active: ${r} (exiting or stale)`):a.push("Active: (none)");if(a.push(`Pending: ${n.length} (waiting only \u2014 not counting the active run above)`),a.push(`Paused: ${s}`),n.length>0){a.push("Waiting (FIFO order):");for(let c=0;c<Math.min(n.length,20);c++)a.push(`${c+1}) ${n[c].recipeLabel}`);n.length>20&&a.push(`\u2026 and ${n.length-20} more`)}return a.push("Next auto-starts only after exit code=0 and signal=0."),a.push("Commands: /run queue resume"),a.join(`
198
+ `)}drainNextQueuedRun(t,n){let o=this.runQueue.get(t);if(!o||o.length===0)return"";let r=o.shift();o.length===0?this.runQueue.delete(t):this.runQueue.set(t,o);let s=fs(r.recipeLabel),i=this.runQueue.get(t)?.length??0,a=r.startOptions??el(null,n),l=this.start(t,s,r.command,n,r.extraEnv,a);if(!$w(l))return o.unshift(r),this.runQueue.set(t,o),this.runQueuePaused.set(t,!0),this.activeQueuedRunHead.set(t,null),`${l}
199
+ Run queue paused; fix the issue, then /run queue resume.`;this.activeQueuedRunHead.set(t,{sessionName:s,recipeLabel:r.recipeLabel});let d=i>0?`
182
200
  Run queue: started head above; ${i} job(s) still waiting in FIFO.`:`
183
- Run queue: started head above; no further queued jobs.`;return`${l}${u}`}start(t,n,o,r,s,i){let a=je(n);if(a)return a;if(!o.trim())return`Usage: /apps start <name> <command\u2026>
184
- Example: /apps start sh bash`;if(this.sessions.has(Fe(t,n)))return`Session "${n}" already exists. /apps stop ${n} or pick another name.`;if(this.countPeerRunning(t)>=r.appsMaxSessions)return`Per-chat app limit (${r.appsMaxSessions}) reached. /apps stop or /apps rm an old session.`;if(this.countTotalRunning()>=r.appsMaxSessionsTotal)return`Global app limit (${r.appsMaxSessionsTotal}) reached. Stop sessions in other chats first.`;se();let l=ie(t).cwd,d={...process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...l?{PWD:l}:{},...s??{}},u;try{u=ns.start({peerKey:t,name:n,shell:r.shell,command:o.trim(),cwd:l,cols:r.appsCols,rows:r.appsRows,envKeysCount:Object.keys(d).length,extraEnv:s,router:this.router,onOutput:()=>{this.onSessionOutput(t,n)},onExit:g=>{this.handleSessionExit(t,n,g)}})}catch(g){return`App spawn failed: ${String(g)}
185
- If install skipped native builds: pnpm approve-builds && pnpm install (needs @whiskeysockets/baileys, sharp, protobufjs, esbuild, node-pty).`}let c=Fe(t,n);this.sessions.set(c,u);let m=i?.attach!==!1,h=i?.mute??!m;if(m&&this.focus.set(t,n),h&&this.muted.add(c),m)return`App "${n}" started and attached.
201
+ Run queue: started head above; no further queued jobs.`;return`${l}${d}`}start(t,n,o,r,s,i){let a=Ge(n);if(a)return a;if(!o.trim())return`Usage: /apps start <name> <command\u2026>
202
+ Example: /apps start sh bash`;if(this.sessions.has(Fe(t,n)))return`Session "${n}" already exists. /apps stop ${n} or pick another name.`;if(this.countPeerRunning(t)>=r.appsMaxSessions)return`Per-chat app limit (${r.appsMaxSessions}) reached. /apps stop or /apps rm an old session.`;if(this.countTotalRunning()>=r.appsMaxSessionsTotal)return`Global app limit (${r.appsMaxSessionsTotal}) reached. Stop sessions in other chats first.`;ie();let l=ae(t).cwd,c={...process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...l?{PWD:l}:{},...s??{}},d;try{d=vs.start({peerKey:t,name:n,shell:r.shell,command:o.trim(),cwd:l,cols:r.appsCols,rows:r.appsRows,envKeysCount:Object.keys(c).length,extraEnv:s,router:this.router,onOutput:()=>{this.onSessionOutput(t,n)},onExit:g=>{this.handleSessionExit(t,n,g)}})}catch(g){return`App spawn failed: ${String(g)}
203
+ If install skipped native builds: pnpm approve-builds && pnpm install (needs @whiskeysockets/baileys, sharp, protobufjs, esbuild, node-pty).`}let u=Fe(t,n);this.sessions.set(u,d);let m=i?.attach!==!1,h=i?.mute??!m;if(m&&this.focus.set(t,n),h&&this.muted.add(u),m)return`App "${n}" started and attached.
186
204
  [cwd: ${l}]
187
205
  Plain DMs go to this session until /apps detach. Use >othername text for another session.`;let f=h?`
188
206
  Output muted \u2014 /apps unmute `+n+" or /apps attach "+n+" to stream to chat.":"";return`App "${n}" started (detached).
189
207
  [cwd: ${l}]`+f+`
190
208
  /apps attach ${n} \u2014 focus for plain DMs
191
209
  >${n} <text> \u2014 send a line
192
- /apps tail ${n} \u2014 recent output`}async handleSessionExit(t,n,o){let r=Fe(t,n);if(!this.sessions.has(r))return;let s=this.activeQueuedRunHead.get(t)?.sessionName===n,i=this.focus.get(t)===n;this.sessions.delete(r),i&&this.focus.set(t,null),this.muted.delete(r),this.rawMode.delete(r),this.passwordHintSent.delete(r);let a=o.signal!=null?` signal=${o.signal}`:"",l=i?" (detached)":"",d=`[${n}] exited code=${o.exitCode}${a}${l}`;try{await this.send(t,d)}catch{}if(!s)return;this.activeQueuedRunHead.set(t,null);let u=this.getCfg(),c=dy(o.exitCode,o.signal),m=this.runQueue.get(t)?.length??0;if(c){if(this.runQueuePaused.set(t,!1),m>0){let h=this.drainNextQueuedRun(t,u);if(h)try{await this.send(t,h)}catch{}}return}if(this.runQueuePaused.set(t,!0),m>0){let h=`Run queue paused (exit code=${o.exitCode}${o.signal!=null?` signal=${o.signal}`:""}). ${m} run(s) waiting. /run queue resume to continue.`;try{await this.send(t,h)}catch{}}}attach(t,n){let o=je(n);return o||(this.getSession(t,n)?.alive?(this.focus.set(t,n),`Attached to "${n}". Plain messages (no ! or / or >) go to this app. /apps detach to stop.`):`No session "${n}". /apps list`)}detach(t){return this.focus.set(t,null),"Detached (attach mode off)."}list(t){let n=[];for(let[s,i]of this.sessions){let a=cy(s);if(!a||a.peerKey!==t||!i.alive)continue;let l=this.focus.get(t)===a.name?" *":"";n.push(`${a.name}${l}`)}if(n.length===0)return"(no app sessions; /apps start <name> <cmd>)";let o=this.focus.get(t);return`${o?`attached: ${o}`:"(no focus)"}
210
+ /apps tail ${n} \u2014 recent output`}async handleSessionExit(t,n,o){let r=Fe(t,n);if(!this.sessions.has(r))return;let s=this.activeQueuedRunHead.get(t)?.sessionName===n,i=this.focus.get(t)===n;this.sessions.delete(r),i&&this.focus.set(t,null),this.muted.delete(r),this.rawMode.delete(r),this.passwordHintSent.delete(r);let a=o.signal!=null?` signal=${o.signal}`:"",l=i?" (detached)":"",c=`[${n}] exited code=${o.exitCode}${a}${l}`;try{await this.send(t,c)}catch{}if(!s)return;this.activeQueuedRunHead.set(t,null);let d=this.getCfg(),u=Mw(o.exitCode,o.signal),m=this.runQueue.get(t)?.length??0;if(u){if(this.runQueuePaused.set(t,!1),m>0){let h=this.drainNextQueuedRun(t,d);if(h)try{await this.send(t,h)}catch{}}return}if(this.runQueuePaused.set(t,!0),m>0){let h=`Run queue paused (exit code=${o.exitCode}${o.signal!=null?` signal=${o.signal}`:""}). ${m} run(s) waiting. /run queue resume to continue.`;try{await this.send(t,h)}catch{}}}attach(t,n){let o=Ge(n);return o||(this.getSession(t,n)?.alive?(this.focus.set(t,n),`Attached to "${n}". Plain messages (no ! or / or >) go to this app. /apps detach to stop.`):`No session "${n}". /apps list`)}detach(t){return this.focus.set(t,null),"Detached (attach mode off)."}list(t){let n=[];for(let[s,i]of this.sessions){let a=Tw(s);if(!a||a.peerKey!==t||!i.alive)continue;let l=this.focus.get(t)===a.name?" *":"";n.push(`${a.name}${l}`)}if(n.length===0)return"(no app sessions; /apps start <name> <cmd>)";let o=this.focus.get(t);return`${o?`attached: ${o}`:"(no focus)"}
193
211
  App sessions:
194
212
  ${n.join(`
195
- `)}`}getSessionCommand(t,n){if(je(n))return null;let r=this.getSession(t,n);return r?.command?.trim()?r.command.trim():null}info(t,n){let o=this.getCfg(),r=n??this.focus.get(t)??"";if(!r)return"Usage: /apps info <name> (or /apps get <name>, or attach first)";let s=je(r);if(s)return s;let i=this.getSession(t,r),a=this.logPath(t,r),l=0;try{l=Zt.statSync(a).size}catch{}let d=this.muted.has(Fe(t,r)),u=this.rawMode.has(Fe(t,r));return[`session: ${r}`,`alive: ${i?.alive??!1}`,`cmd: ${i?.command??"(unknown)"}`,`cwd: ${i?.cwd??"(unknown)"}`,`env keys: ${i?.envKeysCount??"?"}`,`terminal size: ${o.appsCols}x${o.appsRows}`,`ring bytes (approx): ${i?.ringByteCount??0}`,`log: ${a} (${l} bytes)`,`mute outbound: ${d}`,`raw outbound: ${u}`].join(`
196
- `)}async sendText(t,n,o){let r=je(n);if(r)return r;let s=this.getSession(t,n);if(!s?.alive)return`No session "${n}".`;let i=o.replace(/\r\n/g,`
197
- `);return await os(s,i,this.getCfg()),`Sent to "${n}" (${i.length} chars + Enter per line).`}sendKey(t,n,o){let r=je(n);if(r)return r;let s=this.getSession(t,n);if(!s?.alive)return`No session "${n}".`;let i=Vr(o);return i?(s.write(i),`Sent keys to "${n}".`):"Empty key sequence. Example: /apps key sh Enter or ^C,Up,Enter"}tail(t,n,o){let r=je(n);if(r)return r;let s=this.logPath(t,n);if(!Zt.existsSync(s))return`(no log file for ${n})`;let i=this.getCfg(),a=Number.isFinite(o)&&o>0?Math.min(500,o):i.appsLogTailLines;return py(s,a)||"(empty log)"}readSince(t,n,o){let r=this.logPath(t,n);try{let i=Zt.statSync(r).size,a=Zt.openSync(r,"r");try{let l=Math.min(o,i),d=i-l,u=Buffer.alloc(d);return d>0&&Zt.readSync(a,u,0,d,l),{text:u.toString("utf8"),nextOffset:i}}finally{Zt.closeSync(a)}}catch{return{text:"",nextOffset:o}}}mute(t,n){let o=je(n);return o||(this.muted.add(Fe(t,n)),`Muted chat output for "${n}" (log still grows). /apps unmute ${n}`)}unmute(t,n){let o=je(n);return o||(this.muted.delete(Fe(t,n)),`Unmuted "${n}".`)}setRaw(t,n,o){let r=je(n);if(r)return r;let s=Fe(t,n);return o?this.rawMode.add(s):this.rawMode.delete(s),`raw chat output for "${n}": ${o?"on (ANSI kept)":"off (stripped)"}.`}resize(t,n,o,r){let s=je(n);if(s)return s;let i=this.getSession(t,n);if(!i?.alive)return`No session "${n}".`;let a=Math.min(500,Math.max(20,Math.floor(o))),l=Math.min(200,Math.max(5,Math.floor(r)));return i.resize(a,l),`Resized "${n}" to ${a}x${l}.`}stop(t,n){let o=je(n);if(o)return o;let r=this.getSession(t,n);if(!r?.alive)return`No running session "${n}".`;r.kill("SIGTERM");let s=Fe(t,n),i=setTimeout(()=>{this.killTimers.delete(i);let a=this.getSession(t,n);a?.alive&&a.kill("SIGKILL")},5e3);return this.killTimers.add(i),`SIGTERM sent to "${n}". SIGKILL in 5s if still running.`}kill(t,n){let o=je(n);if(o)return o;let r=this.getSession(t,n);return r?.alive?(r.kill("SIGKILL"),`SIGKILL sent to "${n}".`):`No running session "${n}".`}rm(t,n){let o=je(n);if(o)return o;if(this.getSession(t,n)?.alive)return`Session "${n}" is still running. /apps stop ${n} first, then /apps rm ${n}.`;this.removeSessionRecord(t,n),this.muted.delete(Fe(t,n)),this.rawMode.delete(Fe(t,n)),this.passwordHintSent.delete(Fe(t,n));let s=this.logPath(t,n);try{Zt.rmSync(s,{force:!0})}catch{}return`Removed "${n}" metadata and log.`}};Xe();xe();import Wo from"node:fs";import Do from"node:path";import Td from"node:fs";import $d from"node:path";var my=new Set(["jpg","jpeg","png","gif","webp","bmp","tif","tiff","heic","avif"]),hy=new Set(["mp4","mov","webm","mkv","avi","m4v","3gp"]),fy=new Set(["mp3","ogg","opus","wav","m4a","flac","aac","wma"]),rs={jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",bmp:"image/bmp",tif:"image/tiff",tiff:"image/tiff",heic:"image/heic",avif:"image/avif",mp4:"video/mp4",mov:"video/quicktime",webm:"video/webm",mkv:"video/x-matroska",avi:"video/x-msvideo",m4v:"video/x-m4v","3gp":"video/3gpp",mp3:"audio/mpeg",ogg:"audio/ogg",opus:"audio/opus",wav:"audio/wav",m4a:"audio/mp4",flac:"audio/flac",aac:"audio/aac",wma:"audio/x-ms-wma",pdf:"application/pdf",zip:"application/zip",gz:"application/gzip",txt:"text/plain",json:"application/json",xml:"application/xml",csv:"text/csv"};function gy(e){let t=e.replace(/^\./,"").toLowerCase();return my.has(t)?{category:"image",mimetype:rs[t]??"image/jpeg"}:hy.has(t)?{category:"video",mimetype:rs[t]??"video/mp4"}:fy.has(t)?{category:"audio",mimetype:rs[t]??"audio/mpeg"}:{category:"document",mimetype:rs[t]??"application/octet-stream"}}function St(e,t){let n;try{n=Td.realpathSync(e)}catch{return{error:"File not found or unreadable."}}let o;try{o=Td.lstatSync(n)}catch{return{error:"Cannot stat file."}}if(!o.isFile())return{error:"Not a regular file (directories and special files are not supported)."};if(t>0&&o.size>t)return{error:`File too large (max ${t} bytes).`};let r=$d.basename(n),s=$d.extname(n).slice(1),{category:i,mimetype:a}=gy(s);return{absPath:n,category:i,mimetype:a,displayName:r}}import yy from"node:os";import*as Pd from"node-pty";function wy(e,t){return e.length<=t?e:`${e.slice(0,t)}
198
- [...truncated]`}function by(e){if(e===void 0||e===0)return null;let t=yy.constants.signals;for(let[n,o]of Object.entries(t))if(o===e)return n;return null}function Md(e){try{process.platform==="win32"?e.kill():e.kill("SIGTERM")}catch{e.kill()}}function wn(e,t,n){return new Promise(o=>{let r=Date.now(),s=!1,i="",a=n.cwd,l=null,d=!1,u=null,c=null,m,h=y=>{if(d)return;d=!0,m!==void 0&&clearTimeout(m),u?.dispose(),u=null,o(y);let b=c;c=null,queueMicrotask(()=>b?.dispose())},g={...n.env??process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...a?{PWD:a}:{}};try{l=Pd.spawn(e,["-c",t],{name:"xterm-256color",cols:120,rows:40,cwd:a,env:g})}catch(y){h({code:null,stdout:"",stderr:String(y),durationMs:Date.now()-r,timedOut:!1,signal:null});return}m=setTimeout(()=>{s=!0,l&&Md(l)},n.timeoutMs),u=l.onData(y=>{i+=y,i.length>n.maxBytes&&(i=wy(i,n.maxBytes),l&&Md(l))}),c=l.onExit(y=>{h({code:y.exitCode,stdout:i,stderr:"",durationMs:Date.now()-r,timedOut:s,signal:by(y.signal)})})})}function _o(e,t,n){let o=new Date(e),r=o.getFullYear(),s=o.getMonth(),i=o.getDate(),a=new Date(r,s,i,t,n,0,0).getTime();return a<=e&&(a=new Date(r,s,i+1,t,n,0,0).getTime()),a}function ky(e,t,n){let o=e;for(let r=0;r<14;r++){let s=_o(o,t,n),i=new Date(s).getDay();if(i>=1&&i<=5)return s;o=s}return _o(o,t,n)}function vy(e,t,n,o){let r=e;for(let s=0;s<370;s++){let i=_o(r,n,o);if(new Date(i).getDay()===t)return i;r=i}return _o(r,n,o)}function Sy(e,t){let n=new Date(e),o=n.getFullYear(),r=n.getMonth(),s=n.getDate(),i=n.getHours(),a=new Date(o,r,s,i,t,0,0).getTime();return a<=e&&(a=new Date(o,r,s,i+1,t,0,0).getTime()),a}function xy(e,t){return e.kind==="ondemand"||e.kind==="heartbeat"?Number.POSITIVE_INFINITY:e.kind==="daily"?_o(t,e.hour,e.minute):e.kind==="weekdays"?ky(t,e.hour,e.minute):e.kind==="hourly"?Sy(t,e.minute):vy(t,e.weekday,e.hour,e.minute)}function Id(e,t,n,o,r=32){if(e.kind==="ondemand"||e.kind==="heartbeat")return[];let s=t??n-1,i=[],a=s;for(;i.length<r;){let l=xy(e,a);if(l>o)break;i.push(l),a=l}return i}var Cy={sun:0,sunday:0,mon:1,monday:1,tue:2,tues:2,tuesday:2,wed:3,wednesday:3,thu:4,thur:4,thurs:4,thursday:4,fri:5,friday:5,sat:6,saturday:6};function Pa(e){let t=/^(\d{1,2}):(\d{2})$/.exec(e.trim());if(!t)return null;let n=Number(t[1]),o=Number(t[2]);return!Number.isFinite(n)||!Number.isFinite(o)||n>23||o>59?null:{hour:n,minute:o}}function Ry(e){let t=e.trim(),n=/^:(\d{1,2})$/.exec(t),o=/^(\d{1,2})$/.exec(t),r=n?n[1]:o?o[1]:null;if(r===null)return null;let s=Number(r);return!Number.isInteger(s)||s<0||s>59?null:s}function ss(e){if(e.length===0)return{ok:!1,error:"Missing schedule (try: ondemand, hourly [:MM], daily HH:MM, weekdays HH:MM, weekly <dow> HH:MM)."};let t=e[0].toLowerCase();if(t==="ondemand"||t==="manual")return e.length!==1?{ok:!1,error:"ondemand takes no extra tokens."}:{ok:!0,schedule:{kind:"ondemand"}};if(t==="daily"){if(e.length!==2)return{ok:!1,error:"Usage: daily HH:MM"};let n=Pa(e[1]);return n?{ok:!0,schedule:{kind:"daily",hour:n.hour,minute:n.minute}}:{ok:!1,error:"daily needs HH:MM (24h)."}}if(t==="weekdays"){if(e.length!==2)return{ok:!1,error:"Usage: weekdays HH:MM"};let n=Pa(e[1]);return n?{ok:!0,schedule:{kind:"weekdays",hour:n.hour,minute:n.minute}}:{ok:!1,error:"weekdays needs HH:MM (24h)."}}if(t==="hourly"){if(e.length===1)return{ok:!0,schedule:{kind:"hourly",minute:0}};if(e.length!==2)return{ok:!1,error:"Usage: hourly | hourly :MM | hourly MM (minute 0\u201359)."};let n=Ry(e[1]);return n===null?{ok:!1,error:"hourly needs :MM or MM (minute 0\u201359), e.g. hourly :30 or hourly 15."}:{ok:!0,schedule:{kind:"hourly",minute:n}}}if(t==="weekly"){if(e.length!==3)return{ok:!1,error:"Usage: weekly <mon|tue|\u2026|sun> HH:MM"};let n=e[1].toLowerCase(),o=Cy[n];if(o===void 0){let s=Number(n);Number.isInteger(s)&&s>=0&&s<=6&&(o=s)}if(o===void 0)return{ok:!1,error:`Unknown weekday "${e[1]}". Use mon\u2026sun or 0\u20136 (Sun=0).`};let r=Pa(e[2]);return r?{ok:!0,schedule:{kind:"weekly",weekday:o,hour:r.hour,minute:r.minute}}:{ok:!1,error:"weekly needs HH:MM (24h) after weekday."}}if(t==="heartbeat"){if(e.length<2||e.length>3)return{ok:!1,error:"Usage: heartbeat <interval> [grace] \u2014 e.g. heartbeat 1h, heartbeat 30m 10m"};let n=Ed(e[1]);if(n===null||n<6e4)return{ok:!1,error:"heartbeat interval must be >= 1m. Use 5m, 1h, 2h, 1d, etc."};let o=Math.floor(n*.5);if(e.length===3){let r=Ed(e[2]);if(r===null||r<3e4)return{ok:!1,error:"heartbeat grace must be >= 30s. Use 1m, 5m, etc."};o=r}return{ok:!0,schedule:{kind:"heartbeat",intervalMs:n,graceMs:o}}}return{ok:!1,error:`Unknown schedule kind "${t}".`}}function Ed(e){let t=e.trim().toLowerCase();if(!t)return null;let n=t.match(/^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);if(!n||t===""||!n[1]&&!n[2]&&!n[3]&&!n[4])return null;let o=n[1]?Number(n[1]):0,r=n[2]?Number(n[2]):0,s=n[3]?Number(n[3]):0,i=n[4]?Number(n[4]):0;return((o*24+r)*60+s)*6e4+i*1e3}function Ad(e){let t=Math.floor(e/1e3),n=Math.floor(t/86400),o=Math.floor(t%86400/3600),r=Math.floor(t%3600/60),s=[];return n>0&&s.push(`${n}d`),o>0&&s.push(`${o}h`),r>0&&s.push(`${r}m`),s.length>0?s.join(""):`${t}s`}function Kn(e){let t=o=>o<10?`0${o}`:String(o),n=(o,r)=>`${t(o)}:${t(r)}`;switch(e.kind){case"ondemand":return"ondemand";case"daily":return`daily ${n(e.hour,e.minute)}`;case"weekdays":return`weekdays ${n(e.hour,e.minute)}`;case"hourly":return e.minute===0?"hourly":`hourly :${t(e.minute)}`;case"weekly":return`weekly ${["sun","mon","tue","wed","thu","fri","sat"][e.weekday]??e.weekday} ${n(e.hour,e.minute)}`;case"heartbeat":return`heartbeat every ${Ad(e.intervalMs)} grace ${Ad(e.graceMs)}`}}G();xe();import Ty from"better-sqlite3";var Ld=2,tn=null;function Od(){B(ft);let e=new Ty(Xl);e.pragma("journal_mode = WAL"),e.exec(`
213
+ `)}`}getSessionCommand(t,n){if(Ge(n))return null;let r=this.getSession(t,n);return r?.command?.trim()?r.command.trim():null}info(t,n){let o=this.getCfg(),r=n??this.focus.get(t)??"";if(!r)return"Usage: /apps info <name> (or /apps get <name>, or attach first)";let s=Ge(r);if(s)return s;let i=this.getSession(t,r),a=this.logPath(t,r),l=0;try{l=ln.statSync(a).size}catch{}let c=this.muted.has(Fe(t,r)),d=this.rawMode.has(Fe(t,r));return[`session: ${r}`,`alive: ${i?.alive??!1}`,`cmd: ${i?.command??"(unknown)"}`,`cwd: ${i?.cwd??"(unknown)"}`,`env keys: ${i?.envKeysCount??"?"}`,`terminal size: ${o.appsCols}x${o.appsRows}`,`ring bytes (approx): ${i?.ringByteCount??0}`,`log: ${a} (${l} bytes)`,`mute outbound: ${c}`,`raw outbound: ${d}`].join(`
214
+ `)}async sendText(t,n,o){let r=Ge(n);if(r)return r;let s=this.getSession(t,n);if(!s?.alive)return`No session "${n}".`;let i=o.replace(/\r\n/g,`
215
+ `);return await Ss(s,i,this.getCfg()),`Sent to "${n}" (${i.length} chars + Enter per line).`}sendKey(t,n,o){let r=Ge(n);if(r)return r;let s=this.getSession(t,n);if(!s?.alive)return`No session "${n}".`;let i=gs(o);return i?(s.write(i),`Sent keys to "${n}".`):"Empty key sequence. Example: /apps key sh Enter or ^C,Up,Enter"}tail(t,n,o){let r=Ge(n);if(r)return r;let s=this.logPath(t,n);if(!ln.existsSync(s))return`(no log file for ${n})`;let i=this.getCfg(),a=Number.isFinite(o)&&o>0?Math.min(500,o):i.appsLogTailLines;return Pw(s,a)||"(empty log)"}readSince(t,n,o){let r=this.logPath(t,n);try{let i=ln.statSync(r).size,a=ln.openSync(r,"r");try{let l=Math.min(o,i),c=i-l,d=Buffer.alloc(c);return c>0&&ln.readSync(a,d,0,c,l),{text:d.toString("utf8"),nextOffset:i}}finally{ln.closeSync(a)}}catch{return{text:"",nextOffset:o}}}mute(t,n){let o=Ge(n);return o||(this.muted.add(Fe(t,n)),`Muted chat output for "${n}" (log still grows). /apps unmute ${n}`)}unmute(t,n){let o=Ge(n);return o||(this.muted.delete(Fe(t,n)),`Unmuted "${n}".`)}setRaw(t,n,o){let r=Ge(n);if(r)return r;let s=Fe(t,n);return o?this.rawMode.add(s):this.rawMode.delete(s),`raw chat output for "${n}": ${o?"on (ANSI kept)":"off (stripped)"}.`}resize(t,n,o,r){let s=Ge(n);if(s)return s;let i=this.getSession(t,n);if(!i?.alive)return`No session "${n}".`;let a=Math.min(500,Math.max(20,Math.floor(o))),l=Math.min(200,Math.max(5,Math.floor(r)));return i.resize(a,l),`Resized "${n}" to ${a}x${l}.`}stop(t,n){let o=Ge(n);if(o)return o;let r=this.getSession(t,n);if(!r?.alive)return`No running session "${n}".`;r.kill("SIGTERM");let s=Fe(t,n),i=setTimeout(()=>{this.killTimers.delete(i);let a=this.getSession(t,n);a?.alive&&a.kill("SIGKILL")},5e3);return this.killTimers.add(i),`SIGTERM sent to "${n}". SIGKILL in 5s if still running.`}kill(t,n){let o=Ge(n);if(o)return o;let r=this.getSession(t,n);return r?.alive?(r.kill("SIGKILL"),`SIGKILL sent to "${n}".`):`No running session "${n}".`}rm(t,n){let o=Ge(n);if(o)return o;if(this.getSession(t,n)?.alive)return`Session "${n}" is still running. /apps stop ${n} first, then /apps rm ${n}.`;this.removeSessionRecord(t,n),this.muted.delete(Fe(t,n)),this.rawMode.delete(Fe(t,n)),this.passwordHintSent.delete(Fe(t,n));let s=this.logPath(t,n);try{ln.rmSync(s,{force:!0})}catch{}return`Removed "${n}" metadata and log.`}};Ze();Ce();import Qo from"node:fs";import Vo from"node:path";import Ew from"node:os";import*as gp from"node-pty";function Aw(e,t){return e.length<=t?e:`${e.slice(0,t)}
216
+ [...truncated]`}function Iw(e){if(e===void 0||e===0)return null;let t=Ew.constants.signals;for(let[n,o]of Object.entries(t))if(o===e)return n;return null}function fp(e){try{process.platform==="win32"?e.kill():e.kill("SIGTERM")}catch{e.kill()}}function $n(e,t,n){return new Promise(o=>{let r=Date.now(),s=!1,i="",a=n.cwd,l=null,c=!1,d=null,u=null,m,h=y=>{if(c)return;c=!0,m!==void 0&&clearTimeout(m),d?.dispose(),d=null,o(y);let b=u;u=null,queueMicrotask(()=>b?.dispose())},g={...n.env??process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...a?{PWD:a}:{}};try{l=gp.spawn(e,["-c",t],{name:"xterm-256color",cols:120,rows:40,cwd:a,env:g})}catch(y){h({code:null,stdout:"",stderr:String(y),durationMs:Date.now()-r,timedOut:!1,signal:null});return}m=setTimeout(()=>{s=!0,l&&fp(l)},n.timeoutMs),d=l.onData(y=>{i+=y,i.length>n.maxBytes&&(i=Aw(i,n.maxBytes),l&&fp(l))}),u=l.onExit(y=>{h({code:y.exitCode,stdout:i,stderr:"",durationMs:Date.now()-r,timedOut:s,signal:Iw(y.signal)})})})}function Yo(e,t,n){let o=new Date(e),r=o.getFullYear(),s=o.getMonth(),i=o.getDate(),a=new Date(r,s,i,t,n,0,0).getTime();return a<=e&&(a=new Date(r,s,i+1,t,n,0,0).getTime()),a}function Lw(e,t,n){let o=e;for(let r=0;r<14;r++){let s=Yo(o,t,n),i=new Date(s).getDay();if(i>=1&&i<=5)return s;o=s}return Yo(o,t,n)}function Ow(e,t,n,o){let r=e;for(let s=0;s<370;s++){let i=Yo(r,n,o);if(new Date(i).getDay()===t)return i;r=i}return Yo(r,n,o)}function Nw(e,t){let n=new Date(e),o=n.getFullYear(),r=n.getMonth(),s=n.getDate(),i=n.getHours(),a=new Date(o,r,s,i,t,0,0).getTime();return a<=e&&(a=new Date(o,r,s,i+1,t,0,0).getTime()),a}function Fw(e,t){return e.kind==="ondemand"||e.kind==="heartbeat"?Number.POSITIVE_INFINITY:e.kind==="daily"?Yo(t,e.hour,e.minute):e.kind==="weekdays"?Lw(t,e.hour,e.minute):e.kind==="hourly"?Nw(t,e.minute):Ow(t,e.weekday,e.hour,e.minute)}function bp(e,t,n,o,r=32){if(e.kind==="ondemand"||e.kind==="heartbeat")return[];let s=t??n-1,i=[],a=s;for(;i.length<r;){let l=Fw(e,a);if(l>o)break;i.push(l),a=l}return i}var Ww={sun:0,sunday:0,mon:1,monday:1,tue:2,tues:2,tuesday:2,wed:3,wednesday:3,thu:4,thur:4,thurs:4,thursday:4,fri:5,friday:5,sat:6,saturday:6};function tl(e){let t=/^(\d{1,2}):(\d{2})$/.exec(e.trim());if(!t)return null;let n=Number(t[1]),o=Number(t[2]);return!Number.isFinite(n)||!Number.isFinite(o)||n>23||o>59?null:{hour:n,minute:o}}function _w(e){let t=e.trim(),n=/^:(\d{1,2})$/.exec(t),o=/^(\d{1,2})$/.exec(t),r=n?n[1]:o?o[1]:null;if(r===null)return null;let s=Number(r);return!Number.isInteger(s)||s<0||s>59?null:s}function xs(e){if(e.length===0)return{ok:!1,error:"Missing schedule (try: ondemand, hourly [:MM], daily HH:MM, weekdays HH:MM, weekly <dow> HH:MM)."};let t=e[0].toLowerCase();if(t==="ondemand"||t==="manual")return e.length!==1?{ok:!1,error:"ondemand takes no extra tokens."}:{ok:!0,schedule:{kind:"ondemand"}};if(t==="daily"){if(e.length!==2)return{ok:!1,error:"Usage: daily HH:MM"};let n=tl(e[1]);return n?{ok:!0,schedule:{kind:"daily",hour:n.hour,minute:n.minute}}:{ok:!1,error:"daily needs HH:MM (24h)."}}if(t==="weekdays"){if(e.length!==2)return{ok:!1,error:"Usage: weekdays HH:MM"};let n=tl(e[1]);return n?{ok:!0,schedule:{kind:"weekdays",hour:n.hour,minute:n.minute}}:{ok:!1,error:"weekdays needs HH:MM (24h)."}}if(t==="hourly"){if(e.length===1)return{ok:!0,schedule:{kind:"hourly",minute:0}};if(e.length!==2)return{ok:!1,error:"Usage: hourly | hourly :MM | hourly MM (minute 0\u201359)."};let n=_w(e[1]);return n===null?{ok:!1,error:"hourly needs :MM or MM (minute 0\u201359), e.g. hourly :30 or hourly 15."}:{ok:!0,schedule:{kind:"hourly",minute:n}}}if(t==="weekly"){if(e.length!==3)return{ok:!1,error:"Usage: weekly <mon|tue|\u2026|sun> HH:MM"};let n=e[1].toLowerCase(),o=Ww[n];if(o===void 0){let s=Number(n);Number.isInteger(s)&&s>=0&&s<=6&&(o=s)}if(o===void 0)return{ok:!1,error:`Unknown weekday "${e[1]}". Use mon\u2026sun or 0\u20136 (Sun=0).`};let r=tl(e[2]);return r?{ok:!0,schedule:{kind:"weekly",weekday:o,hour:r.hour,minute:r.minute}}:{ok:!1,error:"weekly needs HH:MM (24h) after weekday."}}if(t==="heartbeat"){if(e.length<2||e.length>3)return{ok:!1,error:"Usage: heartbeat <interval> [grace] \u2014 e.g. heartbeat 1h, heartbeat 30m 10m"};let n=yp(e[1]);if(n===null||n<6e4)return{ok:!1,error:"heartbeat interval must be >= 1m. Use 5m, 1h, 2h, 1d, etc."};let o=Math.floor(n*.5);if(e.length===3){let r=yp(e[2]);if(r===null||r<3e4)return{ok:!1,error:"heartbeat grace must be >= 30s. Use 1m, 5m, etc."};o=r}return{ok:!0,schedule:{kind:"heartbeat",intervalMs:n,graceMs:o}}}return{ok:!1,error:`Unknown schedule kind "${t}".`}}function yp(e){let t=e.trim().toLowerCase();if(!t)return null;let n=t.match(/^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);if(!n||t===""||!n[1]&&!n[2]&&!n[3]&&!n[4])return null;let o=n[1]?Number(n[1]):0,r=n[2]?Number(n[2]):0,s=n[3]?Number(n[3]):0,i=n[4]?Number(n[4]):0;return((o*24+r)*60+s)*6e4+i*1e3}function wp(e){let t=Math.floor(e/1e3),n=Math.floor(t/86400),o=Math.floor(t%86400/3600),r=Math.floor(t%3600/60),s=[];return n>0&&s.push(`${n}d`),o>0&&s.push(`${o}h`),r>0&&s.push(`${r}m`),s.length>0?s.join(""):`${t}s`}function co(e){let t=o=>o<10?`0${o}`:String(o),n=(o,r)=>`${t(o)}:${t(r)}`;switch(e.kind){case"ondemand":return"ondemand";case"daily":return`daily ${n(e.hour,e.minute)}`;case"weekdays":return`weekdays ${n(e.hour,e.minute)}`;case"hourly":return e.minute===0?"hourly":`hourly :${t(e.minute)}`;case"weekly":return`weekly ${["sun","mon","tue","wed","thu","fri","sat"][e.weekday]??e.weekday} ${n(e.hour,e.minute)}`;case"heartbeat":return`heartbeat every ${wp(e.intervalMs)} grace ${wp(e.graceMs)}`}}q();Ce();import Dw from"better-sqlite3";var kp=2,un=null;function vp(){H(yt);let e=new Dw(vc);e.pragma("journal_mode = WAL"),e.exec(`
199
217
  CREATE TABLE IF NOT EXISTS cowork_meta (
200
218
  key TEXT PRIMARY KEY,
201
219
  value TEXT NOT NULL
@@ -214,120 +232,127 @@ ${n.join(`
214
232
  last_ok INTEGER NOT NULL,
215
233
  updated_at_ms INTEGER NOT NULL
216
234
  );
217
- `);let t=e.prepare("SELECT value FROM cowork_meta WHERE key = 'schema_version'").get(),n=t?Number(t.value):0;return n<Ld&&(n<2&&e.exec(`
235
+ `);let t=e.prepare("SELECT value FROM cowork_meta WHERE key = 'schema_version'").get(),n=t?Number(t.value):0;return n<kp&&(n<2&&e.exec(`
218
236
  CREATE TABLE IF NOT EXISTS cowork_task_state (
219
237
  task_id TEXT PRIMARY KEY,
220
238
  last_ok INTEGER NOT NULL,
221
239
  updated_at_ms INTEGER NOT NULL
222
240
  );
223
- `),e.prepare("INSERT OR REPLACE INTO cowork_meta (key, value) VALUES ('schema_version', ?)").run(String(Ld))),e}function bn(e){tn||(tn=Od()),My(e)}function Nd(){if(tn){try{tn.close()}catch{}tn=null}}function kn(){return tn||(tn=Od()),tn}function $y(e){let n=kn().prepare("SELECT MAX(slot_ms) AS m FROM cowork_slot_completion WHERE task_id = ?").get(e)?.m;return n==null||!Number.isFinite(n)?null:n}function is(e){let t=$y(e.id);return t??e.lastCompletedSlotMs}function as(e,t){if(t.length===0)return;let n=kn(),o=n.prepare(`
241
+ `),e.prepare("INSERT OR REPLACE INTO cowork_meta (key, value) VALUES ('schema_version', ?)").run(String(kp))),e}function Mn(e){un||(un=vp()),Bw(e)}function Sp(){if(un){try{un.close()}catch{}un=null}}function Pn(){return un||(un=vp()),un}function Uw(e){let n=Pn().prepare("SELECT MAX(slot_ms) AS m FROM cowork_slot_completion WHERE task_id = ?").get(e)?.m;return n==null||!Number.isFinite(n)?null:n}function Cs(e){let t=Uw(e.id);return t??e.lastCompletedSlotMs}function Rs(e,t){if(t.length===0)return;let n=Pn(),o=n.prepare(`
224
242
  INSERT INTO cowork_slot_completion (task_id, slot_ms, kind, completed_at_ms, log_path)
225
243
  VALUES (?, ?, ?, ?, ?)
226
- `);n.transaction(()=>{for(let s of t)o.run(e,s.slotMs,s.kind,s.completedAtMs,s.logPath)})()}function Ea(e){kn().prepare("INSERT OR REPLACE INTO cowork_task_state (task_id, last_ok, updated_at_ms) VALUES (?, 1, ?)").run(e,Date.now())}function ls(e){return kn().prepare("SELECT updated_at_ms FROM cowork_task_state WHERE task_id = ?").get(e)?.updated_at_ms??null}function cs(e){let t=kn().prepare("SELECT last_ok FROM cowork_task_state WHERE task_id = ?").get(e);return t==null?null:t.last_ok===1}function us(e,t){kn().prepare("INSERT OR REPLACE INTO cowork_task_state (task_id, last_ok, updated_at_ms) VALUES (?, ?, ?)").run(e,t?1:0,Date.now())}function My(e){let n=kn().prepare("SELECT COUNT(*) AS c FROM cowork_slot_completion WHERE task_id = ?"),o=Date.now();for(let r of e)if(!(r.lastCompletedSlotMs==null||!Number.isFinite(r.lastCompletedSlotMs)||n.get(r.id).c>0))try{as(r.id,[{slotMs:r.lastCompletedSlotMs,kind:"migrated",logPath:null,completedAtMs:o}]),P.info({taskId:r.id,slotMs:r.lastCompletedSlotMs},"cowork seeded completion from tasks.json")}catch(i){P.warn({err:String(i),taskId:r.id},"cowork seed from tasks.json failed")}}function Aa(e,t){if(e.startsWith("tg:")){let n=e.slice(3);return pr(t.telegramAllowFrom).has(n)}return rc(dr(t.allowFrom),e.replace(/^wa:/,""))}var Py=8,Fd=12e4,_d=10,Ey=1e3;function Ay(e){return e.toISOString().replace(/[:.]/g,"-")}function Iy(e){let t="";for(let n of e)n==="*"?t+="[^/\\\\]*":n==="?"?t+="[^/\\\\]":/[.+^${}()|[\]\\]/.test(n)?t+=`\\${n}`:t+=n;return new RegExp(`^${t}$`)}function Ly(e,t,n){let o=Qe(e,t),r=Do.dirname(o),s=Do.basename(o);if(r.includes("*")||r.includes("?"))return[];if(!s.includes("*")&&!s.includes("?")){try{let d=Wo.statSync(o);if(d.isFile()&&d.mtimeMs>=n)return[o]}catch{}return[]}let i;try{i=Wo.readdirSync(r)}catch{return[]}let a=Iy(s),l=[];for(let d of i){if(!a.test(d))continue;let u=Do.join(r,d);try{let c=Wo.statSync(u);c.isFile()&&c.mtimeMs>=n&&l.push(u)}catch{}}return l}function Wd(e,t,n){let o=St(e,t.fileSendMaxBytes);return"error"in o?{ok:!1,displayName:n??Do.basename(e),reason:o.error.replace(/\.$/,"").toLowerCase()}:{ok:!0,spec:{absPath:o.absPath,category:o.category,mimetype:o.mimetype,displayName:n??o.displayName}}}async function Dd(e,t,n,o){let r=ie(t.ownerPeerKey),s=t.cwd.trim()?t.cwd:r.cwd,i=Qe(t.outputDir,r.cwd);try{Wo.mkdirSync(i,{recursive:!0,mode:448})}catch(E){P.warn({err:String(E),outDir:i},"cowork mkdir outputDir")}let a=n.slotMs!==null?new Date(n.slotMs).toLocaleString(void 0,{dateStyle:"short",timeStyle:"short"}):"on-demand",l=n.onDemand?"on-demand":n.catchUp?"catch-up":"scheduled",d=Date.now(),u=await wn(e.shell,t.command,{timeoutMs:e.syncTimeoutMs,maxBytes:e.syncMaxBytes,cwd:s}),c=Date.now()-d,m=`${Ay(new Date)}-${t.id}-${l}.log`,h=Do.join(i,m),g=[`cowork task=${t.name} id=${t.id}`,`slot=${a} kind=${l}`,`cwd=${s}`,`exit=${u.code} timedOut=${u.timedOut} durationMs=${c}`,"---",""].join(`
227
- `)+(u.stdout||"")+(u.stderr?`
244
+ `);n.transaction(()=>{for(let s of t)o.run(e,s.slotMs,s.kind,s.completedAtMs,s.logPath)})()}function nl(e){Pn().prepare("INSERT OR REPLACE INTO cowork_task_state (task_id, last_ok, updated_at_ms) VALUES (?, 1, ?)").run(e,Date.now())}function Ts(e){return Pn().prepare("SELECT updated_at_ms FROM cowork_task_state WHERE task_id = ?").get(e)?.updated_at_ms??null}function $s(e){let t=Pn().prepare("SELECT last_ok FROM cowork_task_state WHERE task_id = ?").get(e);return t==null?null:t.last_ok===1}function Ms(e,t){Pn().prepare("INSERT OR REPLACE INTO cowork_task_state (task_id, last_ok, updated_at_ms) VALUES (?, ?, ?)").run(e,t?1:0,Date.now())}function Bw(e){let n=Pn().prepare("SELECT COUNT(*) AS c FROM cowork_slot_completion WHERE task_id = ?"),o=Date.now();for(let r of e)if(!(r.lastCompletedSlotMs==null||!Number.isFinite(r.lastCompletedSlotMs)||n.get(r.id).c>0))try{Rs(r.id,[{slotMs:r.lastCompletedSlotMs,kind:"migrated",logPath:null,completedAtMs:o}]),M.info({taskId:r.id,slotMs:r.lastCompletedSlotMs},"cowork seeded completion from tasks.json")}catch(i){M.warn({err:String(i),taskId:r.id},"cowork seed from tasks.json failed")}}function ol(e,t){if(e.startsWith("tg:")){let n=e.slice(3);return xr(t.telegramAllowFrom).has(n)}return $c(Sr(t.allowFrom),e.replace(/^wa:/,""))}var Hw=8,xp=12e4,Cp=10,jw=1e3;function Gw(e){return e.toISOString().replace(/[:.]/g,"-")}function Jw(e){let t="";for(let n of e)n==="*"?t+="[^/\\\\]*":n==="?"?t+="[^/\\\\]":/[.+^${}()|[\]\\]/.test(n)?t+=`\\${n}`:t+=n;return new RegExp(`^${t}$`)}function qw(e,t,n){let o=Qe(e,t),r=Vo.dirname(o),s=Vo.basename(o);if(r.includes("*")||r.includes("?"))return[];if(!s.includes("*")&&!s.includes("?")){try{let c=Qo.statSync(o);if(c.isFile()&&c.mtimeMs>=n)return[o]}catch{}return[]}let i;try{i=Qo.readdirSync(r)}catch{return[]}let a=Jw(s),l=[];for(let c of i){if(!a.test(c))continue;let d=Vo.join(r,c);try{let u=Qo.statSync(d);u.isFile()&&u.mtimeMs>=n&&l.push(d)}catch{}}return l}function Rp(e,t,n){let o=xt(e,t.fileSendMaxBytes);return"error"in o?{ok:!1,displayName:n??Vo.basename(e),reason:o.error.replace(/\.$/,"").toLowerCase()}:{ok:!0,spec:{absPath:o.absPath,category:o.category,mimetype:o.mimetype,displayName:n??o.displayName}}}async function Tp(e,t,n,o){let r=ae(t.ownerPeerKey),s=t.cwd.trim()?t.cwd:r.cwd,i=Qe(t.outputDir,r.cwd);try{Qo.mkdirSync(i,{recursive:!0,mode:448})}catch(P){M.warn({err:String(P),outDir:i},"cowork mkdir outputDir")}let a=n.slotMs!==null?new Date(n.slotMs).toLocaleString(void 0,{dateStyle:"short",timeStyle:"short"}):"on-demand",l=n.onDemand?"on-demand":n.catchUp?"catch-up":"scheduled",c=Date.now(),d=await $n(e.shell,t.command,{timeoutMs:e.syncTimeoutMs,maxBytes:e.syncMaxBytes,cwd:s}),u=Date.now()-c,m=`${Gw(new Date)}-${t.id}-${l}.log`,h=Vo.join(i,m),g=[`cowork task=${t.name} id=${t.id}`,`slot=${a} kind=${l}`,`cwd=${s}`,`exit=${d.code} timedOut=${d.timedOut} durationMs=${u}`,"---",""].join(`
245
+ `)+(d.stdout||"")+(d.stderr?`
228
246
  --- stderr ---
229
- ${u.stderr}`:"");try{Wo.writeFileSync(h,g,{mode:384})}catch(E){P.warn({err:String(E),logPath:h},"cowork write log")}let y=u.code===0&&!u.timedOut&&u.signal===null,k=Pe().find(E=>E.id===t.id&&E.ownerPeerKey===t.ownerPeerKey),M=k?.notify??t.notify,R=k?.notifyWhen??t.notifyWhen??"always",L=k?.attachLog??t.attachLog,$=k?.attachFiles??t.attachFiles,N=!0;if(R==="failure")N=!y;else if(R==="state-change"){let E=cs(t.id);N=E===null||E!==y}us(t.id,y);let A=u.timedOut?"timeout":u.signal?`signal ${u.signal}`:u.code!==0&&u.code!==null?`exit ${u.code}`:null,z=`slot: ${a} \xB7 ${l}${A?` \xB7 ${A}`:""}`,Z=(u.stdout||"").replace(/\s+$/,""),ce=(u.stderr||"").trim(),ve=Z||(ce?`(stderr) ${ce}`:"(no output)"),H=R==="state-change"?y?" [recovered]":" [failing]":"",he=`${t.name}${H} : ${A?`[${A}] `:""}${ve}`,ee=n.onDemand?[z,`output: ${ve}`]:[he];if(N){let E=[],j=[],K=d-Ey,J=[],C=new Set;if(Array.isArray($))for(let ne of $){let Se;try{Se=Ly(ne,s,K)}catch(Ge){P.warn({err:String(Ge),pat:ne},"cowork attach glob"),E.push(`attach: ${ne} skipped (glob error)`);continue}if(Se.length!==0)for(let Ge of Se)C.has(Ge)||(C.add(Ge),J.push(Ge))}if(L){let ne=Wd(h,e,`${t.name}-${l}.log`);ne.ok?j.push(ne.spec):E.push(`attach: ${ne.displayName} skipped (${ne.reason})`)}let x=0;for(let ne of J){if(j.length>=_d){x+=1;continue}let Se=Wd(ne,e);Se.ok?j.push(Se.spec):E.push(`attach: ${Se.displayName} skipped (${Se.reason})`)}x>0&&E.push(`attached: skipped ${x} file(s) over cap ${_d}`),E.length>0&&ee.push(...E);let Y=ee.join(`
230
- `),oe=p(Y),ye=On(M,t.ownerPeerKey,e);for(let ne of ye){let Se=Te(oe,ne.startsWith("tg:")?"telegram":"whatsapp").text;try{await o.sendToPeer(ne,Se)}catch(Ge){P.warn({err:String(Ge),pk:ne},"cowork notify failed")}for(let Ge of j)try{await o.sendMediaToPeer(ne,Ge)}catch(mt){P.warn({err:String(mt),pk:ne,file:Ge.displayName},"cowork media notify failed")}}}return{commandOk:y,logPath:h}}function ds(e){let t=!1;bn(Pe());let n=async()=>{if(t)return;t=!0;let s=Date.now(),i=0,a=0,l=0,d=0,u=0;try{let c=e.getConfig(),{batch:m,remainingAfter:h}=uc(Py);a=m.length,l=h,i=m.length+h;for(let y of m)try{let k=Pe().find(M=>M.name===y.name.toLowerCase()&&M.ownerPeerKey===y.ownerPeerKey);if(k&&k.enabled){if(!Aa(k.ownerPeerKey,c)){P.warn({task:k.name,peer:k.ownerPeerKey},"cowork: skipping on-demand run \u2014 owner no longer on allowlist");continue}d+=1,await Dd(c,k,{slotMs:null,catchUp:!1,onDemand:!0},e)}}catch(b){P.warn({err:String(b),pending:y.name},"cowork on-demand run failed")}let f=Pe();bn(f);let g=Date.now();for(let y of f){if(!y.enabled||y.schedule.kind!=="heartbeat"||!Aa(y.ownerPeerKey,c))continue;let b=ls(y.id);if(b===null)continue;let k=b+y.schedule.intervalMs+y.schedule.graceMs;if(g>k){if(cs(y.id)===!1)continue;us(y.id,!1);let R=Math.round((g-b)/6e4),L=`${y.name} [heartbeat missed] \u2014 last check-in ${R}m ago`,$=y.notify,N=On($,y.ownerPeerKey,c);for(let A of N)try{await e.sendToPeer(A,L)}catch(z){P.warn({err:String(z),pk:A},"cowork heartbeat notify failed")}}else if(g<=k&&cs(y.id)===!1){us(y.id,!0);let R=`${y.name} [heartbeat recovered]`,L=y.notify,$=On(L,y.ownerPeerKey,c);for(let N of $)try{await e.sendToPeer(N,R)}catch(A){P.warn({err:String(A),pk:N},"cowork heartbeat recovery notify failed")}}}for(let y of f){if(!y.enabled||y.schedule.kind==="ondemand"||y.schedule.kind==="heartbeat")continue;if(!Aa(y.ownerPeerKey,c)){P.warn({task:y.name,peer:y.ownerPeerKey},"cowork: skipping scheduled run \u2014 owner no longer on allowlist");continue}let b=is(y),k=Id(y.schedule,b,y.createdAtMs,g);if(k.length===0)continue;let M=k[k.length-1],R=g-M>Fd;try{u+=1;let{commandOk:L,logPath:$}=await Dd(c,y,{slotMs:M,catchUp:R,onDemand:!1},e);if(L){let N=Date.now(),A=g-M<=Fd?"on_time":"catch_up";if(k.length===1)as(y.id,[{slotMs:M,kind:A,logPath:$,completedAtMs:N}]);else{let Z=k.slice(0,-1).map(ce=>({slotMs:ce,kind:"coalesced",logPath:null,completedAtMs:N}));Z.push({slotMs:M,kind:A,logPath:$,completedAtMs:N}),as(y.id,Z)}}}catch(L){P.warn({err:String(L),task:y.name},"cowork scheduled run failed")}}}finally{let c=Date.now()-s;P.info({tickMs:c,pendingDepthStart:i,pendingDequeued:a,pendingRemainingAfter:l,pendingRunsStarted:d,scheduledRan:u},"cowork tick"),t=!1}},o=setInterval(()=>void n(),3e4),r=setTimeout(()=>void n(),5e3);return()=>{clearInterval(o),clearTimeout(r),Nd()}}import Oy from"node:fs";import Ud from"node:path";import{fileURLToPath as Ny}from"node:url";var ps=null;function ot(){if(ps!==null)return ps;let e=Ud.dirname(Ny(import.meta.url)),t=Ud.join(e,"..","package.json"),n=Oy.readFileSync(t,"utf8"),o=JSON.parse(n);return ps=typeof o.version=="string"&&o.version.trim()?o.version.trim():"0.0.0",ps}xe();G();import{spawn as Fy}from"node:child_process";import Bd from"node:fs";import Hd from"node:path";var _y=new Set(["PATH","HOME","USER","LOGNAME","SHELL","LANG","LC_ALL","LC_CTYPE","LC_MESSAGES","TMPDIR","TZ"]),Wy=new Set(["OPENAI_API_KEY","ANTHROPIC_API_KEY","GOOGLE_API_KEY","GEMINI_API_KEY","MISTRAL_API_KEY","GROQ_API_KEY","COHERE_API_KEY","HUGGINGFACE_TOKEN","TOGETHER_API_KEY","FIREWORKS_API_KEY","PERPLEXITY_API_KEY","DEEPSEEK_API_KEY","XAI_API_KEY","CURSOR_API_KEY"]);function Dy(e,t){let n={OMNISH_PEER_KEY:e,OMNISH_CHAT_MESSAGE:t};for(let o of _y){let r=process.env[o];r!==void 0&&(n[o]=r)}for(let[o,r]of Object.entries(process.env))r&&(o.startsWith("OMNISH_")||Wy.has(o))&&(n[o]=r);return n}var jd=new Map;function Uy(e,t){let o=(jd.get(e)??Promise.resolve()).then(t).catch(r=>{P.warn({peerKey:e,err:String(r)},"chat LLM fallback queue task failed")});jd.set(e,o)}function By(e){se();let t=e.chatLlmWorkDir.trim();if(t){let o=Hd.resolve(t);return B(o),{cwd:o,cleanup:()=>{}}}let n=Bd.mkdtempSync(Hd.join(W,"chat-llm-"));return{cwd:n,cleanup:()=>{try{Bd.rmSync(n,{recursive:!0,force:!0})}catch{}}}}function Hy(e,t){return e.length<=t?e:`${e.slice(0,t)}
231
- [...input truncated]`}function jy(e,t){let n=[],o=e.stdout.trimEnd(),r=e.stderr.trimEnd();return o&&n.push(o.length>t?`${o.slice(0,t)}
247
+ ${d.stderr}`:"");try{Qo.writeFileSync(h,g,{mode:384})}catch(P){M.warn({err:String(P),logPath:h},"cowork write log")}let y=d.code===0&&!d.timedOut&&d.signal===null,k=Pe().find(P=>P.id===t.id&&P.ownerPeerKey===t.ownerPeerKey),R=k?.notify??t.notify,T=k?.notifyWhen??t.notifyWhen??"always",L=k?.attachLog??t.attachLog,x=k?.attachFiles??t.attachFiles,O=!0;if(T==="failure")O=!y;else if(T==="state-change"){let P=$s(t.id);O=P===null||P!==y}Ms(t.id,y);let A=d.timedOut?"timeout":d.signal?`signal ${d.signal}`:d.code!==0&&d.code!==null?`exit ${d.code}`:null,Y=`slot: ${a} \xB7 ${l}${A?` \xB7 ${A}`:""}`,te=(d.stdout||"").replace(/\s+$/,""),ue=(d.stderr||"").trim(),xe=te||(ue?`(stderr) ${ue}`:"(no output)"),W=T==="state-change"?y?" [recovered]":" [failing]":"",fe=`${t.name}${W} : ${A?`[${A}] `:""}${xe}`,V=n.onDemand?[Y,`output: ${xe}`]:[fe];if(O){let P=[],G=[],Q=c-jw,J=[],le=new Set;if(Array.isArray(x))for(let j of x){let oe;try{oe=qw(j,s,Q)}catch(de){M.warn({err:String(de),pat:j},"cowork attach glob"),P.push(`attach: ${j} skipped (glob error)`);continue}if(oe.length!==0)for(let de of oe)le.has(de)||(le.add(de),J.push(de))}if(L){let j=Rp(h,e,`${t.name}-${l}.log`);j.ok?G.push(j.spec):P.push(`attach: ${j.displayName} skipped (${j.reason})`)}let K=0;for(let j of J){if(G.length>=Cp){K+=1;continue}let oe=Rp(j,e);oe.ok?G.push(oe.spec):P.push(`attach: ${oe.displayName} skipped (${oe.reason})`)}K>0&&P.push(`attached: skipped ${K} file(s) over cap ${Cp}`),P.length>0&&V.push(...P);let _e=V.join(`
248
+ `),$=p(_e),E=Jn(R,t.ownerPeerKey,e);for(let j of E){let oe=me($,j.startsWith("tg:")?"telegram":"whatsapp").text;try{await o.sendToPeer(j,oe)}catch(de){M.warn({err:String(de),pk:j},"cowork notify failed")}for(let de of G)try{await o.sendMediaToPeer(j,de)}catch(jt){M.warn({err:String(jt),pk:j,file:de.displayName},"cowork media notify failed")}}}return{commandOk:y,logPath:h}}function Ps(e){let t=!1;Mn(Pe());let n=async()=>{if(t)return;t=!0;let s=Date.now(),i=0,a=0,l=0,c=0,d=0;try{let u=e.getConfig(),{batch:m,remainingAfter:h}=Lc(Hw);a=m.length,l=h,i=m.length+h;for(let y of m)try{let k=Pe().find(R=>R.name===y.name.toLowerCase()&&R.ownerPeerKey===y.ownerPeerKey);if(k&&k.enabled){if(!ol(k.ownerPeerKey,u)){M.warn({task:k.name,peer:k.ownerPeerKey},"cowork: skipping on-demand run \u2014 owner no longer on allowlist");continue}c+=1,await Tp(u,k,{slotMs:null,catchUp:!1,onDemand:!0},e)}}catch(b){M.warn({err:String(b),pending:y.name},"cowork on-demand run failed")}let f=Pe();Mn(f);let g=Date.now();for(let y of f){if(!y.enabled||y.schedule.kind!=="heartbeat"||!ol(y.ownerPeerKey,u))continue;let b=Ts(y.id);if(b===null)continue;let k=b+y.schedule.intervalMs+y.schedule.graceMs;if(g>k){if($s(y.id)===!1)continue;Ms(y.id,!1);let T=Math.round((g-b)/6e4),L=`${y.name} [heartbeat missed] \u2014 last check-in ${T}m ago`,x=y.notify,O=Jn(x,y.ownerPeerKey,u);for(let A of O)try{await e.sendToPeer(A,L)}catch(Y){M.warn({err:String(Y),pk:A},"cowork heartbeat notify failed")}}else if(g<=k&&$s(y.id)===!1){Ms(y.id,!0);let T=`${y.name} [heartbeat recovered]`,L=y.notify,x=Jn(L,y.ownerPeerKey,u);for(let O of x)try{await e.sendToPeer(O,T)}catch(A){M.warn({err:String(A),pk:O},"cowork heartbeat recovery notify failed")}}}for(let y of f){if(!y.enabled||y.schedule.kind==="ondemand"||y.schedule.kind==="heartbeat")continue;if(!ol(y.ownerPeerKey,u)){M.warn({task:y.name,peer:y.ownerPeerKey},"cowork: skipping scheduled run \u2014 owner no longer on allowlist");continue}let b=Cs(y),k=bp(y.schedule,b,y.createdAtMs,g);if(k.length===0)continue;let R=k[k.length-1],T=g-R>xp;try{d+=1;let{commandOk:L,logPath:x}=await Tp(u,y,{slotMs:R,catchUp:T,onDemand:!1},e);if(L){let O=Date.now(),A=g-R<=xp?"on_time":"catch_up";if(k.length===1)Rs(y.id,[{slotMs:R,kind:A,logPath:x,completedAtMs:O}]);else{let te=k.slice(0,-1).map(ue=>({slotMs:ue,kind:"coalesced",logPath:null,completedAtMs:O}));te.push({slotMs:R,kind:A,logPath:x,completedAtMs:O}),Rs(y.id,te)}}}catch(L){M.warn({err:String(L),task:y.name},"cowork scheduled run failed")}}}finally{let u=Date.now()-s;M.info({tickMs:u,pendingDepthStart:i,pendingDequeued:a,pendingRemainingAfter:l,pendingRunsStarted:c,scheduledRan:d},"cowork tick"),t=!1}},o=setInterval(()=>void n(),3e4),r=setTimeout(()=>void n(),5e3);return()=>{clearInterval(o),clearTimeout(r),Sp()}}import zw from"node:fs";import $p from"node:path";import{fileURLToPath as Kw}from"node:url";var Es=null;function rt(){if(Es!==null)return Es;let e=$p.dirname(Kw(import.meta.url)),t=$p.join(e,"..","package.json"),n=zw.readFileSync(t,"utf8"),o=JSON.parse(n);return Es=typeof o.version=="string"&&o.version.trim()?o.version.trim():"0.0.0",Es}Ce();q();import{spawn as Yw}from"node:child_process";import Mp from"node:fs";import Pp from"node:path";var Qw=new Set(["PATH","HOME","USER","LOGNAME","SHELL","LANG","LC_ALL","LC_CTYPE","LC_MESSAGES","TMPDIR","TZ"]),Vw=new Set(["OPENAI_API_KEY","ANTHROPIC_API_KEY","GOOGLE_API_KEY","GEMINI_API_KEY","MISTRAL_API_KEY","GROQ_API_KEY","COHERE_API_KEY","HUGGINGFACE_TOKEN","TOGETHER_API_KEY","FIREWORKS_API_KEY","PERPLEXITY_API_KEY","DEEPSEEK_API_KEY","XAI_API_KEY","CURSOR_API_KEY"]);function Xw(e,t){let n={OMNISH_PEER_KEY:e,OMNISH_CHAT_MESSAGE:t};for(let o of Qw){let r=process.env[o];r!==void 0&&(n[o]=r)}for(let[o,r]of Object.entries(process.env))r&&(o.startsWith("OMNISH_")||Vw.has(o))&&(n[o]=r);return n}var Ep=new Map;function Zw(e,t){let o=(Ep.get(e)??Promise.resolve()).then(t).catch(r=>{M.warn({peerKey:e,err:String(r)},"chat LLM fallback queue task failed")});Ep.set(e,o)}function eb(e){ie();let t=e.chatLlmWorkDir.trim();if(t){let o=Pp.resolve(t);return H(o),{cwd:o,cleanup:()=>{}}}let n=Mp.mkdtempSync(Pp.join(D,"chat-llm-"));return{cwd:n,cleanup:()=>{try{Mp.rmSync(n,{recursive:!0,force:!0})}catch{}}}}function tb(e,t){return e.length<=t?e:`${e.slice(0,t)}
249
+ [...input truncated]`}function nb(e,t){let n=[],o=e.stdout.trimEnd(),r=e.stderr.trimEnd();return o&&n.push(o.length>t?`${o.slice(0,t)}
232
250
  [...truncated]`:o),r&&(n.push("\u2014 stderr \u2014"),n.push(r.length>t?`${r.slice(0,t)}
233
251
  [...truncated]`:r)),n.length===0&&n.push("(no output)"),e.timedOut?n.push(`(timed out after ${Math.round(e.durationMs/1e3)}s)`):e.code!==0&&e.code!==null?n.push(`(exit ${e.code})`):e.signal&&n.push(`(signal ${e.signal})`),n.join(`
234
- `)}function ms(e){let t=e&&typeof e=="object"&&"code"in e?String(e.code):"";return t==="EPIPE"||t==="EOF"}function Gd(e){try{e?.stdin?.end()}catch(t){if(!ms(t))throw t}}function Gy(e,t,n,o){return new Promise(r=>{let s=Date.now(),i=!1,a="",l="",d=!1,u=null,c,m=b=>{d||(d=!0,clearTimeout(c),r(b))},h=o.maxOutChars,f=(b,k)=>{b==="out"?a+=k:l+=k;let M=a.length+l.length;if(M>h){let R=M-h;if(l.length>=R)l=`${l.slice(0,Math.max(0,l.length-R))}
235
- [...truncated]`;else{let L=R-l.length;l="",a=`${a.slice(0,Math.max(0,a.length-L))}
236
- [...truncated]`}try{u?.kill("SIGTERM")}catch{}}};try{u=Fy(e,["-c",t],{cwd:o.cwd,env:o.env,stdio:["pipe","pipe","pipe"]})}catch(b){m({code:null,stdout:"",stderr:String(b),durationMs:Date.now()-s,timedOut:!1,signal:null});return}c=setTimeout(()=>{i=!0;try{u?.kill("SIGTERM")}catch{}},o.timeoutMs),u.stdout?.setEncoding("utf8"),u.stderr?.setEncoding("utf8"),u.stdout?.on("data",b=>f("out",b)),u.stderr?.on("data",b=>f("err",b));let g=u.stdin;g&&g.on("error",b=>{ms(b)||m({code:null,stdout:a,stderr:`${l}
237
- ${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null})}),(()=>{if(!(!g||d))try{g.write(n,"utf8",b=>{if(b&&!ms(b)){try{u?.kill("SIGTERM")}catch{}m({code:null,stdout:a,stderr:`${l}
238
- ${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}Gd(u)})}catch(b){if(!ms(b)){m({code:null,stdout:a,stderr:`${l}
239
- ${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}Gd(u)}})(),u.on("error",b=>{m({code:null,stdout:a,stderr:`${l}
240
- ${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null})}),u.on("close",(b,k)=>{m({code:b,stdout:a,stderr:l,durationMs:Date.now()-s,timedOut:i,signal:k??null})})})}async function qy(e,t,n,o){let r=e.chatLlmShellCommand.trim();if(!r){await o("(chat LLM fallback: chatLlmShellCommand is empty)");return}let{cwd:s,cleanup:i}=By(e);try{let a=Hy(n,e.chatLlmMaxInputChars),l=Dy(t,a),d=e.chatLlmMaxOutputChars,u;e.chatLlmNeedsTty?u=await wn(e.shell,r,{cwd:s,timeoutMs:e.chatLlmTimeoutMs,maxBytes:d,env:l}):u=await Gy(e.shell,r,a,{cwd:s,timeoutMs:e.chatLlmTimeoutMs,maxOutChars:d,env:l});let c=jy(u,d);await o(c)}catch(a){P.warn({peerKey:t,err:String(a)},"chat LLM fallback run failed"),await o(`(assistant error) ${String(a)}`)}finally{i()}}function Yn(e,t,n,o){P.info({peerKey:t,len:n.length},"chat LLM fallback enqueued"),Uy(t,async()=>{await qy(e,t,n,o),P.info({peerKey:t},"chat LLM fallback completed")})}G();import{spawn as Jy}from"node:child_process";import zy from"node:crypto";import Ve from"node:fs";import Ia from"node:path";var qd=64,Ky=/^[a-zA-Z0-9._-]+$/;function Yy(e){let t=e.trim();return t?t.length>qd?`Job name must be at most ${qd} characters.`:Ky.test(t)?null:"Job name may only use letters, digits, and . _ -":"Job name must not be empty."}function Jd(e){let t=e.trim();if(!t)return{error:"empty"};let n=null,o=!1,r=!0;for(;r;){r=!1;let i=t.trim();if(/^--notify(?:\s|$)/i.test(i)){o=!0,t=i.slice(8).trim(),r=!0;continue}if(/^-N(?:\s|$)/.test(i)){o=!0,t=i.slice(2).trim(),r=!0;continue}let a=[[/^--name\s*=\s*(\S+)\s+([\s\S]+)$/i,1,2],[/^--name\s+(\S+)\s+([\s\S]+)$/i,1,2],[/^-n\s+(\S+)\s+([\s\S]+)$/i,1,2]];for(let[l,d,u]of a){let c=i.match(l);if(c){n=c[d],t=c[u].trim(),r=!0;break}}}let s=t.trim();if(n===null&&[/^-n\s+\S+\s*$/i,/^--name\s+\S+\s*$/i,/^--name=\S+\s*$/i,/^-n\s*$/i,/^--name\s*$/i].some(a=>a.test(s)))return{error:"Add a shell command after the name."};if(n!==null){let i=Yy(n);if(i)return{error:i}}return s?{cmd:s,name:n,notify:o}:{error:"Add a shell command after the flags."}}function Qy(e,t){let n=t.trim();if(!n)return{ok:!1,error:"Missing job id or name."};let o=n.toLowerCase(),r=/^[a-f0-9]{8}$/;if(r.test(o)&&e.find(i=>i.id===o))return{ok:!0,id:o};for(let s of e)if(s.name&&s.name.toLowerCase()===o)return{ok:!0,id:s.id};return r.test(o)?{ok:!1,error:`Unknown job id: ${o}`}:{ok:!1,error:`Unknown job name: ${n}`}}function Qn(e,t){Ve.writeFileSync(e,JSON.stringify(t,null,2)+`
241
- `,{mode:384})}function Uo(e){try{return JSON.parse(Ve.readFileSync(e,"utf8"))}catch{return null}}function zd(e){let t=e.name?`${e.id} (${e.name})`:e.id,n=e.finishedAt&&e.startedAt?Date.parse(e.finishedAt)-Date.parse(e.startedAt):null,o=n!==null?`${(n/1e3).toFixed(1)}s`:"?",r=e.exitCode===0&&!e.signal,s=e.status==="killed"?`killed (signal ${e.signal??"?"})`:r?"completed successfully":e.signal?`signal ${e.signal}`:`exit ${e.exitCode??"?"}`,i=e.cmd.length>100?`${e.cmd.slice(0,100)}\u2026`:e.cmd;return[`Job ${t} ${s}`,`duration: ${o}`,`$ ${i}`,`/log ${e.name??e.id}`].join(`
242
- `)}var Ft=class{running=new Map;metaPath(t){return Ia.join(it,`${t}.meta.json`)}logPath(t){return Ia.join(it,`${t}.log`)}spawnJob(t,n,o={}){se();let r=zy.randomBytes(4).toString("hex"),s=this.logPath(r),i=this.metaPath(r);Ve.writeFileSync(s,"",{flag:"w",mode:384});let a=Ve.createWriteStream(s,{flags:"a"}),l=new Date().toISOString(),d=o.cwd,u=Jy(t,["-c",n],{stdio:["ignore","pipe","pipe"],env:{...process.env,TERM:"dumb",...d?{PWD:d}:{}},...d?{cwd:d}:{}});this.running.set(r,u);let c={id:r,cmd:n,...o.name?{name:o.name}:{},pid:u.pid??null,startedAt:l,status:"running",exitCode:null,signal:null,...o.notifyPeerKey?{notifyPeerKey:o.notifyPeerKey}:{}};return Qn(i,c),u.stdout?.on("data",m=>{a.write(m)}),u.stderr?.on("data",m=>{a.write(m)}),u.on("close",(m,h)=>{this.running.delete(r),a.end();let f=Uo(i)??c,g={...f,status:f.status==="killed"?"killed":"done",exitCode:m,signal:h??null,finishedAt:new Date().toISOString()};Qn(i,g),o.onComplete&&o.onComplete(g)}),u.on("error",m=>{this.running.delete(r),a.end(()=>{try{Ve.appendFileSync(s,`
252
+ `)}function As(e){let t=e&&typeof e=="object"&&"code"in e?String(e.code):"";return t==="EPIPE"||t==="EOF"}function Ap(e){try{e?.stdin?.end()}catch(t){if(!As(t))throw t}}function ob(e,t,n,o){return new Promise(r=>{let s=Date.now(),i=!1,a="",l="",c=!1,d=null,u,m=b=>{c||(c=!0,clearTimeout(u),r(b))},h=o.maxOutChars,f=(b,k)=>{b==="out"?a+=k:l+=k;let R=a.length+l.length;if(R>h){let T=R-h;if(l.length>=T)l=`${l.slice(0,Math.max(0,l.length-T))}
253
+ [...truncated]`;else{let L=T-l.length;l="",a=`${a.slice(0,Math.max(0,a.length-L))}
254
+ [...truncated]`}try{d?.kill("SIGTERM")}catch{}}};try{d=Yw(e,["-c",t],{cwd:o.cwd,env:o.env,stdio:["pipe","pipe","pipe"]})}catch(b){m({code:null,stdout:"",stderr:String(b),durationMs:Date.now()-s,timedOut:!1,signal:null});return}u=setTimeout(()=>{i=!0;try{d?.kill("SIGTERM")}catch{}},o.timeoutMs),d.stdout?.setEncoding("utf8"),d.stderr?.setEncoding("utf8"),d.stdout?.on("data",b=>f("out",b)),d.stderr?.on("data",b=>f("err",b));let g=d.stdin;g&&g.on("error",b=>{As(b)||m({code:null,stdout:a,stderr:`${l}
255
+ ${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null})}),(()=>{if(!(!g||c))try{g.write(n,"utf8",b=>{if(b&&!As(b)){try{d?.kill("SIGTERM")}catch{}m({code:null,stdout:a,stderr:`${l}
256
+ ${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}Ap(d)})}catch(b){if(!As(b)){m({code:null,stdout:a,stderr:`${l}
257
+ ${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}Ap(d)}})(),d.on("error",b=>{m({code:null,stdout:a,stderr:`${l}
258
+ ${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null})}),d.on("close",(b,k)=>{m({code:b,stdout:a,stderr:l,durationMs:Date.now()-s,timedOut:i,signal:k??null})})})}async function rb(e,t,n,o){let r=e.chatLlmShellCommand.trim();if(!r){await o("(chat LLM fallback: chatLlmShellCommand is empty)");return}let{cwd:s,cleanup:i}=eb(e);try{let a=tb(n,e.chatLlmMaxInputChars),l=Xw(t,a),c=e.chatLlmMaxOutputChars,d;e.chatLlmNeedsTty?d=await $n(e.shell,r,{cwd:s,timeoutMs:e.chatLlmTimeoutMs,maxBytes:c,env:l}):d=await ob(e.shell,r,a,{cwd:s,timeoutMs:e.chatLlmTimeoutMs,maxOutChars:c,env:l});let u=nb(d,c);await o(u)}catch(a){M.warn({peerKey:t,err:String(a)},"chat LLM fallback run failed"),await o(`(assistant error) ${String(a)}`)}finally{i()}}function uo(e,t,n,o){M.info({peerKey:t,len:n.length},"chat LLM fallback enqueued"),Zw(t,async()=>{await rb(e,t,n,o),M.info({peerKey:t},"chat LLM fallback completed")})}q();import{spawn as sb}from"node:child_process";import ib from"node:crypto";import Xe from"node:fs";import rl from"node:path";var Ip=64,ab=/^[a-zA-Z0-9._-]+$/;function lb(e){let t=e.trim();return t?t.length>Ip?`Job name must be at most ${Ip} characters.`:ab.test(t)?null:"Job name may only use letters, digits, and . _ -":"Job name must not be empty."}function Lp(e){let t=e.trim();if(!t)return{error:"empty"};let n=null,o=!1,r=!0;for(;r;){r=!1;let i=t.trim();if(/^--notify(?:\s|$)/i.test(i)){o=!0,t=i.slice(8).trim(),r=!0;continue}if(/^-N(?:\s|$)/.test(i)){o=!0,t=i.slice(2).trim(),r=!0;continue}let a=[[/^--name\s*=\s*(\S+)\s+([\s\S]+)$/i,1,2],[/^--name\s+(\S+)\s+([\s\S]+)$/i,1,2],[/^-n\s+(\S+)\s+([\s\S]+)$/i,1,2]];for(let[l,c,d]of a){let u=i.match(l);if(u){n=u[c],t=u[d].trim(),r=!0;break}}}let s=t.trim();if(n===null&&[/^-n\s+\S+\s*$/i,/^--name\s+\S+\s*$/i,/^--name=\S+\s*$/i,/^-n\s*$/i,/^--name\s*$/i].some(a=>a.test(s)))return{error:"Add a shell command after the name."};if(n!==null){let i=lb(n);if(i)return{error:i}}return s?{cmd:s,name:n,notify:o}:{error:"Add a shell command after the flags."}}function cb(e,t){let n=t.trim();if(!n)return{ok:!1,error:"Missing job id or name."};let o=n.toLowerCase(),r=/^[a-f0-9]{8}$/;if(r.test(o)&&e.find(i=>i.id===o))return{ok:!0,id:o};for(let s of e)if(s.name&&s.name.toLowerCase()===o)return{ok:!0,id:s.id};return r.test(o)?{ok:!1,error:`Unknown job id: ${o}`}:{ok:!1,error:`Unknown job name: ${n}`}}function Xo(e,t){Xe.writeFileSync(e,JSON.stringify(t,null,2)+`
259
+ `,{mode:384})}function Zo(e){try{return JSON.parse(Xe.readFileSync(e,"utf8"))}catch{return null}}function Is(e){let t=e.name?`${e.id} (${e.name})`:e.id,n=e.finishedAt&&e.startedAt?Date.parse(e.finishedAt)-Date.parse(e.startedAt):null,o=n!==null?`${(n/1e3).toFixed(1)}s`:"?",r=e.exitCode===0&&!e.signal,s=e.status==="killed"?`killed (signal ${e.signal??"?"})`:r?"completed successfully":e.signal?`signal ${e.signal}`:`exit ${e.exitCode??"?"}`,i=e.cmd.length>100?`${e.cmd.slice(0,100)}\u2026`:e.cmd;return[`Job ${t} ${s}`,`duration: ${o}`,`$ ${i}`,`/log ${e.name??e.id}`].join(`
260
+ `)}var Dt=class{running=new Map;onJobExit;constructor(t={}){this.onJobExit=t.onJobExit}finishJob(t,n,o,r){Xo(t,o),r.onComplete&&r.onComplete(o),this.onJobExit&&this.onJobExit(o)}metaPath(t){return rl.join(it,`${t}.meta.json`)}logPath(t){return rl.join(it,`${t}.log`)}spawnJob(t,n,o={}){ie();let r=ib.randomBytes(4).toString("hex"),s=this.logPath(r),i=this.metaPath(r);Xe.writeFileSync(s,"",{flag:"w",mode:384});let a=Xe.createWriteStream(s,{flags:"a"}),l=new Date().toISOString(),c=o.cwd,d=sb(t,["-c",n],{stdio:["ignore","pipe","pipe"],env:{...process.env,TERM:"dumb",...c?{PWD:c}:{}},...c?{cwd:c}:{}});this.running.set(r,d);let u={id:r,cmd:n,...o.name?{name:o.name}:{},pid:d.pid??null,startedAt:l,status:"running",exitCode:null,signal:null,...o.notifyPeerKey?{notifyPeerKey:o.notifyPeerKey}:{}};return Xo(i,u),d.stdout?.on("data",m=>{a.write(m)}),d.stderr?.on("data",m=>{a.write(m)}),d.on("close",(m,h)=>{this.running.delete(r),a.end();let f=Zo(i)??u,g={...f,status:f.status==="killed"?"killed":"done",exitCode:m,signal:h??null,finishedAt:new Date().toISOString()};this.finishJob(i,u,g,o)}),d.on("error",m=>{this.running.delete(r),a.end(()=>{try{Xe.appendFileSync(s,`
243
261
  [spawn error] ${String(m)}
244
- `)}catch{}});let f={...Uo(i)??c,status:"done",exitCode:null,signal:null,finishedAt:new Date().toISOString()};Qn(i,f),o.onComplete&&o.onComplete(f)}),{id:r,meta:c}}list(){se();let t=[];try{t=Ve.readdirSync(it)}catch{return[]}let n=[];for(let o of t){if(!o.endsWith(".meta.json"))continue;let r=Uo(Ia.join(it,o));r&&n.push(r)}return n.sort((o,r)=>o.startedAt<r.startedAt?1:-1),n}tailLog(t,n){let o=this.logPath(t);if(!Ve.existsSync(o))return"(no log file)";let s=Ve.readFileSync(o,"utf8").split(`
262
+ `)}catch{}});let f={...Zo(i)??u,status:"done",exitCode:null,signal:null,finishedAt:new Date().toISOString()};this.finishJob(i,u,f,o)}),{id:r,meta:u}}list(){ie();let t=[];try{t=Xe.readdirSync(it)}catch{return[]}let n=[];for(let o of t){if(!o.endsWith(".meta.json"))continue;let r=Zo(rl.join(it,o));r&&n.push(r)}return n.sort((o,r)=>o.startedAt<r.startedAt?1:-1),n}tailLog(t,n){let o=this.logPath(t);if(!Xe.existsSync(o))return"(no log file)";let s=Xe.readFileSync(o,"utf8").split(`
245
263
  `);return s.slice(Math.max(0,s.length-n)).join(`
246
- `).trimEnd()||"(empty log)"}readSince(t,n){let o=this.logPath(t);if(!Ve.existsSync(o))return{text:"",nextOffset:n};let s=Ve.statSync(o).size;if(n>=s)return{text:"",nextOffset:s};let i=Buffer.alloc(s-n),a=Ve.openSync(o,"r");try{Ve.readSync(a,i,0,i.length,n)}finally{Ve.closeSync(a)}return{text:i.toString("utf8"),nextOffset:s}}resolveJobRef(t){return Qy(this.list(),t)}kill(t){let n=this.metaPath(t),o=Uo(n);if(!o)return`Unknown job id: ${t}`;let r=this.running.get(t);if(r&&!r.killed)return r.kill("SIGTERM"),Qn(n,{...o,status:"killed",signal:"SIGTERM"}),`Sent SIGTERM to job ${t} (pid ${o.pid??"?"})`;if(o.pid)try{return process.kill(o.pid,"SIGTERM"),Qn(n,{...o,status:"killed",signal:"SIGTERM"}),`Sent SIGTERM to pid ${o.pid} (${t})`}catch{return`Job ${t} is not running (no live process).`}return`Job ${t} is not running.`}killAllRunning(){for(let[t,n]of this.running){n.killed||n.kill("SIGTERM");let o=Uo(this.metaPath(t));o&&Qn(this.metaPath(t),{...o,status:"killed",signal:"SIGTERM"})}this.running.clear()}};import Ew from"node:fs";import{Bot as Aw,InputFile as Iw}from"grammy";G();import Kd from"node:fs";import Vn from"node:path";function Vy(e){return e.replace(/[^a-zA-Z0-9._-]+/g,"_").slice(0,120)||"unknown"}function Xy(e){let o=Vn.basename(e.trim()||"file").replace(/[^a-zA-Z0-9._-]+/g,"_").replace(/^\.+/,"").slice(0,180);return o.length>0?o:"file"}function Zy(e,t){let n=new Date().toISOString().slice(0,10),o=Vy(t);return Vn.join(e,o,n)}function Xn(e,t,n){let o=Zy(e,t);B(o);let r=Xy(n),s=Vn.join(o,r);if(!Kd.existsSync(s))return s;let i=Vn.extname(r),a=i?r.slice(0,-i.length):r;for(let l=1;l<1e4;l+=1){let d=`${a}-${l}${i}`;if(s=Vn.join(o,d),!Kd.existsSync(s))return s}return Vn.join(o,`${a}-${Date.now()}${i}`)}xe();import ew from"node:dns";import tw from"node:https";import{URL as nw}from"node:url";function Zn(e){if(e instanceof Error){let t=e.cause;return t!=null?`${e.message} (${String(t)})`:e.message}return String(e)}xe();function Yd(e){if("photo"in e&&e.photo&&e.photo.length>0)return{fileId:e.photo[e.photo.length-1].file_id,baseName:"photo.jpg"};if("document"in e&&e.document){let t=e.document,n=t.file_name?.trim();return{fileId:t.file_id,baseName:n&&n.length>0?n:"document.bin"}}if("video"in e&&e.video){let t=e.video,n=t.file_name?.trim();return{fileId:t.file_id,baseName:n&&n.length>0?n:"video.mp4"}}if("audio"in e&&e.audio){let t=e.audio,n=t.file_name?.trim();return{fileId:t.file_id,baseName:n&&n.length>0?n:"audio.m4a"}}if("voice"in e&&e.voice)return{fileId:e.voice.file_id,baseName:"voice.ogg"};if("video_note"in e&&e.video_note)return{fileId:e.video_note.file_id,baseName:"video_note.mp4"};if("sticker"in e&&e.sticker){let t=e.sticker,n=t.is_video?"webm":"webp";return{fileId:t.file_id,baseName:`sticker.${n}`}}return null}function ow(e,t){let o=t.split("/").filter(r=>r.length>0).map(encodeURIComponent).join("/");return`https://api.telegram.org/file/bot${e}/${o}`}var Qd="omnish (Telegram bot; https://github.com/labKnowledge/whatsLive)";function rw(e){let t=new nw(e);return new Promise((n,o)=>{let r=tw.request({hostname:t.hostname,port:t.port||443,path:t.pathname+t.search,method:"GET",headers:{"User-Agent":Qd,Accept:"*/*"},lookup(s,i,a){ew.lookup(s,{family:4},a)}},s=>{let i=s.statusCode??0,a=[];s.on("data",l=>a.push(l)),s.on("end",()=>n({status:i,buffer:Buffer.concat(a)})),s.on("error",o)});r.on("error",o),r.end()})}async function Vd(e,t,n){let o;try{o=await e.api.getFile(t)}catch(i){return P.warn({err:String(i),fileId:t},"telegram getFile failed"),{error:"Could not resolve file on Telegram."}}if(!o.file_path)return{error:"Telegram did not return a file path."};let r=ow(e.token,o.file_path),s;try{let i=await fetch(r,{headers:{"User-Agent":Qd,Accept:"*/*"}});if(!i.ok)return P.warn({status:i.status,statusText:i.statusText,fileId:t},"telegram file fetch HTTP error"),{error:`Could not download file from Telegram (HTTP ${i.status}${i.statusText?` ${i.statusText}`:""}).`};try{s=Buffer.from(await i.arrayBuffer())}catch(a){return P.warn({err:String(a),fileId:t},"telegram file read body failed"),{error:`Could not read file from Telegram (${String(a)}).`}}}catch(i){P.warn({err:String(i),fileId:t},"telegram file fetch failed; retrying via HTTPS IPv4");try{let a=await rw(r);if(a.status<200||a.status>=300)return P.warn({status:a.status,fileId:t},"telegram HTTPS IPv4 fallback HTTP error"),{error:`Could not download file from Telegram (HTTP ${a.status}).`};s=a.buffer}catch(a){return P.warn({err:String(i),err2:String(a),fileId:t},"telegram file download failed (fetch and IPv4 fallback)"),{error:`Could not download file from Telegram (network: ${Zn(i)}; IPv4 fallback: ${Zn(a)}).`}}}return n>0&&s.byteLength>n?{error:`Media too large (max ${n} bytes).`}:{buffer:s}}pe();Xe();import{downloadMediaMessage as lw,extensionForMediaMessage as cw,getContentType as hs,isJidGroup as tp,isLidUser as uw}from"@whiskeysockets/baileys";import dw from"node:fs";xe();var nn=new Map;function sw(){let e=Date.now();for(let[t,n]of nn)e-n>6e5&&nn.delete(t);for(;nn.size>500;){let t=nn.keys().next().value;if(t===void 0)break;nn.delete(t)}}function iw(e){!e||typeof e!="string"||(sw(),nn.set(e,Date.now()))}function La(e){iw(e?.key?.id??void 0)}function aw(e){if(!e)return!1;let t=nn.get(e);return t===void 0?!1:Date.now()-t>6e5?(nn.delete(e),!1):!0}function Xd(e){return!aw(e)}function pw(e){if(!e||typeof e!="object")return"";let t=e;if(typeof t.conversation=="string")return t.conversation;let n=t.extendedTextMessage;return n&&typeof n.text=="string"?n.text:""}function mw(e){if(!e||typeof e!="object")return"";let t=e;for(let n of["imageMessage","videoMessage","documentMessage","audioMessage","stickerMessage"]){let o=t[n];if(o&&typeof o.caption=="string"&&o.caption.trim())return o.caption}return""}function np(e){return[pw(e),mw(e)].filter(n=>n.trim().length>0).join(`
247
- `).trim()}function Oa(e){return e.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g,"").replace(/\uFEFF/g,"").trim()}function op(e){let t=hs(e??void 0);return t==="imageMessage"||t==="videoMessage"||t==="audioMessage"||t==="documentMessage"||t==="stickerMessage"}function hw(e){let t=hs(e??void 0);if(!t)return;let o=e?.[t]?.fileLength;if(typeof o=="number"&&Number.isFinite(o))return o;if(o&&typeof o=="object"&&"toNumber"in o&&typeof o.toNumber=="function")try{let r=o.toNumber();return Number.isFinite(r)?r:void 0}catch{return}}function Zd(e){try{if(!e)return"file.bin";let t=cw(e);return`file${t&&t.length>0?t.startsWith(".")?t:`.${t}`:".bin"}`}catch{return`${(hs(e??void 0)??"file").replace("Message","")||"file"}.bin`}}function fw(e){let t=hs(e??void 0);if(!t)return Zd(e);let n=e?.[t];return n?.fileName&&typeof n.fileName=="string"&&n.fileName.trim()?n.fileName.trim():Zd(e)}async function ep(e,t,n,o){let r=t.message??void 0;if(!op(r))return{};let s=o.fileReceiveMaxBytes,i=hw(r);if(s>0&&i!==void 0&&i>s)return{error:`Media too large (max ${s} bytes).`};let a;try{a=await lw(t,"buffer",{},{logger:e.logger,reuploadRequest:e.updateMediaMessage})}catch(c){return P.warn({err:String(c),detail:Zn(c)},"whatsapp downloadMediaMessage failed"),{error:`Could not download media from WhatsApp (${Zn(c)}).`}}if(s>0&&a.length>s)return{error:`Media too large (max ${s} bytes).`};let l;try{l=Jt(o,n)}catch(c){return{error:String(c)}}let d=fw(r),u=Xn(l,n,d);try{dw.writeFileSync(u,a,{mode:384})}catch{return{error:"Could not write media to inbox."}}return{path:u}}async function gw(e,t){let n=t.remoteJid??"";if(!n)return{fromJid:"",fromE164:""};if(t.remoteJidAlt){let o=te(t.remoteJidAlt)??te(n)??"";return{fromJid:n,fromE164:o}}if(uw(n))try{let o=await e.signalRepository?.lidMapping?.getPNForLID?.(n);if(o){let r=te(o)??"";if(r)return{fromJid:n,fromE164:r}}}catch{}return{fromJid:n,fromE164:te(n)??""}}function yw(e){try{if(!S().clusterEnabled)return;let n=np(e.message??void 0);if(!n)return;let o=Hu(n);if(!o)return;let r=e.key?.remoteJid??"",s=null;if(r&&!tp(r)){let i=te(r);i&&(s=`wa:${i}`)}Vu(o,s)}catch(t){P.warn({err:String(t)},"cluster footer observation failed")}}function rp(e,t){let n=o=>{o.type==="notify"&&(async()=>{for(let r of o.messages){let s=r.key;if(!s||s.fromMe&&(yw(r),!Xd(s.id)))continue;let i=s.remoteJid;if(!i||tp(i)||i.toLowerCase().endsWith("@status")||i==="status@broadcast")continue;let l=Oa(np(r.message??void 0)),{fromJid:d,fromE164:u}=await gw(e,s),c=`wa:${d}`,m=op(r.message??void 0);if(m&&!l){(async()=>{try{let g=S(),y=await ep(e,r,c,g);await t({fromJid:d,fromE164:u,text:"",messageId:s.id??void 0,mediaSavedPath:y.path,mediaError:y.error})}catch(g){P.error({err:String(g),fromJid:d},"whatsapp media-only background task failed")}})();continue}let h,f;if(m){let g=S(),y=await ep(e,r,c,g);h=y.path,f=y.error}!l&&!h&&!f||await t({fromJid:d,fromE164:u,text:l,messageId:s.id??void 0,mediaSavedPath:h,mediaError:f})}})()};return e.ev.on("messages.upsert",n),()=>{e.ev.off("messages.upsert",n)}}xe();import Rw from"node:fs";import ww from"node:process";import bw,{DisconnectReason as vn,fetchLatestBaileysVersion as kw,makeCacheableSignalKeyStore as vw,useMultiFileAuthState as Sw}from"@whiskeysockets/baileys";import xw from"qrcode-terminal";G();xe();var Cw="0.1.0";function Na(e){return e?.error?.output?.statusCode}async function fs(e={}){se();let t=e.authDir??le,n=e.verbose===!0,o=ic(),{state:r,saveCreds:s}=await Sw(t),{version:i}=await kw(),a=bw({version:i,logger:o,printQRInTerminal:!1,browser:["omnish","cli",Cw],auth:{creds:r.creds,keys:vw(r.keys,o)},syncFullHistory:!1,markOnlineOnConnect:!1});return a.ev.on("creds.update",s),a.ev.on("connection.update",l=>{let{connection:d,lastDisconnect:u,qr:c}=l;if(c&&(e.onQr?.(c),e.printQr)){let m=ww.stdout,h=w(m,"\xB7".repeat(42));console.log(Q(m,"Scan with WhatsApp \u2192 Linked devices")),console.log(h),xw.generate(c,{small:!0}),console.log(h)}if(d==="close"){let m=Na(u);n&&m===vn.loggedOut&&o.warn("WhatsApp session logged out (401).")}d==="open"&&n&&o.info("WhatsApp Web connected.")}),a.ws&&typeof a.ws.on=="function"&&a.ws.on("error",l=>{o.error({err:String(l)},"WebSocket error")}),a}function eo(e){try{e.end(new Error("omnish: socket closed"))}catch{e.ws?.close()}}function Fa(e){return e?.output?.statusCode??e?.status??e?.error?.output?.statusCode}function gs(e){return e.user?.id?Promise.resolve():new Promise((t,n)=>{let o=r=>{if(r.connection==="open"){e.ev.off("connection.update",o),t();return}if(r.connection==="close"){e.ev.off("connection.update",o);let i=r.lastDisconnect?.error??r.lastDisconnect??new Error("Connection closed");n(i)}};e.ev.on("connection.update",o)})}function Sn(e,t,n){return new Promise((o,r)=>{let s=setTimeout(()=>r(new Error(n)),t);e.then(i=>{clearTimeout(s),o(i)},i=>{clearTimeout(s),r(i)})})}var cp=3500,Tw=400,sp=18e4,ip=9e4,ap=3e5;function _a(e,t=cp){if(e.length<=t)return[e];let n=[],o=0;for(;o<e.length;){let r=Math.min(o+t,e.length);if(r<e.length){let i=e.slice(o,r).lastIndexOf(`
264
+ `).trimEnd()||"(empty log)"}readSince(t,n){let o=this.logPath(t);if(!Xe.existsSync(o))return{text:"",nextOffset:n};let s=Xe.statSync(o).size;if(n>=s)return{text:"",nextOffset:s};let i=Buffer.alloc(s-n),a=Xe.openSync(o,"r");try{Xe.readSync(a,i,0,i.length,n)}finally{Xe.closeSync(a)}return{text:i.toString("utf8"),nextOffset:s}}resolveJobRef(t){return cb(this.list(),t)}kill(t){let n=this.metaPath(t),o=Zo(n);if(!o)return`Unknown job id: ${t}`;let r=this.running.get(t);if(r&&!r.killed)return r.kill("SIGTERM"),Xo(n,{...o,status:"killed",signal:"SIGTERM"}),`Sent SIGTERM to job ${t} (pid ${o.pid??"?"})`;if(o.pid)try{return process.kill(o.pid,"SIGTERM"),Xo(n,{...o,status:"killed",signal:"SIGTERM"}),`Sent SIGTERM to pid ${o.pid} (${t})`}catch{return`Job ${t} is not running (no live process).`}return`Job ${t} is not running.`}killAllRunning(){for(let[t,n]of this.running){n.killed||n.kill("SIGTERM");let o=Zo(this.metaPath(t));o&&Xo(this.metaPath(t),{...o,status:"killed",signal:"SIGTERM"})}this.running.clear()}};import jb from"node:fs";import{Bot as Gb,InputFile as Jb}from"grammy";q();import Op from"node:fs";import po from"node:path";function ub(e){return e.replace(/[^a-zA-Z0-9._-]+/g,"_").slice(0,120)||"unknown"}function db(e){let o=po.basename(e.trim()||"file").replace(/[^a-zA-Z0-9._-]+/g,"_").replace(/^\.+/,"").slice(0,180);return o.length>0?o:"file"}function pb(e,t){let n=new Date().toISOString().slice(0,10),o=ub(t);return po.join(e,o,n)}function mo(e,t,n){let o=pb(e,t);H(o);let r=db(n),s=po.join(o,r);if(!Op.existsSync(s))return s;let i=po.extname(r),a=i?r.slice(0,-i.length):r;for(let l=1;l<1e4;l+=1){let c=`${a}-${l}${i}`;if(s=po.join(o,c),!Op.existsSync(s))return s}return po.join(o,`${a}-${Date.now()}${i}`)}Ce();import mb from"node:dns";import hb from"node:https";import{URL as fb}from"node:url";function ho(e){if(e instanceof Error){let t=e.cause;return t!=null?`${e.message} (${String(t)})`:e.message}return String(e)}Ce();function Np(e){if("photo"in e&&e.photo&&e.photo.length>0)return{fileId:e.photo[e.photo.length-1].file_id,baseName:"photo.jpg"};if("document"in e&&e.document){let t=e.document,n=t.file_name?.trim();return{fileId:t.file_id,baseName:n&&n.length>0?n:"document.bin"}}if("video"in e&&e.video){let t=e.video,n=t.file_name?.trim();return{fileId:t.file_id,baseName:n&&n.length>0?n:"video.mp4"}}if("audio"in e&&e.audio){let t=e.audio,n=t.file_name?.trim();return{fileId:t.file_id,baseName:n&&n.length>0?n:"audio.m4a"}}if("voice"in e&&e.voice)return{fileId:e.voice.file_id,baseName:"voice.ogg"};if("video_note"in e&&e.video_note)return{fileId:e.video_note.file_id,baseName:"video_note.mp4"};if("sticker"in e&&e.sticker){let t=e.sticker,n=t.is_video?"webm":"webp";return{fileId:t.file_id,baseName:`sticker.${n}`}}return null}function gb(e,t){let o=t.split("/").filter(r=>r.length>0).map(encodeURIComponent).join("/");return`https://api.telegram.org/file/bot${e}/${o}`}var Fp="omnish (Telegram bot; https://github.com/labKnowledge/whatsLive)";function yb(e){let t=new fb(e);return new Promise((n,o)=>{let r=hb.request({hostname:t.hostname,port:t.port||443,path:t.pathname+t.search,method:"GET",headers:{"User-Agent":Fp,Accept:"*/*"},lookup(s,i,a){mb.lookup(s,{family:4},a)}},s=>{let i=s.statusCode??0,a=[];s.on("data",l=>a.push(l)),s.on("end",()=>n({status:i,buffer:Buffer.concat(a)})),s.on("error",o)});r.on("error",o),r.end()})}async function Wp(e,t,n){let o;try{o=await e.api.getFile(t)}catch(i){return M.warn({err:String(i),fileId:t},"telegram getFile failed"),{error:"Could not resolve file on Telegram."}}if(!o.file_path)return{error:"Telegram did not return a file path."};let r=gb(e.token,o.file_path),s;try{let i=await fetch(r,{headers:{"User-Agent":Fp,Accept:"*/*"}});if(!i.ok)return M.warn({status:i.status,statusText:i.statusText,fileId:t},"telegram file fetch HTTP error"),{error:`Could not download file from Telegram (HTTP ${i.status}${i.statusText?` ${i.statusText}`:""}).`};try{s=Buffer.from(await i.arrayBuffer())}catch(a){return M.warn({err:String(a),fileId:t},"telegram file read body failed"),{error:`Could not read file from Telegram (${String(a)}).`}}}catch(i){M.warn({err:String(i),fileId:t},"telegram file fetch failed; retrying via HTTPS IPv4");try{let a=await yb(r);if(a.status<200||a.status>=300)return M.warn({status:a.status,fileId:t},"telegram HTTPS IPv4 fallback HTTP error"),{error:`Could not download file from Telegram (HTTP ${a.status}).`};s=a.buffer}catch(a){return M.warn({err:String(i),err2:String(a),fileId:t},"telegram file download failed (fetch and IPv4 fallback)"),{error:`Could not download file from Telegram (network: ${ho(i)}; IPv4 fallback: ${ho(a)}).`}}}return n>0&&s.byteLength>n?{error:`Media too large (max ${n} bytes).`}:{buffer:s}}pe();Ze();import{downloadMediaMessage as vb,extensionForMediaMessage as Sb,getContentType as Ls,isJidGroup as Bp,isLidUser as xb}from"@whiskeysockets/baileys";import Cb from"node:fs";Ce();var dn=new Map;function wb(){let e=Date.now();for(let[t,n]of dn)e-n>6e5&&dn.delete(t);for(;dn.size>500;){let t=dn.keys().next().value;if(t===void 0)break;dn.delete(t)}}function bb(e){!e||typeof e!="string"||(wb(),dn.set(e,Date.now()))}function sl(e){bb(e?.key?.id??void 0)}function kb(e){if(!e)return!1;let t=dn.get(e);return t===void 0?!1:Date.now()-t>6e5?(dn.delete(e),!1):!0}function _p(e){return!kb(e)}function Rb(e){if(!e||typeof e!="object")return"";let t=e;if(typeof t.conversation=="string")return t.conversation;let n=t.extendedTextMessage;return n&&typeof n.text=="string"?n.text:""}function Tb(e){if(!e||typeof e!="object")return"";let t=e;for(let n of["imageMessage","videoMessage","documentMessage","audioMessage","stickerMessage"]){let o=t[n];if(o&&typeof o.caption=="string"&&o.caption.trim())return o.caption}return""}function Hp(e){return[Rb(e),Tb(e)].filter(n=>n.trim().length>0).join(`
265
+ `).trim()}function il(e){return e.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g,"").replace(/\uFEFF/g,"").trim()}function jp(e){let t=Ls(e??void 0);return t==="imageMessage"||t==="videoMessage"||t==="audioMessage"||t==="documentMessage"||t==="stickerMessage"}function $b(e){let t=Ls(e??void 0);if(!t)return;let o=e?.[t]?.fileLength;if(typeof o=="number"&&Number.isFinite(o))return o;if(o&&typeof o=="object"&&"toNumber"in o&&typeof o.toNumber=="function")try{let r=o.toNumber();return Number.isFinite(r)?r:void 0}catch{return}}function Dp(e){try{if(!e)return"file.bin";let t=Sb(e);return`file${t&&t.length>0?t.startsWith(".")?t:`.${t}`:".bin"}`}catch{return`${(Ls(e??void 0)??"file").replace("Message","")||"file"}.bin`}}function Mb(e){let t=Ls(e??void 0);if(!t)return Dp(e);let n=e?.[t];return n?.fileName&&typeof n.fileName=="string"&&n.fileName.trim()?n.fileName.trim():Dp(e)}async function Up(e,t,n,o){let r=t.message??void 0;if(!jp(r))return{};let s=o.fileReceiveMaxBytes,i=$b(r);if(s>0&&i!==void 0&&i>s)return{error:`Media too large (max ${s} bytes).`};let a;try{a=await vb(t,"buffer",{},{logger:e.logger,reuploadRequest:e.updateMediaMessage})}catch(u){return M.warn({err:String(u),detail:ho(u)},"whatsapp downloadMediaMessage failed"),{error:`Could not download media from WhatsApp (${ho(u)}).`}}if(s>0&&a.length>s)return{error:`Media too large (max ${s} bytes).`};let l;try{l=Qt(o,n)}catch(u){return{error:String(u)}}let c=Mb(r),d=mo(l,n,c);try{Cb.writeFileSync(d,a,{mode:384})}catch{return{error:"Could not write media to inbox."}}return{path:d}}async function Pb(e,t){let n=t.remoteJid??"";if(!n)return{fromJid:"",fromE164:""};if(t.remoteJidAlt){let o=ne(t.remoteJidAlt)??ne(n)??"";return{fromJid:n,fromE164:o}}if(xb(n))try{let o=await e.signalRepository?.lidMapping?.getPNForLID?.(n);if(o){let r=ne(o)??"";if(r)return{fromJid:n,fromE164:r}}}catch{}return{fromJid:n,fromE164:ne(n)??""}}function Eb(e){try{if(!S().clusterEnabled)return;let n=Hp(e.message??void 0);if(!n)return;let o=dd(n);if(!o)return;let r=e.key?.remoteJid??"",s=null;if(r&&!Bp(r)){let i=ne(r);i&&(s=`wa:${i}`)}kd(o,s)}catch(t){M.warn({err:String(t)},"cluster footer observation failed")}}function Gp(e,t){let n=o=>{o.type==="notify"&&(async()=>{for(let r of o.messages){let s=r.key;if(!s||s.fromMe&&(Eb(r),!_p(s.id)))continue;let i=s.remoteJid;if(!i||Bp(i)||i.toLowerCase().endsWith("@status")||i==="status@broadcast")continue;let l=il(Hp(r.message??void 0)),{fromJid:c,fromE164:d}=await Pb(e,s),u=`wa:${c}`,m=jp(r.message??void 0);if(m&&!l){(async()=>{try{let g=S(),y=await Up(e,r,u,g);await t({fromJid:c,fromE164:d,text:"",messageId:s.id??void 0,mediaSavedPath:y.path,mediaError:y.error})}catch(g){M.error({err:String(g),fromJid:c},"whatsapp media-only background task failed")}})();continue}let h,f;if(m){let g=S(),y=await Up(e,r,u,g);h=y.path,f=y.error}!l&&!h&&!f||await t({fromJid:c,fromE164:d,text:l,messageId:s.id??void 0,mediaSavedPath:h,mediaError:f})}})()};return e.ev.on("messages.upsert",n),()=>{e.ev.off("messages.upsert",n)}}Ce();import _b from"node:fs";import Ab from"node:process";import Ib,{DisconnectReason as En,fetchLatestBaileysVersion as Lb,makeCacheableSignalKeyStore as Ob,useMultiFileAuthState as Nb}from"@whiskeysockets/baileys";import Fb from"qrcode-terminal";q();Ce();var Wb="0.1.0";function al(e){return e?.error?.output?.statusCode}async function Os(e={}){ie();let t=e.authDir??ce,n=e.verbose===!0,o=Pc(),{state:r,saveCreds:s}=await Nb(t),{version:i}=await Lb(),a=Ib({version:i,logger:o,printQRInTerminal:!1,browser:["omnish","cli",Wb],auth:{creds:r.creds,keys:Ob(r.keys,o)},syncFullHistory:!1,markOnlineOnConnect:!1});return a.ev.on("creds.update",s),a.ev.on("connection.update",l=>{let{connection:c,lastDisconnect:d,qr:u}=l;if(u&&(e.onQr?.(u),e.printQr)){let m=Ab.stdout,h=w(m,"\xB7".repeat(42));console.log(X(m,"Scan with WhatsApp \u2192 Linked devices")),console.log(h),Fb.generate(u,{small:!0}),console.log(h)}if(c==="close"){let m=al(d);n&&m===En.loggedOut&&o.warn("WhatsApp session logged out (401).")}c==="open"&&n&&o.info("WhatsApp Web connected.")}),a.ws&&typeof a.ws.on=="function"&&a.ws.on("error",l=>{o.error({err:String(l)},"WebSocket error")}),a}function fo(e){try{e.end(new Error("omnish: socket closed"))}catch{e.ws?.close()}}function ll(e){return e?.output?.statusCode??e?.status??e?.error?.output?.statusCode}function Ns(e){return e.user?.id?Promise.resolve():new Promise((t,n)=>{let o=r=>{if(r.connection==="open"){e.ev.off("connection.update",o),t();return}if(r.connection==="close"){e.ev.off("connection.update",o);let i=r.lastDisconnect?.error??r.lastDisconnect??new Error("Connection closed");n(i)}};e.ev.on("connection.update",o)})}function An(e,t,n){return new Promise((o,r)=>{let s=setTimeout(()=>r(new Error(n)),t);e.then(i=>{clearTimeout(s),o(i)},i=>{clearTimeout(s),r(i)})})}var Yp=3500,Db=400,Jp=18e4,qp=9e4,zp=3e5;function cl(e,t=Yp){if(e.length<=t)return[e];let n=[],o=0;for(;o<e.length;){let r=Math.min(o+t,e.length);if(r<e.length){let i=e.slice(o,r).lastIndexOf(`
248
266
 
249
- `);i>Math.floor(t*.35)&&(r=o+i+2)}n.push(e.slice(o,r)),o=r}return n}function ys(e){return new Promise(t=>setTimeout(t,e))}async function lp(e,t){await Promise.race([e,ys(sp).then(()=>{P.warn({jid:t,ms:sp},"whatsapp outbound self-heal: prior send chain waited too long; continuing")})])}async function $w(e,t,n,o=3){let r;for(let s=1;s<=o;s+=1)try{let i=await Sn(e.sendMessage(t,{text:n}),ip,`whatsapp sendMessage timed out after ${ip}ms`);La(i);return}catch(i){r=i;let a=String(i);if(/not connected|closed|timed out|timeout/i.test(a)&&s<o){await ys(800*s);continue}throw i}throw r}function Mw(e,t){let n=e.caption;switch(e.category){case"image":return{image:t,caption:n,mimetype:e.mimetype};case"video":return{video:t,caption:n,mimetype:e.mimetype};case"audio":return{audio:t,mimetype:e.mimetype,ptt:!1};case"document":return{document:t,mimetype:e.mimetype,fileName:e.displayName,caption:n}}}async function Pw(e,t,n,o=3){let r;for(let s=1;s<=o;s+=1)try{let i=await Sn(e.sendMessage(t,n),ap,`whatsapp sendMedia timed out after ${ap}ms`);La(i);return}catch(i){r=i;let a=String(i);if(/not connected|closed|timed out|timeout/i.test(a)&&s<o){await ys(800*s);continue}throw i}throw r}function up(e,t={}){let n=new Map,o=t.decorate??(r=>r);return{async sendText(r,s){let i=o(s,r),a=n.get(r)??Promise.resolve(),l=lp(a,r).then(async()=>{let d=_a(i,cp);for(let u=0;u<d.length;u+=1)await $w(e,r,d[u]??""),u<d.length-1&&await ys(Tw)}).catch(d=>{P.error({err:String(d),jid:r},"sendText failed")});n.set(r,l),await l.finally(()=>{n.get(r)===l&&n.delete(r)})},async sendMedia(r,s){let i=Rw.readFileSync(s.absPath),a=Mw(s,i),l=n.get(r)??Promise.resolve(),d=lp(l,r).then(async()=>{await Pw(e,r,a)}).catch(u=>{P.error({err:String(u),jid:r},"sendMedia failed")});n.set(r,d),await d.finally(()=>{n.get(r)===d&&n.delete(r)})}}}function dp(e){let t=e.trim();return t?/^\/id(?:@[\w_]+)?$/i.test(t):!1}function pp(e){let t=String(e).replace(/\D/g,"");return`Your Telegram user id: ${t}
267
+ `);i>Math.floor(t*.35)&&(r=o+i+2)}n.push(e.slice(o,r)),o=r}return n}function Fs(e){return new Promise(t=>setTimeout(t,e))}async function Kp(e,t){await Promise.race([e,Fs(Jp).then(()=>{M.warn({jid:t,ms:Jp},"whatsapp outbound self-heal: prior send chain waited too long; continuing")})])}async function Ub(e,t,n,o=3){let r;for(let s=1;s<=o;s+=1)try{let i=await An(e.sendMessage(t,{text:n}),qp,`whatsapp sendMessage timed out after ${qp}ms`);sl(i);return}catch(i){r=i;let a=String(i);if(/not connected|closed|timed out|timeout/i.test(a)&&s<o){await Fs(800*s);continue}throw i}throw r}function Bb(e,t){let n=e.caption;switch(e.category){case"image":return{image:t,caption:n,mimetype:e.mimetype};case"video":return{video:t,caption:n,mimetype:e.mimetype};case"audio":return{audio:t,mimetype:e.mimetype,ptt:!1};case"document":return{document:t,mimetype:e.mimetype,fileName:e.displayName,caption:n}}}async function Hb(e,t,n,o=3){let r;for(let s=1;s<=o;s+=1)try{let i=await An(e.sendMessage(t,n),zp,`whatsapp sendMedia timed out after ${zp}ms`);sl(i);return}catch(i){r=i;let a=String(i);if(/not connected|closed|timed out|timeout/i.test(a)&&s<o){await Fs(800*s);continue}throw i}throw r}function Qp(e,t={}){let n=new Map,o=t.decorate??(r=>r);return{async sendText(r,s){let i=o(s,r),a=n.get(r)??Promise.resolve(),l=Kp(a,r).then(async()=>{let c=cl(i,Yp);for(let d=0;d<c.length;d+=1)await Ub(e,r,c[d]??""),d<c.length-1&&await Fs(Db)}).catch(c=>{M.error({err:String(c),jid:r},"sendText failed")});n.set(r,l),await l.finally(()=>{n.get(r)===l&&n.delete(r)})},async sendMedia(r,s){let i=_b.readFileSync(s.absPath),a=Bb(s,i),l=n.get(r)??Promise.resolve(),c=Kp(l,r).then(async()=>{await Hb(e,r,a)}).catch(d=>{M.error({err:String(d),jid:r},"sendMedia failed")});n.set(r,c),await c.finally(()=>{n.get(r)===c&&n.delete(r)})}}}function Vp(e){let t=e.trim();return t?/^\/id(?:@[\w_]+)?$/i.test(t):!1}function Xp(e){let t=String(e).replace(/\D/g,"");return`Your Telegram user id: ${t}
250
268
  Add to allowlist on this gateway host:
251
- omnish allow tg:${t}`}var Lw=400;async function mp(e,t,n,o){let r=t(),s=await Vd(e,o.fileId,r.fileReceiveMaxBytes);if("error"in s)return{mediaError:s.error};let i;try{i=Jt(r,n)}catch(l){return{mediaError:String(l)}}let a=Xn(i,n,o.baseName);try{return Ew.writeFileSync(a,s.buffer,{mode:384}),{mediaSavedPath:a}}catch{return{mediaError:"Could not write media to inbox."}}}function Ow(e){return new Promise(t=>setTimeout(t,e))}async function Wa(e,t,n,o={}){let r=new Aw(e);await r.api.deleteWebhook({drop_pending_updates:!1});let s=new Map,i=o.decorate??(u=>u);async function a(u,c){let m=Math.min(t().appsMaxWaChars,4096),h=Te(c,"telegram"),f=i(h.text,ia(u)),g=h.parseModeHtml,b=(s.get(u)??Promise.resolve()).then(async()=>{let k=_a(f,m);for(let M=0;M<k.length;M+=1){let R=k[M]??"",L=g?{parse_mode:"HTML"}:void 0;try{await r.api.sendMessage(u,R,L)}catch($){if(g){P.warn({err:String($),chatId:u},"telegram HTML send failed; retrying plain");let N=R.replace(/<[^>]+>/g,"");await r.api.sendMessage(u,N)}else throw $}M<k.length-1&&await Ow(Lw)}}).catch(k=>{P.error({err:String(k),chatId:u},"telegram sendText failed")});s.set(u,b),await b.finally(()=>{s.get(u)===b&&s.delete(u)})}async function l(u,c){let m=new Iw(c.absPath,c.displayName),h=c.caption,g=(s.get(u)??Promise.resolve()).then(async()=>{switch(c.category){case"image":await r.api.sendPhoto(u,m,h?{caption:h}:void 0);break;case"video":await r.api.sendVideo(u,m,h?{caption:h}:void 0);break;case"audio":await r.api.sendAudio(u,m,h?{caption:h}:void 0);break;case"document":await r.api.sendDocument(u,m,h?{caption:h}:void 0);break}}).catch(y=>{P.error({err:String(y),chatId:u},"telegram sendMedia failed")});s.set(u,g),await g.finally(()=>{s.get(u)===g&&s.delete(u)})}r.on("message",async u=>{let c=u.chat,m=u.message;if(!c||c.type!=="private"||!u.from||!m)return;let h="text"in m&&m.text?m.text:"",f="caption"in m&&m.caption?m.caption:"",g=Oa([h,f].filter(Boolean).join(`
252
- `));if(g&&dp(g)){let $=u.from.id;await r.api.sendMessage(c.id,pp($));return}let y=Yd(m),b=ia(c.id),k=c.id,M=async $=>{if($.kind==="file")await l(k,$.spec);else if($.kind==="files")for(let N of $.specs)await l(k,N);else if($.kind==="texts")for(let N of $.bodies)await a(k,N);else await a(k,$.body)};if(y&&!g){(async()=>{try{let $=await mp(r,t,b,y);if(!$.mediaSavedPath&&!$.mediaError)return;await n({peerKey:b,text:"",tgChatId:c.id,tgReplyToMessageId:m.message_id,mediaSavedPath:$.mediaSavedPath,mediaError:$.mediaError},M)}catch($){P.error({err:String($),chatId:k},"telegram media-only background task failed")}})();return}let R,L;if(y){let $=await mp(r,t,b,y);R=$.mediaSavedPath,L=$.mediaError}!g&&!R&&!L||await n({peerKey:b,text:g,tgChatId:c.id,tgReplyToMessageId:m.message_id,mediaSavedPath:R,mediaError:L},M)}),r.catch(u=>{P.error({err:String(u)},"telegram bot error")});let d=r.start();return{bot:r,sendText:a,sendMedia:l,stop:async()=>{await r.stop(),await d.catch(()=>{})}}}Xe();import Nw from"node:fs";import Fw from"node:path";function _w(e){let t=Fw.join(e,"creds.json");try{let n=Nw.readFileSync(t,"utf8"),o=JSON.parse(n);if(!o.me||typeof o.me!="object"||o.me===null)return null;let r=o.me,s=typeof r.id=="string"?r.id:void 0,i=typeof r.phoneNumber=="string"?r.phoneNumber:void 0;return!s&&!i?null:{id:s,phoneNumber:i}}catch{return null}}function hp(e){let t=_w(e);if(!t)return null;let n=t.phoneNumber?.trim();if(n){let s=te(n);return s?`phone ${s}`:`phone ${n}`}let o=t.id?.trim();if(!o)return null;let r=o.toLowerCase();if(r.endsWith("@s.whatsapp.net")||r.endsWith("@c.us")){let s=te(o);return s?`phone ${s}`:`id ${o}`}return r.endsWith("@lid")?`device ${o} (LID \u2014 not an E.164; your number may appear after the gateway runs)`:`id ${o}`}import bs from"node:fs";import rt from"node:process";G();G();import fp from"node:fs";var Ww="Timed out after 10 minutes. Phone stuck on \u201CLogging in\u201D usually means: run `pnpm approve-builds && pnpm install`, check network/firewall, then try `omnish link --force` again.";function ws(e){let t=String(e).toLowerCase();return t.includes("401")||t.includes("logged out")?!0:Fa(e)===vn.loggedOut}function Dw(e){return new Promise((t,n)=>{let o=()=>{n(new Error("Pairing cancelled."))};if(e.aborted){o();return}e.addEventListener("abort",o,{once:!0})})}async function Da(e){let t=!1;for(let n=0;n<3;n++){if(e.signal?.aborted)throw new Error("Pairing cancelled.");let o=await fs({authDir:e.authDir,printQr:e.printQr===!0,verbose:e.verbose===!0,onQr:e.onQr});e.onSocketReady?.(o);try{let r=Sn(gs(o),6e5,Ww);e.signal?await Promise.race([r,Dw(e.signal)]):await r;return}catch(r){if(r instanceof Error&&r.message==="Pairing cancelled.")throw r;if(Fa(r)===vn.restartRequired&&!t){t=!0,e.onRestart515?.(),await new Promise(i=>setTimeout(i,1500));continue}throw r}finally{e.onSocketClosed?.(),eo(o)}}throw new Error("Pairing failed after restart (515) retries.")}async function gp(e){let t=e.authDir??le;se();for(let n=1;n<=2;n++)try{await Da({...e,authDir:t});return}catch(o){if(o instanceof Error&&o.message==="Pairing cancelled.")throw o;if(n===1&&ws(o)){fp.rmSync(t,{recursive:!0,force:!0}),fp.mkdirSync(t,{recursive:!0,mode:448});continue}throw o}}async function Uw(e,t){let n=rt.stdout;console.log(`
253
- ${Ce(n,"omnish link")} ${w(n,"\u2014 QR appears below; scan from WhatsApp \u2192 Linked devices.")}
254
- `),await Da({authDir:e,verbose:t,printQr:!0,onRestart515:()=>{console.warn(`
255
- ${D(rt.stderr,"WhatsApp requested a restart after pairing (code 515). This is normal. Opening a new connection\u2026")}
256
- `)}})}async function yp(e={}){let t=e.authDir??le,n=e.verbose===!0;se(),e.force&&(bs.rmSync(t,{recursive:!0,force:!0}),bs.mkdirSync(t,{recursive:!0,mode:448}),console.log(`${we(rt.stdout,"Cleared saved session (--force).")} ${w(rt.stdout,"Requesting a new QR\u2026")}
257
- `));for(let o=1;o<=2;o++)try{await Uw(t,n),console.log(`
258
- ${me(rt.stdout,"Linked.")} ${v(rt.stdout,"Session saved. You can run")} ${me(rt.stdout,"omnish run")} ${v(rt.stdout,"now.")}
259
- `);return}catch(r){if(o===1&&ws(r)){console.warn(`
260
- ${D(rt.stderr,"WhatsApp returned logged-out (401). This often happens after Ctrl+C during link or corrupt auth files.")}
261
- ${D(rt.stderr,"Clearing auth dir and retrying once with a fresh QR\u2026")}
262
- `),bs.rmSync(t,{recursive:!0,force:!0}),bs.mkdirSync(t,{recursive:!0,mode:448});continue}throw ws(r)&&console.error(`
263
- ${T(rt.stderr,"Still failing after a clean auth directory. Try:")}
264
- ${w(rt.stderr,` pnpm approve-builds && pnpm install
269
+ omnish allow tg:${t}`}var qb=400;async function Zp(e,t,n,o){let r=t(),s=await Wp(e,o.fileId,r.fileReceiveMaxBytes);if("error"in s)return{mediaError:s.error};let i;try{i=Qt(r,n)}catch(l){return{mediaError:String(l)}}let a=mo(i,n,o.baseName);try{return jb.writeFileSync(a,s.buffer,{mode:384}),{mediaSavedPath:a}}catch{return{mediaError:"Could not write media to inbox."}}}function zb(e){return new Promise(t=>setTimeout(t,e))}async function ul(e,t,n,o={}){let r=new Gb(e);await r.api.deleteWebhook({drop_pending_updates:!1});let s=new Map,i=o.decorate??(d=>d);async function a(d,u){let m=Math.min(t().appsMaxWaChars,4096),h=me(u,"telegram"),f=i(h.text,Ra(d)),g=h.parseModeHtml,b=(s.get(d)??Promise.resolve()).then(async()=>{let k=cl(f,m);for(let R=0;R<k.length;R+=1){let T=k[R]??"",L=g?{parse_mode:"HTML"}:void 0;try{await r.api.sendMessage(d,T,L)}catch(x){if(g){M.warn({err:String(x),chatId:d},"telegram HTML send failed; retrying plain");let O=T.replace(/<[^>]+>/g,"");await r.api.sendMessage(d,O)}else throw x}R<k.length-1&&await zb(qb)}}).catch(k=>{M.error({err:String(k),chatId:d},"telegram sendText failed")});s.set(d,b),await b.finally(()=>{s.get(d)===b&&s.delete(d)})}async function l(d,u){let m=new Jb(u.absPath,u.displayName),h=u.caption,g=(s.get(d)??Promise.resolve()).then(async()=>{switch(u.category){case"image":await r.api.sendPhoto(d,m,h?{caption:h}:void 0);break;case"video":await r.api.sendVideo(d,m,h?{caption:h}:void 0);break;case"audio":await r.api.sendAudio(d,m,h?{caption:h}:void 0);break;case"document":await r.api.sendDocument(d,m,h?{caption:h}:void 0);break}}).catch(y=>{M.error({err:String(y),chatId:d},"telegram sendMedia failed")});s.set(d,g),await g.finally(()=>{s.get(d)===g&&s.delete(d)})}r.on("message",async d=>{let u=d.chat,m=d.message;if(!u||u.type!=="private"||!d.from||!m)return;let h="text"in m&&m.text?m.text:"",f="caption"in m&&m.caption?m.caption:"",g=il([h,f].filter(Boolean).join(`
270
+ `));if(g&&Vp(g)){let x=d.from.id;await r.api.sendMessage(u.id,Xp(x));return}let y=Np(m),b=Ra(u.id),k=u.id,R=async x=>{if(x.kind==="file")await l(k,x.spec);else if(x.kind==="files")for(let O of x.specs)await l(k,O);else if(x.kind==="texts")for(let O of x.bodies)await a(k,O);else if(x.kind==="bundle"){for(let O of x.texts??[])await a(k,O);for(let O of x.files??[])await l(k,O)}else x.kind==="text"&&await a(k,x.body)};if(y&&!g){(async()=>{try{let x=await Zp(r,t,b,y);if(!x.mediaSavedPath&&!x.mediaError)return;await n({peerKey:b,text:"",tgChatId:u.id,tgReplyToMessageId:m.message_id,mediaSavedPath:x.mediaSavedPath,mediaError:x.mediaError},R)}catch(x){M.error({err:String(x),chatId:k},"telegram media-only background task failed")}})();return}let T,L;if(y){let x=await Zp(r,t,b,y);T=x.mediaSavedPath,L=x.mediaError}!g&&!T&&!L||await n({peerKey:b,text:g,tgChatId:u.id,tgReplyToMessageId:m.message_id,mediaSavedPath:T,mediaError:L},R)}),r.catch(d=>{M.error({err:String(d)},"telegram bot error")});let c=r.start();return{bot:r,sendText:a,sendMedia:l,stop:async()=>{await r.stop(),await c.catch(()=>{})}}}Ze();import Kb from"node:fs";import Yb from"node:path";function Qb(e){let t=Yb.join(e,"creds.json");try{let n=Kb.readFileSync(t,"utf8"),o=JSON.parse(n);if(!o.me||typeof o.me!="object"||o.me===null)return null;let r=o.me,s=typeof r.id=="string"?r.id:void 0,i=typeof r.phoneNumber=="string"?r.phoneNumber:void 0;return!s&&!i?null:{id:s,phoneNumber:i}}catch{return null}}function em(e){let t=Qb(e);if(!t)return null;let n=t.phoneNumber?.trim();if(n){let s=ne(n);return s?`phone ${s}`:`phone ${n}`}let o=t.id?.trim();if(!o)return null;let r=o.toLowerCase();if(r.endsWith("@s.whatsapp.net")||r.endsWith("@c.us")){let s=ne(o);return s?`phone ${s}`:`id ${o}`}return r.endsWith("@lid")?`device ${o} (LID \u2014 not an E.164; your number may appear after the gateway runs)`:`id ${o}`}import _s from"node:fs";import st from"node:process";q();q();import tm from"node:fs";var Vb="Timed out after 10 minutes. Phone stuck on \u201CLogging in\u201D usually means: run `pnpm approve-builds && pnpm install`, check network/firewall, then try `omnish link --force` again.";function Ws(e){let t=String(e).toLowerCase();return t.includes("401")||t.includes("logged out")?!0:ll(e)===En.loggedOut}function Xb(e){return new Promise((t,n)=>{let o=()=>{n(new Error("Pairing cancelled."))};if(e.aborted){o();return}e.addEventListener("abort",o,{once:!0})})}async function dl(e){let t=!1;for(let n=0;n<3;n++){if(e.signal?.aborted)throw new Error("Pairing cancelled.");let o=await Os({authDir:e.authDir,printQr:e.printQr===!0,verbose:e.verbose===!0,onQr:e.onQr});e.onSocketReady?.(o);try{let r=An(Ns(o),6e5,Vb);e.signal?await Promise.race([r,Xb(e.signal)]):await r;return}catch(r){if(r instanceof Error&&r.message==="Pairing cancelled.")throw r;if(ll(r)===En.restartRequired&&!t){t=!0,e.onRestart515?.(),await new Promise(i=>setTimeout(i,1500));continue}throw r}finally{e.onSocketClosed?.(),fo(o)}}throw new Error("Pairing failed after restart (515) retries.")}async function nm(e){let t=e.authDir??ce;ie();for(let n=1;n<=2;n++)try{await dl({...e,authDir:t});return}catch(o){if(o instanceof Error&&o.message==="Pairing cancelled.")throw o;if(n===1&&Ws(o)){tm.rmSync(t,{recursive:!0,force:!0}),tm.mkdirSync(t,{recursive:!0,mode:448});continue}throw o}}async function Zb(e,t){let n=st.stdout;console.log(`
271
+ ${Re(n,"omnish link")} ${w(n,"\u2014 QR appears below; scan from WhatsApp \u2192 Linked devices.")}
272
+ `),await dl({authDir:e,verbose:t,printQr:!0,onRestart515:()=>{console.warn(`
273
+ ${U(st.stderr,"WhatsApp requested a restart after pairing (code 515). This is normal. Opening a new connection\u2026")}
274
+ `)}})}async function om(e={}){let t=e.authDir??ce,n=e.verbose===!0;ie(),e.force&&(_s.rmSync(t,{recursive:!0,force:!0}),_s.mkdirSync(t,{recursive:!0,mode:448}),console.log(`${ke(st.stdout,"Cleared saved session (--force).")} ${w(st.stdout,"Requesting a new QR\u2026")}
275
+ `));for(let o=1;o<=2;o++)try{await Zb(t,n),console.log(`
276
+ ${ye(st.stdout,"Linked.")} ${v(st.stdout,"Session saved. You can run")} ${ye(st.stdout,"omnish run")} ${v(st.stdout,"now.")}
277
+ `);return}catch(r){if(o===1&&Ws(r)){console.warn(`
278
+ ${U(st.stderr,"WhatsApp returned logged-out (401). This often happens after Ctrl+C during link or corrupt auth files.")}
279
+ ${U(st.stderr,"Clearing auth dir and retrying once with a fresh QR\u2026")}
280
+ `),_s.rmSync(t,{recursive:!0,force:!0}),_s.mkdirSync(t,{recursive:!0,mode:448});continue}throw Ws(r)&&console.error(`
281
+ ${C(st.stderr,"Still failing after a clean auth directory. Try:")}
282
+ ${w(st.stderr,` pnpm approve-builds && pnpm install
265
283
  (pnpm may have skipped Baileys/sharp/protobuf build scripts.)
266
284
  Then: omnish link --force
267
- `)}`),r}}G();import{spawn as Bw,spawnSync as Hw}from"node:child_process";import to from"node:fs";import jw from"node:path";import no from"node:process";function ks(e,t={}){se(),B(jw.dirname(e));let n=no.argv[1];if(!n)return{ok:!1,message:"Cannot resolve entry script; invoke via node path/to/dist/index.js."};let o=["run"];t.verbose&&o.push("-vb");let r=to.openSync(e,"a"),s=Bw(no.execPath,[n,...o],{detached:!0,stdio:["ignore",r,r],env:{...no.env,OMNISH_BACKGROUND_GATEWAY:"1"}});return to.closeSync(r),s.unref(),s.pid?{ok:!0,pid:s.pid}:{ok:!1,message:"Failed to start background gateway."}}function vs(){if(se(),!to.existsSync(de))return{outcome:"no_pidfile"};let e=to.readFileSync(de,"utf8").trim(),t=Number(e);if(!Number.isFinite(t)||t<=0){try{to.unlinkSync(de)}catch{}return{outcome:"invalid_pidfile"}}try{no.kill(t,0)}catch{try{to.unlinkSync(de)}catch{}return{outcome:"stale_cleaned",pid:t}}try{return no.kill(t,"SIGTERM"),{outcome:"sent_signal",pid:t}}catch(n){return no.platform==="win32"&&Hw("taskkill",["/PID",String(t),"/T"],{windowsHide:!0}).status===0?{outcome:"taskkill_ok",pid:t}:{outcome:"failed",message:`could not signal process: ${String(n)}`}}}import xp from"node:crypto";import Ba from"node:fs";import Gw from"node:net";Xe();xe();G();import vp from"node:fs";var wp=Number(process.env.PLATFORM_INBOUND_MEDIA_MAX_BYTES)>0?Number(process.env.PLATFORM_INBOUND_MEDIA_MAX_BYTES):54525952,bp=50;function kp(e){try{return JSON.parse(e)}catch{return null}}function oo(e){return e.fileSendMaxBytes<=0?8388608:Math.min(e.fileSendMaxBytes,8388608)}function Sp(e){if(vp.statSync(e.absPath).size>8388608)throw new Error(`File too large for attached mode (max ${8388608} bytes).`);let n=vp.readFileSync(e.absPath).toString("base64");return{name:e.displayName,mimetype:e.mimetype,category:e.category,dataBase64:n,...e.caption?{caption:e.caption}:{}}}function Ua(e,t,n,o){return t.kind==="texts"?{body:t.bodies.map(s=>Te(s,o).text).filter(s=>s.trim()).join(`
268
-
269
- `),...n?{messageId:n}:{}}:t.kind==="text"?{body:Te(t.body,o).text,...n?{messageId:n}:{}}:t.kind==="file"?{...n?{messageId:n}:{},files:[Sp(t.spec)]}:{...n?{messageId:n}:{},files:t.specs.map(Sp)}}var Bo=null;function qw(){try{let e=Ba.readFileSync(Mn,"utf8"),t=JSON.parse(e);return typeof t.token=="string"?t.token:""}catch{return""}}function Jw(e){Ba.writeFileSync(Mn,JSON.stringify(e,null,2)+`
270
- `,{mode:384})}function zw(){try{Ba.unlinkSync(Mn)}catch{}}async function Kw(e,t){let n=t.getCfg(),o=typeof e.absPath=="string"?e.absPath.trim():"";if(!o)return{ok:!1,error:"Missing absPath."};let r=typeof e.caption=="string"&&e.caption.length>0?e.caption:void 0,s=t.sendPlatformMedia&&!t.getWaOutbound()&&!t.getTgSendMedia()?oo(n):n.fileSendMaxBytes,i=St(o,s);if("error"in i)return{ok:!1,error:i.error};let a={absPath:i.absPath,category:i.category,mimetype:i.mimetype,displayName:i.displayName,caption:r};if(e.channel==="whatsapp"){let l=t.getWaOutbound(),d=typeof e.e164=="string"?e.e164.trim():"";if(!d.startsWith("+"))return{ok:!1,error:"WhatsApp destination must be E.164 (e.g. +15551234567)."};let u=Bt(d);if(!l){let m=t.sendPlatformMedia;if(!m)return{ok:!1,error:"WhatsApp outbound is not connected."};try{return await m(`wa:${u}`,a),{ok:!0}}catch(h){return{ok:!1,error:String(h)}}}let c=n.gatewayMode;if(c!=="whatsapp"&&c!=="both")return{ok:!1,error:"gatewayMode does not include WhatsApp."};try{return await l.sendMedia(u,a),{ok:!0}}catch(m){return{ok:!1,error:String(m)}}}if(e.channel==="telegram"){let l=t.getTgSendMedia();if(!l){let u=t.sendPlatformMedia;if(!u)return{ok:!1,error:"Telegram outbound is not connected."};if(!Number.isFinite(e.chatId))return{ok:!1,error:"Invalid Telegram chat id."};let c=`tg:${e.chatId}`;try{return await u(c,a),{ok:!0}}catch(m){return{ok:!1,error:String(m)}}}let d=n.gatewayMode;if(d!=="telegram"&&d!=="both")return{ok:!1,error:"gatewayMode does not include Telegram."};if(!Number.isFinite(e.chatId))return{ok:!1,error:"Invalid Telegram chat id."};try{return await l(e.chatId,a),{ok:!0}}catch(u){return{ok:!1,error:String(u)}}}return{ok:!1,error:"Unknown channel."}}async function Yw(e,t){let n=t.getCfg(),o=typeof e.text=="string"?e.text.trim():"";if(!o)return{ok:!1,error:"Missing or empty text."};if(e.channel==="whatsapp"){let r=t.getWaOutbound();if(!r){let l=t.sendPlatformText;if(!l)return{ok:!1,error:"WhatsApp outbound is not connected."};let d=typeof e.e164=="string"?e.e164.trim():"";if(!d.startsWith("+"))return{ok:!1,error:"WhatsApp destination must be E.164 (e.g. +15551234567)."};let u=Bt(d);try{return await l(`wa:${u}`,o),{ok:!0}}catch(c){return{ok:!1,error:String(c)}}}let s=n.gatewayMode;if(s!=="whatsapp"&&s!=="both")return{ok:!1,error:"gatewayMode does not include WhatsApp."};let i=typeof e.e164=="string"?e.e164.trim():"";if(!i.startsWith("+"))return{ok:!1,error:"WhatsApp destination must be E.164 (e.g. +15551234567)."};let a=Bt(i);try{return await r.sendText(a,o),{ok:!0}}catch(l){return{ok:!1,error:String(l)}}}if(e.channel==="telegram"){let r=t.getTgSendText();if(!r){let i=t.sendPlatformText;if(!i)return{ok:!1,error:"Telegram outbound is not connected."};if(!Number.isFinite(e.chatId))return{ok:!1,error:"Invalid Telegram chat id."};try{return await i(`tg:${e.chatId}`,o),{ok:!0}}catch(a){return{ok:!1,error:String(a)}}}let s=n.gatewayMode;if(s!=="telegram"&&s!=="both")return{ok:!1,error:"gatewayMode does not include Telegram."};if(!Number.isFinite(e.chatId))return{ok:!1,error:"Invalid Telegram chat id."};try{return await r(e.chatId,p(o)),{ok:!0}}catch(i){return{ok:!1,error:String(i)}}}return{ok:!1,error:"Unknown channel."}}async function Qw(e,t,n){let o;try{o=JSON.parse(e)}catch{return{ok:!1,error:"Invalid JSON."}}if(!o||typeof o!="object")return{ok:!1,error:"Invalid request."};if(typeof o.token!="string"||!n)return{ok:!1,error:"Unauthorized."};try{let r=Buffer.from(o.token,"utf8"),s=Buffer.from(n,"utf8");if(r.length!==s.length||!xp.timingSafeEqual(r,s))return{ok:!1,error:"Unauthorized."}}catch{return{ok:!1,error:"Unauthorized."}}return o.op==="sendMedia"?Kw(o,t):o.op==="sendText"?Yw(o,t):{ok:!1,error:"Unsupported operation."}}function xs(e){if(Bo)return;let t=xp.randomBytes(32).toString("hex"),n=Gw.createServer(o=>{let r="";o.setTimeout(12e4),o.on("data",s=>{r+=s.toString("utf8");let i=r.indexOf(`
271
- `);if(i===-1)return;let a=r.slice(0,i).trim();r=r.slice(i+1);let l=qw();Qw(a,e,l).then(d=>{o.write(`${JSON.stringify(d)}
272
- `),o.end()})}),o.on("error",()=>{})});n.listen(0,"127.0.0.1",()=>{let o=n.address();if(!o||typeof o=="string"){P.error("gateway control: could not read listen address");return}let r={token:t,host:o.address,port:o.port};Jw(r),P.info({port:o.port},"gateway control listening")}),n.on("error",o=>{P.error({err:String(o)},"gateway control server error")}),Bo=n}function ro(){if(Bo){try{Bo.close()}catch{}Bo=null,zw()}}xe();import Vw from"node:crypto";import Xw from"node:http";var Zw=256*1024;function eb(e,t){if(!e||!t)return!1;try{let n=Buffer.from(e,"utf8"),o=Buffer.from(t,"utf8");return n.length===o.length&&Vw.timingSafeEqual(n,o)}catch{return!1}}function tb(e){let t=typeof e.source=="string"?e.source:"webhook";if(e.action==="completed"&&e.workflow_run&&typeof e.workflow_run=="object"){let a=e.workflow_run,l=a.name??"?",d=a.conclusion??"?",u=a.head_branch??"?",c=a.html_url??"",m=e.repository?.full_name??"?";return`[${t}] ${m} \u2014 ${l}
273
- result: ${d}
274
- branch: ${u}${c?`
275
- ${c}`:""}`}if(e.object_kind==="pipeline"&&e.object_attributes&&typeof e.object_attributes=="object"){let a=e.object_attributes,l=a.status??"?",d=a.ref??"?",u=a.id??"?",c=e.project?.path_with_namespace??"?";return`[${t}] ${c} \u2014 pipeline #${u}
285
+ `)}`),r}}q();import{spawn as ek,spawnSync as tk}from"node:child_process";import go from"node:fs";import nk from"node:path";import yo from"node:process";function Ds(e,t={}){ie(),H(nk.dirname(e));let n=yo.argv[1];if(!n)return{ok:!1,message:"Cannot resolve entry script; invoke via node path/to/dist/index.js."};let o=["run"];t.verbose&&o.push("-vb");let r=go.openSync(e,"a"),s=ek(yo.execPath,[n,...o],{detached:!0,stdio:["ignore",r,r],env:{...yo.env,OMNISH_BACKGROUND_GATEWAY:"1"}});return go.closeSync(r),s.unref(),s.pid?{ok:!0,pid:s.pid}:{ok:!1,message:"Failed to start background gateway."}}function Us(){if(ie(),!go.existsSync(ge))return{outcome:"no_pidfile"};let e=go.readFileSync(ge,"utf8").trim(),t=Number(e);if(!Number.isFinite(t)||t<=0){try{go.unlinkSync(ge)}catch{}return{outcome:"invalid_pidfile"}}try{yo.kill(t,0)}catch{try{go.unlinkSync(ge)}catch{}return{outcome:"stale_cleaned",pid:t}}try{return yo.kill(t,"SIGTERM"),{outcome:"sent_signal",pid:t}}catch(n){return yo.platform==="win32"&&tk("taskkill",["/PID",String(t),"/T"],{windowsHide:!0}).status===0?{outcome:"taskkill_ok",pid:t}:{outcome:"failed",message:`could not signal process: ${String(n)}`}}}import rm from"node:crypto";import pl from"node:fs";import ok from"node:net";Ze();Ce();q();var er=null;function rk(){try{let e=pl.readFileSync(Dn,"utf8"),t=JSON.parse(e);return typeof t.token=="string"?t.token:""}catch{return""}}function sk(e){pl.writeFileSync(Dn,JSON.stringify(e,null,2)+`
286
+ `,{mode:384})}function ik(){try{pl.unlinkSync(Dn)}catch{}}async function ak(e,t){let n=t.getCfg(),o=typeof e.absPath=="string"?e.absPath.trim():"";if(!o)return{ok:!1,error:"Missing absPath."};let r=typeof e.caption=="string"&&e.caption.length>0?e.caption:void 0,s=t.sendPlatformMedia&&!t.getWaOutbound()&&!t.getTgSendMedia()?rn(n):n.fileSendMaxBytes,i=xt(o,s);if("error"in i)return{ok:!1,error:i.error};let a={absPath:i.absPath,category:i.category,mimetype:i.mimetype,displayName:i.displayName,caption:r};if(e.channel==="whatsapp"){let l=t.getWaOutbound(),c=typeof e.e164=="string"?e.e164.trim():"";if(!c.startsWith("+"))return{ok:!1,error:"WhatsApp destination must be E.164 (e.g. +15551234567)."};let d=Jt(c);if(!l){let m=t.sendPlatformMedia;if(!m)return{ok:!1,error:"WhatsApp outbound is not connected."};try{return await m(`wa:${d}`,a),{ok:!0}}catch(h){return{ok:!1,error:String(h)}}}let u=n.gatewayMode;if(u!=="whatsapp"&&u!=="both")return{ok:!1,error:"gatewayMode does not include WhatsApp."};try{return await l.sendMedia(d,a),{ok:!0}}catch(m){return{ok:!1,error:String(m)}}}if(e.channel==="telegram"){let l=t.getTgSendMedia();if(!l){let d=t.sendPlatformMedia;if(!d)return{ok:!1,error:"Telegram outbound is not connected."};if(!Number.isFinite(e.chatId))return{ok:!1,error:"Invalid Telegram chat id."};let u=`tg:${e.chatId}`;try{return await d(u,a),{ok:!0}}catch(m){return{ok:!1,error:String(m)}}}let c=n.gatewayMode;if(c!=="telegram"&&c!=="both")return{ok:!1,error:"gatewayMode does not include Telegram."};if(!Number.isFinite(e.chatId))return{ok:!1,error:"Invalid Telegram chat id."};try{return await l(e.chatId,a),{ok:!0}}catch(d){return{ok:!1,error:String(d)}}}return{ok:!1,error:"Unknown channel."}}async function lk(e,t){let n=t.getCfg(),o=typeof e.text=="string"?e.text.trim():"";if(!o)return{ok:!1,error:"Missing or empty text."};if(e.channel==="whatsapp"){let r=t.getWaOutbound();if(!r){let l=t.sendPlatformText;if(!l)return{ok:!1,error:"WhatsApp outbound is not connected."};let c=typeof e.e164=="string"?e.e164.trim():"";if(!c.startsWith("+"))return{ok:!1,error:"WhatsApp destination must be E.164 (e.g. +15551234567)."};let d=Jt(c);try{return await l(`wa:${d}`,o),{ok:!0}}catch(u){return{ok:!1,error:String(u)}}}let s=n.gatewayMode;if(s!=="whatsapp"&&s!=="both")return{ok:!1,error:"gatewayMode does not include WhatsApp."};let i=typeof e.e164=="string"?e.e164.trim():"";if(!i.startsWith("+"))return{ok:!1,error:"WhatsApp destination must be E.164 (e.g. +15551234567)."};let a=Jt(i);try{return await r.sendText(a,o),{ok:!0}}catch(l){return{ok:!1,error:String(l)}}}if(e.channel==="telegram"){let r=t.getTgSendText();if(!r){let i=t.sendPlatformText;if(!i)return{ok:!1,error:"Telegram outbound is not connected."};if(!Number.isFinite(e.chatId))return{ok:!1,error:"Invalid Telegram chat id."};try{return await i(`tg:${e.chatId}`,o),{ok:!0}}catch(a){return{ok:!1,error:String(a)}}}let s=n.gatewayMode;if(s!=="telegram"&&s!=="both")return{ok:!1,error:"gatewayMode does not include Telegram."};if(!Number.isFinite(e.chatId))return{ok:!1,error:"Invalid Telegram chat id."};try{return await r(e.chatId,p(o)),{ok:!0}}catch(i){return{ok:!1,error:String(i)}}}return{ok:!1,error:"Unknown channel."}}async function ck(e,t,n){let o;try{o=JSON.parse(e)}catch{return{ok:!1,error:"Invalid JSON."}}if(!o||typeof o!="object")return{ok:!1,error:"Invalid request."};if(typeof o.token!="string"||!n)return{ok:!1,error:"Unauthorized."};try{let r=Buffer.from(o.token,"utf8"),s=Buffer.from(n,"utf8");if(r.length!==s.length||!rm.timingSafeEqual(r,s))return{ok:!1,error:"Unauthorized."}}catch{return{ok:!1,error:"Unauthorized."}}return o.op==="sendMedia"?ak(o,t):o.op==="sendText"?lk(o,t):o.op==="sendPeerText"?uk(o,t):{ok:!1,error:"Unsupported operation."}}async function uk(e,t){let n=typeof e.peerKey=="string"?e.peerKey.trim():"",o=typeof e.text=="string"?e.text.trim():"";if(!n)return{ok:!1,error:"Missing peerKey."};if(!o)return{ok:!1,error:"Missing or empty text."};if(t.sendPlatformText)try{return await t.sendPlatformText(n,o),{ok:!0}}catch(s){return{ok:!1,error:String(s)}}let r=t.getCfg();if(n.startsWith("wa:")){let s=t.getWaOutbound();if(!s)return{ok:!1,error:"WhatsApp outbound is not connected."};let i=r.gatewayMode;if(i!=="whatsapp"&&i!=="both")return{ok:!1,error:"gatewayMode does not include WhatsApp."};try{return await s.sendText(n.slice(3),o),{ok:!0}}catch(a){return{ok:!1,error:String(a)}}}if(n.startsWith("tg:")){let s=t.getTgSendText();if(!s)return{ok:!1,error:"Telegram outbound is not connected."};let i=r.gatewayMode;if(i!=="telegram"&&i!=="both")return{ok:!1,error:"gatewayMode does not include Telegram."};let a=Number(n.slice(3));if(!Number.isFinite(a))return{ok:!1,error:"Invalid Telegram peer key."};try{return await s(a,p(o)),{ok:!0}}catch(l){return{ok:!1,error:String(l)}}}return{ok:!1,error:"Unknown peer key prefix."}}function Bs(e){if(er)return;let t=rm.randomBytes(32).toString("hex"),n=ok.createServer(o=>{let r="";o.setTimeout(12e4),o.on("data",s=>{r+=s.toString("utf8");let i=r.indexOf(`
287
+ `);if(i===-1)return;let a=r.slice(0,i).trim();r=r.slice(i+1);let l=rk();ck(a,e,l).then(c=>{o.write(`${JSON.stringify(c)}
288
+ `),o.end()})}),o.on("error",()=>{})});n.listen(0,"127.0.0.1",()=>{let o=n.address();if(!o||typeof o=="string"){M.error("gateway control: could not read listen address");return}let r={token:t,host:o.address,port:o.port};sk(r),M.info({port:o.port},"gateway control listening")}),n.on("error",o=>{M.error({err:String(o)},"gateway control server error")}),er=n}function wo(){if(er){try{er.close()}catch{}er=null,ik()}}Ce();import dk from"node:crypto";import pk from"node:http";var mk=256*1024;function hk(e,t){if(!e||!t)return!1;try{let n=Buffer.from(e,"utf8"),o=Buffer.from(t,"utf8");return n.length===o.length&&dk.timingSafeEqual(n,o)}catch{return!1}}function fk(e){let t=typeof e.source=="string"?e.source:"webhook";if(e.action==="completed"&&e.workflow_run&&typeof e.workflow_run=="object"){let a=e.workflow_run,l=a.name??"?",c=a.conclusion??"?",d=a.head_branch??"?",u=a.html_url??"",m=e.repository?.full_name??"?";return`[${t}] ${m} \u2014 ${l}
289
+ result: ${c}
290
+ branch: ${d}${u?`
291
+ ${u}`:""}`}if(e.object_kind==="pipeline"&&e.object_attributes&&typeof e.object_attributes=="object"){let a=e.object_attributes,l=a.status??"?",c=a.ref??"?",d=a.id??"?",u=e.project?.path_with_namespace??"?";return`[${t}] ${u} \u2014 pipeline #${d}
276
292
  status: ${l}
277
- ref: ${d}`}let n=typeof e.text=="string"?e.text:null,o=typeof e.message=="string"?e.message:null,r=typeof e.title=="string"?e.title:null,s=typeof e.status=="string"?e.status:null;if(n)return`[${t}] ${n}`;let i=[`[${t}]`];if(r&&i.push(r),o&&i.push(o),s&&i.push(`status: ${s}`),i.length===1){let a=JSON.stringify(e).slice(0,500);i.push(a)}return i.join(`
278
- `)}var Ha=null;function Cs(e,t){if(Ha)return{stop:()=>{}};let n=Xw.createServer((o,r)=>{if(o.method!=="POST"){r.writeHead(405,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Method not allowed"}));return}let s=o.headers.authorization,i=s?.startsWith("Bearer ")?s.slice(7):void 0,a=new URL(o.url??"/",`http://${o.headers.host??"localhost"}`).searchParams.get("token")??void 0;if(!eb(i??a,e.token)){r.writeHead(401,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Unauthorized"}));return}let d="",u=0;o.on("data",c=>{if(u+=c.length,u>Zw){r.writeHead(413,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Payload too large"})),o.destroy();return}d+=c.toString("utf8")}),o.on("end",()=>{let c;try{c=JSON.parse(d)}catch{r.writeHead(400,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Invalid JSON"}));return}let h=new URL(o.url??"/",`http://${o.headers.host??"localhost"}`).searchParams.get("source")??o.headers["x-webhook-source"]??void 0;h&&(c.source=h);let f=typeof c.peerKey=="string"&&c.peerKey||t.getDefaultPeerKey();if(!f){r.writeHead(400,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"No target peer. Set peerKey in body or configure an allowlisted identity."}));return}let g=tb(c);t.sendToPeer(f,g).then(()=>{r.writeHead(200,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!0}))},y=>{P.warn({err:String(y)},"webhook: sendToPeer failed"),r.writeHead(502,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Failed to deliver message"}))})})});return n.listen(e.port,e.host,()=>{let o=n.address(),r=typeof o=="object"&&o?o.port:e.port;P.info({port:r,host:e.host},"webhook receiver listening")}),n.on("error",o=>{P.error({err:String(o)},"webhook receiver error")}),Ha=n,{stop:()=>{try{n.close()}catch{}Ha=null}}}pe();import Dk from"node:crypto";import Pl from"node:fs";xe();import nb from"node:fs";function Cp(e,t,n){let o=e.fileReceiveMaxBytes,r;try{r=Buffer.from(n.dataBase64,"base64")}catch{return{mediaError:"Invalid inbound media payload."}}if(o>0&&r.length>o)return{mediaError:`Media too large (max ${o} bytes).`};let s;try{s=Jt(e,t)}catch(a){return{mediaError:String(a)}}let i=Xn(s,t,n.name);try{return nb.writeFileSync(i,r,{mode:384}),{mediaSavedPath:i}}catch{return{mediaError:"Could not write media to inbox."}}}G();jo();xe();import Xa from"ws";import cb from"ws";var lb=["/platform/device","/control/device"];function Ms(e){let t=e.replace(/\/$/,"");return lb.map(n=>{let o=new URL(n,t);return o.protocol=o.protocol==="https:"?"wss:":"ws:",o.toString()})}var ub=Math.ceil(wp*1.4)+512*1024;function db(e){let t=String(e instanceof Error?e.message:e);return/Unexpected server response:\s*400/.test(t)||/Unexpected server response:\s*404/.test(t)}async function Mp(e,t){let n=Ms(e),o=null;for(let s of n)try{return{ws:await new Promise((a,l)=>{let d=new cb(s,{headers:{Authorization:`Bearer ${t}`},maxPayload:ub});d.once("open",()=>a(d)),d.once("error",l)}),pathname:new URL(s).pathname}}catch(i){if(o=i instanceof Error?i:new Error(String(i)),db(i))continue;throw o}let r="Could not open a device WebSocket. Redeploy the relay (for /control/device fallback) and ensure /platform/* or /control/* route to port 8788.";throw o&&/Unexpected server response:\s*400/.test(o.message)?new Error(`${o.message} \u2014 ${r}`):new Error(o?`${o.message} \u2014 ${r}`:r)}var pb=1e3,mb=6e4,Ps=class{constructor(t){this.options=t}ws=null;stopped=!1;pingTimer=null;registerResolve=null;registerReject=null;registeredAccount=null;registeredDeviceId=null;reconnectAttempt=0;reconnectTimer=null;outboundQueue=[];getRegisteredAccount(){return this.registeredAccount}async connect(){return this.connectOnce()}async connectOnce(){let{env:t}=this.options,n=new Promise((a,l)=>{this.registerResolve=a,this.registerReject=l}),{ws:o,pathname:r}=await Mp(t.platformUrl,t.token);this.ws=o,r!=="/platform/device"&&P.info({pathname:r},"platform device websocket connected via fallback path"),o.on("message",a=>{this.handleMessage(a.toString())}),o.on("close",()=>{this.stopped||(P.warn("platform device websocket closed; scheduling reconnect"),this.scheduleReconnect())}),o.on("error",a=>{P.warn({err:String(a)},"platform device websocket error")});let s=setTimeout(()=>{this.registerReject?.(new Error("Platform device register timeout (15s)"))},15e3);this.sendRaw({type:"register",...t.deviceId?{deviceId:t.deviceId}:{},label:process.env.OMNISH_DEVICE_LABEL?.trim()||"default"});let i=await n;return clearTimeout(s),this.registeredDeviceId=i.deviceId,this.reconnectAttempt=0,this.pingTimer&&clearInterval(this.pingTimer),this.pingTimer=setInterval(()=>{this.sendRaw({type:"ping"})},3e4),this.flushOutboundQueue(),i}scheduleReconnect(){if(this.stopped||this.reconnectTimer)return;this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null),this.ws=null;let t=Math.min(pb*2**this.reconnectAttempt,mb);this.reconnectAttempt+=1,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,!this.stopped&&this.connectOnce().catch(n=>{P.warn({err:String(n)},"platform device reconnect failed"),this.scheduleReconnect()})},t),this.reconnectTimer.unref?.()}async handleMessage(t){let n=kp(t);if(n){if(n.type==="registered"&&"deviceId"in n){n.account&&(this.registeredAccount=n.account),this.registerResolve?.({deviceId:n.deviceId,...n.account?{account:n.account}:{}}),this.registerResolve=null,this.registerReject=null;return}if(n.type==="message"){await this.options.onMessage(n);return}if(n.type==="reply_error"){await this.options.onReplyError?.(n.peerKey,n.error,n.messageId);return}n.type==="error"&&(P.warn({message:n.message},"platform error"),this.registerReject?.(new Error(n.message)),this.registerResolve=null,this.registerReject=null)}}sendReply(t,n,o,r){this.enqueue({type:"reply",peerKey:t,...n?{body:n}:{},...o?{messageId:o}:{},...r?.length?{files:r}:{}})}sendRoutedReply(t,n){this.enqueue({type:"reply",peerKey:t,...n})}enqueue(t){if(this.ws?.readyState===Xa.OPEN){this.sendRaw(t);return}if(this.outboundQueue.length>=bp){P.warn("platform outbound queue full; dropping reply");return}this.outboundQueue.push(t)}flushOutboundQueue(){for(;this.outboundQueue.length>0&&this.ws?.readyState===Xa.OPEN;){let t=this.outboundQueue.shift();t&&this.sendRaw(t)}}sendRaw(t){this.ws?.readyState===Xa.OPEN&&this.ws.send(JSON.stringify(t))}stop(){this.stopped=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null);try{this.ws?.close()}catch{}this.ws=null,this.outboundQueue.length=0}};import nl from"node:readline/promises";import{stdin as ol,stdout as Fs}from"node:process";pe();var hb=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;function Ep(e){let t=e.trim().toLowerCase();return t?t.length>63?"Tunnel name must be at most 63 characters.":hb.test(t)?null:"Tunnel name may only use letters, digits, and hyphens.":"Tunnel name must not be empty."}function Ap(e){let t=Number.parseInt(e,10);return!Number.isInteger(t)||t<1||t>65535?null:t}function Pp(e,t){let n="127.0.0.1",o,r,s=!1,i=[];for(let d=0;d<t.length;d++){let u=t[d];if(u==="--host"){let c=t[++d];if(!c)return{kind:"error",message:"--host requires an address."};n=c;continue}if(u.startsWith("--host=")){n=u.slice(7);continue}if(u==="--relay"){let c=t[++d];if(!c)return{kind:"error",message:"--relay requires a URL."};o=c;continue}if(u.startsWith("--relay=")){o=u.slice(8);continue}if(u==="--name"){let c=t[++d];if(!c)return{kind:"error",message:"--name requires a slug."};r=c;continue}if(u.startsWith("--name=")){r=u.slice(7);continue}if(u==="--background"||u==="-b"){s=!0;continue}i.push(u)}let a=i[0];if(!a)return{kind:"error",message:`Usage: omnish tunnel ${e} <port> [--host <addr>] [--name <slug>] [--relay <url>] [--background]`};let l=Ap(a);if(l===null)return{kind:"error",message:"Port must be an integer between 1 and 65535."};if(r){let d=Ep(r);if(d)return{kind:"error",message:d}}return{kind:"expose",options:{kind:e,port:l,host:n,relayUrl:o??"",name:r,background:s}}}function Za(e){let[t,...n]=e,o=(t??"").trim().toLowerCase();if(!o||o==="help"||o==="-h"||o==="--help")return{kind:"help"};if(o==="signup"){let r,s,i,a;for(let l=0;l<n.length;l++){let d=n[l];if(d==="--email"){r=n[++l];continue}if(d.startsWith("--email=")){r=d.slice(8);continue}if(d==="--phone"){s=n[++l];continue}if(d.startsWith("--phone=")){s=d.slice(8);continue}if(d==="--password"){i=n[++l];continue}if(d.startsWith("--password=")){i=d.slice(11);continue}if(d==="--relay"){a=n[++l];continue}if(d.startsWith("--relay=")){a=d.slice(8);continue}}return{kind:"signup",email:r,phone:s,password:i,relayUrl:a}}if(o==="login"){let r,s,i,a,l;for(let d=0;d<n.length;d++){let u=n[d];if(u==="--token"){r=n[++d];continue}if(u.startsWith("--token=")){r=u.slice(8);continue}if(u==="--email"){s=n[++d];continue}if(u.startsWith("--email=")){s=u.slice(8);continue}if(u==="--phone"){i=n[++d];continue}if(u.startsWith("--phone=")){i=u.slice(8);continue}if(u==="--password"){a=n[++d];continue}if(u.startsWith("--password=")){a=u.slice(11);continue}if(u==="--relay"){l=n[++d];continue}if(u.startsWith("--relay=")){l=u.slice(8);continue}r||(r=u)}return{kind:"login",token:r,email:s,phone:i,password:a,relayUrl:l}}if(o==="logout")return{kind:"logout"};if(o==="list")return{kind:"list"};if(o==="status"){let r;for(let s=0;s<n.length;s++){let i=n[s];if(i==="--relay"){r=n[s+1],s++;continue}i.startsWith("--relay=")&&(r=i.slice(8))}return{kind:"status",relayUrl:r}}if(o==="stop"){let r=n[0]?.trim();return r?{kind:"stop",target:r}:{kind:"error",message:"Usage: omnish tunnel stop <id|slug>"}}return o==="http"?Pp("http",n):o==="tcp"?Pp("tcp",n):{kind:"error",message:`Unknown tunnel subcommand "${t}". Try: omnish tunnel help`}}function fb(e){let t=[],n="",o=null;for(let r=0;r<e.length;r++){let s=e[r];if(o){s===o?o=null:n+=s;continue}if(s==='"'||s==="'"){o=s;continue}if(/\s/.test(s)){n.length&&(t.push(n),n="");continue}n+=s}return n.length&&t.push(n),t}function Ip(e){let t=e.trim();if(!t||t==="help")return{kind:"help"};let n=fb(t),o=n[0]?.toLowerCase();if(o==="login"||o==="logout"||o==="status"||o==="signup")return Za(n);if(t.toLowerCase()==="list"||t.toLowerCase()==="ls")return{kind:"list"};let r=t.match(/^stop\s+(\S+)\s*$/i);if(r)return{kind:"stop",target:r[1]};let s=t.match(/^(http|tcp)\s+(\d+)(?:\s+--name\s+(\S+))?(?:\s+--host\s+(\S+))?\s*$/i);if(s){let i=s[1].toLowerCase(),a=Ap(s[2]);if(a===null)return{kind:"error",message:"Port must be an integer between 1 and 65535."};let l=s[3],d=s[4]??"127.0.0.1";if(l){let u=Ep(l);if(u)return{kind:"error",message:u}}return{kind:"expose",options:{kind:i,port:a,host:d,relayUrl:"",name:l,background:!0}}}return{kind:"error",message:"Usage: /tunnel login \u2026 | /tunnel status | /tunnel http <port> | /tunnel tcp <port> | /tunnels | /tunnel stop <id>"}}import gb from"ws";import{URL as Lp}from"node:url";function ao(e){let t=new Lp(e);return t.protocol=t.protocol==="https:"?"wss:":"ws:",(!t.pathname||t.pathname==="/")&&(t.pathname="/control"),t.toString()}function Op(e){return new Lp("/health",e).toString()}async function Es(e,t,n=1e4){let o=t.trim();if(!o)return{ok:!1,healthOk:!1,controlOk:!1,error:"Tunnel token is missing."};let r=Op(e),s=!1,i;try{let d=await fetch(r,{method:"GET",signal:AbortSignal.timeout(n)});if(!d.ok)return{ok:!1,healthOk:!1,controlOk:!1,error:`Health HTTP ${d.status} (${r})`};let u=await d.json();if(!u?.ok)return{ok:!1,healthOk:!1,controlOk:!1,error:"Health JSON missing ok:true"};s=!0,typeof u.version=="string"&&(i=u.version)}catch(d){return{ok:!1,healthOk:!1,controlOk:!1,error:`Health fetch failed: ${String(d)}`}}let a=ao(e),l=!1;try{await new Promise((d,u)=>{let c=new gb(a,{headers:{Authorization:`Bearer ${o}`}}),m=setTimeout(()=>{c.terminate(),u(new Error("WSS auth timeout"))},n);c.once("message",h=>{clearTimeout(m);try{let f=JSON.parse(h.toString());if(f.type!=="auth_ok"){u(new Error(`Expected auth_ok, got ${f.type??"?"}`));return}c.close(),d()}catch(f){u(f)}}),c.once("error",h=>{clearTimeout(m),u(h)})}),l=!0}catch(d){return{ok:!1,healthOk:!0,healthVersion:i,controlOk:!1,error:`Control WebSocket: ${String(d)}`}}return{ok:!0,healthOk:s,healthVersion:i,controlOk:!0}}pn();mn();mn();pn();import _p from"node:crypto";import yb from"node:http";import wb from"node:net";import rn from"ws";function As(e,t,n=Buffer.alloc(0)){let o=Buffer.allocUnsafe(9+n.length);return o.writeUInt8(e,0),o.writeUInt32BE(t,1),o.writeUInt32BE(n.length,5),n.copy(o,9),o}function Np(e){if(e.length<9)return null;let t=e.readUInt8(0),n=e.readUInt32BE(1),o=e.readUInt32BE(5);return e.length<9+o?null:{frameType:t,streamId:n,payload:e.subarray(9,9+o)}}function el(e){try{let t=JSON.parse(e);return!t||typeof t!="object"||typeof t.type!="string"?null:t}catch{return null}}function lo(e){return JSON.stringify(e)}function bb(e){let t=_p.createHash("sha1").update(e,"utf8").digest("hex").slice(0,8);return Number.parseInt(t,16)>>>0}var Is=class{constructor(t){this.opts=t;let n=_p.randomBytes(4).toString("hex");this.record={id:n,kind:t.expose.kind,slug:t.expose.name?.trim().toLowerCase()??"",localHost:t.expose.host,localPort:t.expose.port,publicUrl:"",status:"connecting",startedAt:new Date().toISOString()}}ws=null;record;tcpStreams=new Map;tcpStreamIds=new Map;stopped=!1;pingTimer=null;getRecord(){return{...this.record}}setStatus(t,n){this.record.status=t,this.record.error=n,this.opts.onStatus?.(this.getRecord())}async start(){if(this.stopped)throw new Error("Tunnel client already stopped.");let t=ao(this.opts.relayUrl),n=new rn(t,{headers:{Authorization:`Bearer ${this.opts.token}`}});this.ws=n,await new Promise((r,s)=>{let i=d=>{l(),s(d)},a=()=>{l(),r()},l=()=>{n.off("error",i),n.off("open",a)};n.once("error",i),n.once("open",a)}),n.on("message",(r,s)=>{if(s){this.handleBinary(Buffer.isBuffer(r)?r:Buffer.from(r));return}let i=typeof r=="string"?r:r.toString("utf8");this.handleControl(i)}),n.on("close",()=>{this.stopped||this.setStatus("error","Relay connection closed."),this.cleanupTcpStreams()}),n.on("error",r=>{this.stopped||this.setStatus("error",String(r))}),this.pingTimer=setInterval(()=>{n.readyState===rn.OPEN&&n.send(lo({type:"ping"}))},3e4),n.send(lo({type:"register",id:this.record.id,kind:this.opts.expose.kind,localHost:this.opts.expose.host,localPort:this.opts.expose.port,...this.opts.expose.name?{name:this.opts.expose.name}:{}}));let o=await this.waitForRegistered();return this.record.slug=o.slug,this.record.publicUrl=o.publicUrl,this.setStatus("active"),this.getRecord()}waitForRegistered(){let t=this.ws;return t?new Promise((n,o)=>{let r=s=>{if(typeof s!="string"&&!Buffer.isBuffer(s))return;let i=typeof s=="string"?s:s.toString("utf8"),a=el(i);if(a){if(a.type==="registered"&&a.id===this.record.id){t.off("message",r),n(a);return}a.type==="error"&&(t.off("message",r),o(new Error(a.message)))}};t.on("message",r)}):Promise.reject(new Error("WebSocket not connected."))}async handleControl(t){let n=el(t);if(n&&!(n.type==="pong"||n.type==="auth_ok")){if(n.type==="error"){this.setStatus("error",n.message);return}if(n.type==="http"){await this.handleHttpRequest(n);return}if(n.type==="tcp_open"){await this.handleTcpOpen(n.streamId);return}n.type==="tcp_close"&&this.closeTcpStream(n.streamId)}}handleBinary(t){let n=Np(t);if(!n)return;let o=[...this.tcpStreamIds.entries()].find(([,s])=>s===n.streamId)?.[0];if(!o)return;let r=this.tcpStreams.get(o);if(r){if(n.frameType===2){r.write(n.payload);return}n.frameType===3&&(r.end(),this.tcpStreams.delete(o),this.tcpStreamIds.delete(o))}}async handleHttpRequest(t){let n=this.ws;if(!n||n.readyState!==rn.OPEN)return;let o=t.bodyBase64?Buffer.from(t.bodyBase64,"base64"):void 0,r={host:this.opts.expose.host,port:this.opts.expose.port,method:t.method,path:t.path,headers:{...t.headers}};await new Promise(s=>{let i=yb.request(r,a=>{let l=[];a.on("data",d=>l.push(Buffer.isBuffer(d)?d:Buffer.from(d))),a.on("end",()=>{let d=Buffer.concat(l);n.send(lo({type:"http_res",requestId:t.requestId,status:a.statusCode??502,headers:a.headers,...d.length>0?{bodyBase64:d.toString("base64")}:{}})),s()})});i.on("error",a=>{n.send(lo({type:"http_res",requestId:t.requestId,status:502,headers:{"content-type":"text/plain"},bodyBase64:Buffer.from(String(a)).toString("base64")})),s()}),o&&o.length>0&&i.write(o),i.end()})}async handleTcpOpen(t){let n=this.ws;if(!n||n.readyState!==rn.OPEN)return;let o=bb(t);this.tcpStreamIds.set(t,o);let r=wb.connect({host:this.opts.expose.host,port:this.opts.expose.port});this.tcpStreams.set(t,r),r.on("data",s=>{n.readyState===rn.OPEN&&n.send(As(2,o,Buffer.isBuffer(s)?s:Buffer.from(s)))}),r.on("close",()=>{n.readyState===rn.OPEN&&n.send(As(3,o)),this.tcpStreams.delete(t),this.tcpStreamIds.delete(t)}),r.on("error",()=>{n.readyState===rn.OPEN&&n.send(As(3,o)),this.tcpStreams.delete(t),this.tcpStreamIds.delete(t)})}closeTcpStream(t){let n=this.tcpStreams.get(t);n&&n.destroy(),this.tcpStreams.delete(t),this.tcpStreamIds.delete(t)}cleanupTcpStreams(){for(let t of this.tcpStreams.values())t.destroy();this.tcpStreams.clear(),this.tcpStreamIds.clear()}async stop(){if(this.stopped)return;this.stopped=!0,this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null);let t=this.ws;t&&t.readyState===rn.OPEN&&(t.send(lo({type:"unregister",id:this.record.id})),t.close()),this.cleanupTcpStreams(),this.setStatus("stopped")}};var Ls=class{clients=new Map;list(){return[...new Set(this.clients.values())].map(t=>t.getRecord())}getActiveCount(){return[...new Set(this.clients.values())].filter(t=>t.getRecord().status==="active").length}async expose(t,n){let o=kt();if(!o)throw new Error("No tunnel token configured. Run `omnish tunnel login` or set OMNISH_TUNNEL_TOKEN.");let r=t.tunnelMaxActive>0?t.tunnelMaxActive:5;if(this.getActiveCount()>=r)throw new Error(`Active tunnel limit reached (${r}). Stop one with \`omnish tunnel stop <id>\`.`);let s=n.relayUrl||ct(t.tunnelRelayUrl||Ee),i=new Is({relayUrl:s,token:o,expose:n,onStatus:l=>{(l.status==="stopped"||l.status==="error")&&this.clients.delete(l.id)}}),a=await i.start();return this.clients.set(a.id,i),a.slug&&this.clients.set(a.slug,i),a}async stop(t){let n=t.trim().toLowerCase(),o=this.clients.get(n)??this.clients.get(t.trim());if(!o)return null;let r=o.getRecord();return await o.stop(),this.clients.delete(r.id),r.slug&&this.clients.delete(r.slug),o.getRecord()}async stopAll(){let t=[...new Set(this.clients.values())];await Promise.all(t.map(n=>n.stop())),this.clients.clear()}};async function Wp(e,t){let n=await fetch(e,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(t),signal:AbortSignal.timeout(15e3)}),o=await n.json();return!n.ok||!o.token?{ok:!1,error:o.error??`HTTP ${n.status}`}:{ok:!0,token:o.token}}function Os(e,t){let n=new URL("/auth/signup",e).toString();return Wp(n,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:t.password})}function Ns(e,t){let n=new URL("/auth/login",e).toString();return Wp(n,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:t.password})}var co=new Ls;function Cn(){return co}function kb(e){let t=[q(e,"omnish tunnel"),w(e,"Expose local HTTP or TCP ports through the omnish relay."),"",q(e,"Usage:"),` ${v(e,"omnish tunnel signup [--email <email>] [--phone <phone>] [--password <pass>] [--relay <url>]")}`,` ${v(e,"omnish tunnel login [--token <token>] [--relay <url>]")}`,` ${v(e,"omnish tunnel login --email <email> --password <pass> [--relay <url>]")}`,` ${v(e,"omnish tunnel logout")}`,` ${v(e,"omnish tunnel status [--relay <url>]")}`,` ${v(e,"omnish tunnel http <port> [--host <addr>] [--name <slug>] [--relay <url>] [--background]")}`,` ${v(e,"omnish tunnel tcp <port> [--host <addr>] [--name <slug>] [--relay <url>] [--background]")}`,` ${v(e,"omnish tunnel list")}`,` ${v(e,"omnish tunnel stop <id|slug>")}`,"",w(e,"Secrets live in ~/.omnish/tunnel-auth.json or OMNISH_TUNNEL_TOKEN."),w(e,`Default relay: ${Ee}`),""];console.log(t.join(`
279
- `))}async function vb(){let e=nl.createInterface({input:ol,output:Fs});try{return(await e.question("Tunnel token: ")).trim()}finally{e.close()}}async function Dp(e){let t=Za(e),n=Fs,o=process.stderr;if(t.kind==="help"){kb(n);return}if(t.kind==="error"){console.error(T(o,t.message)),process.exitCode=1;return}let r=S();if(t.kind==="signup"){let s=t.relayUrl||ct(r.tunnelRelayUrl||Ee),i=nl.createInterface({input:ol,output:Fs});try{let a=t.email?.trim()||(await i.question("Email (or leave empty for phone): ")).trim(),l=t.phone?.trim()||"";if(a||(l=l||(await i.question("Phone: ")).trim()),!a&&!l){console.error(T(o,"Email or phone is required.")),process.exitCode=1;return}let d=t.password||(await i.question("Password (min 8 chars): ")).trim();if(d.length<8){console.error(T(o,"Password must be at least 8 characters.")),process.exitCode=1;return}let u=await Os(s,{...a?{email:a}:{},...l?{phone:l}:{},password:d});if(!u.ok){console.error(T(o,u.error)),process.exitCode=1;return}bt({token:u.token,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(D(n,"Account created. Token saved."))}finally{i.close()}return}if(t.kind==="login"){if(t.email||t.phone){let a=t.relayUrl||ct(r.tunnelRelayUrl||Ee),l=nl.createInterface({input:ol,output:Fs});try{let d=t.password||(await l.question("Password: ")).trim(),u=await Ns(a,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:d});if(!u.ok){console.error(T(o,u.error)),process.exitCode=1;return}bt({token:u.token,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(D(n,"Logged in. Token saved."))}finally{l.close()}return}let i=t.token?.trim()||await vb();if(!i){console.error(T(o,"Tunnel token is required.")),process.exitCode=1;return}bt({token:i,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(D(n,"Tunnel token saved."));return}if(t.kind==="logout"){Er(),console.log(D(n,"Tunnel token removed."));return}if(t.kind==="status"){let s=t.relayUrl||ct(r.tunnelRelayUrl||Ee),i=kt(),a=!!process.env.OMNISH_TUNNEL_TOKEN?.trim(),l=await Es(s,i);console.log(`${w(n,"relay:")} ${v(n,s)}`),console.log(`${w(n,"token:")} ${v(n,i?`configured${a?" (OMNISH_TUNNEL_TOKEN)":""}`:T(o,"missing"))}`),console.log(`${w(n,"health:")} ${l.healthOk?v(n,`ok${l.healthVersion?` (${l.healthVersion})`:""}`):T(o,"fail")}`),console.log(`${w(n,"control:")} ${l.controlOk?v(n,"auth ok"):T(o,"fail")}${l.error&&!l.ok?` \u2014 ${l.error}`:""}`),console.log(`${w(n,"active:")} ${v(n,String(co.getActiveCount()))}`);return}if(t.kind==="list"){let s=co.list();if(s.length===0){console.log(D(n,"(no active tunnels)"));return}for(let i of s)console.log(`${v(n,i.id)} ${i.kind} ${i.status} ${i.publicUrl||"(pending)"}
280
- ${w(n,`${i.localHost}:${i.localPort}`)}`);return}if(t.kind==="stop"){let s=await co.stop(t.target);if(!s){console.error(T(o,`No active tunnel matched "${t.target}".`)),process.exitCode=1;return}console.log(D(n,`Stopped tunnel ${s.id}.`));return}if(t.kind==="expose"){let s=t.options.relayUrl||ct(r.tunnelRelayUrl||Ee),i=await co.expose(r,{...t.options,relayUrl:s});console.log(D(n,`${i.kind.toUpperCase()} tunnel active`)),console.log(`${w(n,"public:")} ${v(n,i.publicUrl)}`),console.log(`${w(n,"local:")} ${v(n,`${i.localHost}:${i.localPort}`)}`),console.log(`${w(n,"id:")} ${v(n,i.id)}`),t.options.background||(console.log(w(n,"Press Ctrl+C to stop.")),await new Promise(a=>{let l=async()=>{await co.stop(i.id),a()};process.once("SIGINT",l),process.once("SIGTERM",l)}))}}function Up(e){let t=e.trim().match(/^(\d+)\.(\d+)\.(\d+)/);return t?[Number(t[1]),Number(t[2]),Number(t[3])]:null}function Bp(e,t){let n=Up(e),o=Up(t);if(!n||!o)return e.trim().localeCompare(t.trim());for(let r=0;r<3;r++)if(n[r]!==o[r])return n[r]<o[r]?-1:1;return 0}var Sb="https://registry.npmjs.org",Hp=null,rl=0;function Go(){return Hp}function xb(e){let t=e.trim();return t.length<1||t.length>214?!1:!/\s/.test(t)}function jp(e){let t=e.trim();if(!t||t.length>2048)return null;try{let n=new URL(t);return n.protocol!=="https:"?null:n.href}catch{return null}}async function Cb(e){let t=e.trim(),n=`${Sb}/${encodeURIComponent(t)}/latest`;try{let o=await fetch(n,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(2e4)});if(!o.ok)return{error:`npm registry: HTTP ${o.status}`};let r=await o.json(),s=typeof r.version=="string"?r.version.trim():"";return s?{version:s}:{error:"npm registry: missing version in response"}}catch(o){return{error:`npm registry: ${String(o)}`}}}async function Rb(e){try{let t=await fetch(e,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(15e3)});if(!t.ok)return{error:`updateInfoUrl: HTTP ${t.status}`};let n=await t.arrayBuffer(),o=n.byteLength>65536?n.slice(0,65536):n,r=new TextDecoder("utf-8",{fatal:!1}).decode(o),s;try{s=JSON.parse(r)}catch{return{error:"updateInfoUrl: body is not JSON"}}if(!s||typeof s!="object"||Array.isArray(s))return{error:"updateInfoUrl: JSON must be an object"};let i=s,a=null;typeof i.message=="string"&&i.message.trim()&&(a=i.message.trim().slice(0,1500));let l=null,d=i.link??i.url;return typeof d=="string"&&d.trim()&&(l=jp(d)),{message:a,link:l}}catch(t){return{error:`updateInfoUrl: ${String(t)}`}}}function Tb(e){return!Number.isFinite(e)||e<36e5?36e5:e>6048e5?6048e5:Math.floor(e)}async function qo(e,t){let n=xb(t.updateCheckPackageName)?t.updateCheckPackageName.trim():"omnish",o=await Cb(n),r="version"in o?o.version:null,s="error"in o?o.error:null,i=!1;r&&!s&&(i=Bp(e,r)<0);let a=null,l=null,d=null,u=jp(t.updateInfoUrl);if(u){let m=await Rb(u);"error"in m?d=m.error:(a=m.message,l=m.link)}let c={runningVersion:e,checkedAtIso:new Date().toISOString(),registryPackage:n,registryLatest:r,registryError:s,updateAvailable:i,infoMessage:a,infoLink:l,infoError:d};return Hp=c,c}function _s(e){if(!e)return;let t=[];if(e.registryError?t.push(`npm check: ${e.registryError}`):e.registryLatest&&t.push(e.updateAvailable?`npm latest *${e.registryLatest}* (running ${e.runningVersion})`:`npm latest ${e.registryLatest} (up to date)`),e.infoError?t.push(`info URL: ${e.infoError}`):e.infoMessage&&t.push(`notice: ${e.infoMessage}`),t.length!==0)return t.join(" \xB7 ")}function Ws(e){let t=!1,n=async()=>{if(t)return;let s=e.getConfig();if(!s.updateCheckEnabled)return;let i=Tb(s.updateCheckIntervalMs),a=Date.now();if(!(rl!==0&&a-rl<i)){rl=a;try{let l=await qo(e.getRunningVersion(),s);l.updateAvailable?e.log.info({updateAvailable:!0,running:l.runningVersion,npmLatest:l.registryLatest,pkg:l.registryPackage},"omnish update check: newer npm version available"):e.log.info({running:l.runningVersion,npmLatest:l.registryLatest,pkg:l.registryPackage},"omnish update check ok"),l.infoMessage&&e.log.info({len:l.infoMessage.length},"omnish update info message present")}catch(l){e.log.warn({err:String(l)},"omnish update check failed")}}},o=setInterval(()=>void n(),6e4),r=setTimeout(()=>void n(),3e4);return()=>{t=!0,clearInterval(o),clearTimeout(r)}}pe();st();import Gp from"node:path";import{glob as $b,stat as Mb}from"node:fs/promises";function Ds(e){let t=e.trim();if(!t||t.startsWith("-- ")||t==="--")return null;let n=t.indexOf(" -- "),o,r;return n!==-1?(o=t.slice(0,n).trim(),r=t.slice(n+4).trim()||void 0):o=t,(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'"))&&(o=o.slice(1,-1)),o.trim()?{selectorPart:o,caption:r}:null}function Pb(e){return/[*?[]/.test(e)}function Eb(e){return e.split(",").map(t=>t.trim()).filter(t=>t.length>0)}async function Jo(e,t){let n=Eb(t),o=new Set,r=[];for(let s of n){if(Pb(s)){for await(let a of $b(s,{cwd:e,withFileTypes:!1})){let l=Gp.resolve(e,a);o.has(l)||(o.add(l),r.push(l))}continue}let i=Gp.resolve(e,s);o.has(i)||(o.add(i),r.push(i))}return r}async function zo(e){for(let t of e)try{if(!(await Mb(t)).isFile())return{ok:!1,error:`Not a file: ${t}`}}catch{return{ok:!1,error:`File not found: ${t}`}}return{ok:!0}}G();import zp from"node:fs";import Ab from"node:path";var Rn="__omnish_shortcuts_global__",qp=500,Kp=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,Ib=new Set(["help","apps","bg","jobs","log","tail","kill","send","file","files","receive","reload","restart","updates","gateway","gw","mode","allow","deny","allowlist","whatsapp","wa","telegram","tg","cowork","cw","shortcut","shortcuts","alias","aliases"]),Ko=new Map,Jp=!1;function Lb(){try{let e=zp.readFileSync(ir,"utf8"),t=JSON.parse(e);if(t&&typeof t=="object")for(let[n,o]of Object.entries(t)){if(!o||typeof o!="object")continue;let r={};for(let[s,i]of Object.entries(o)){let a=String(s).toLowerCase();typeof i=="string"&&i.length>0&&(r[a]=i.trim())}Ko.set(n,r)}}catch{}}function Us(){B(Ab.dirname(ir));let e={};for(let[t,n]of Ko)Object.entries(n).length>0&&(e[t]={...n});zp.writeFileSync(ir,JSON.stringify(e,null,2)+`
281
- `,{mode:384})}function Bs(){Jp||(Lb(),Jp=!0)}function Ob(e){return Ib.has(e.trim().toLowerCase())}function Ct(e){let t=e.trim();if(!t)return{ok:!1,error:"Name is empty."};let n=t.toLowerCase();return Kp.test(n)?Ob(n)?{ok:!1,error:`Reserved name: ${n}`}:{ok:!0,normalized:n}:{ok:!1,error:`Invalid name (use letters, digits, _ or -; max 32 chars): ${t}`}}function sl(e){let t=e.replace(/\r\n/g,`
282
- `).replace(/\n/g," ").trim();return t?t.length>qp?{ok:!1,error:`Body too long (max ${qp} characters).`}:{ok:!0,body:t}:{ok:!1,error:"Body is empty."}}function xt(e,t){Bs();let n=Ko.get(e);return!n&&t&&(n={},Ko.set(e,n)),n??{}}function Yp(e){let t=e.trim();if(/^--global$/i.test(t))return{mode:"global",remainder:""};if(/^-g$/i.test(t))return{mode:"global",remainder:""};if(/^--chat$/i.test(t))return{mode:"chat",remainder:""};if(/^-p$/i.test(t))return{mode:"chat",remainder:""};let n=/^--global\s+/i.exec(t);if(n)return{mode:"global",remainder:t.slice(n[0].length).trimStart()};let o=/^-g\s+/i.exec(t);if(o)return{mode:"global",remainder:t.slice(o[0].length).trimStart()};let r=/^--chat\s+/i.exec(t);if(r)return{mode:"chat",remainder:t.slice(r[0].length).trimStart()};let s=/^-p\s+/i.exec(t);return s?{mode:"chat",remainder:t.slice(s[0].length).trimStart()}:{mode:"resolved",remainder:t}}function il(e){let t=e.trim();if(/^--global$/i.test(t))return{scope:"global",remainder:"",explicit:!0};if(/^-g$/i.test(t))return{scope:"global",remainder:"",explicit:!0};if(/^--chat$/i.test(t))return{scope:"chat",remainder:"",explicit:!0};if(/^-p$/i.test(t))return{scope:"chat",remainder:"",explicit:!0};let n=/^--global\s+([\s\S]*)$/i.exec(t);if(n?.[1]!==void 0)return{scope:"global",remainder:n[1].trimStart(),explicit:!0};let o=/^-g\s+([\s\S]*)$/i.exec(t);if(o?.[1]!==void 0)return{scope:"global",remainder:o[1].trimStart(),explicit:!0};let r=/^--chat\s+([\s\S]*)$/i.exec(t);if(r?.[1]!==void 0)return{scope:"chat",remainder:r[1].trimStart(),explicit:!0};let s=/^-p\s+([\s\S]*)$/i.exec(t);return s?.[1]!==void 0?{scope:"chat",remainder:s[1].trimStart(),explicit:!0}:{scope:"chat",remainder:t,explicit:!1}}function al(e){let t=il(e);return{scope:t.scope,remainder:t.remainder}}function Qp(e){let t=e.trim();return!t||/^-+$/i.test(t)?{filter:"merged"}:/^(?:--global|-g)$/i.test(t)?{filter:"global"}:/^(?:--chat|-p)$/i.test(t)?{filter:"chat"}:{filter:"merged",bad:t}}function Nb(e){let t=e.trim().toLowerCase();if(t==="--global"||t==="-g")return"global";if(t==="--chat"||t==="-p")return"chat"}function Vp(e){let t=e.trim().match(/^(\S+)\s+(--global|-g|--chat|-p)\s*$/i);if(!t?.[1]||!t[2])return;let n=Nb(t[2]);if(n)return{name:t[1],target:n}}function Fb(e,t){Bs();let n=e,o=xt(n,!1),r=xt(Rn,!1);if(t==="chat")return Object.entries(o).map(([a,l])=>({name:a,body:l,scope:"chat"})).sort((a,l)=>a.name.localeCompare(l.name));if(t==="global")return Object.entries(r).map(([a,l])=>({name:a,body:l,scope:"global"})).sort((a,l)=>a.name.localeCompare(l.name));let s=new Set([...Object.keys(o),...Object.keys(r)]),i=[];for(let a of[...s].sort()){if(a in o){let d=o[a];d!==void 0&&i.push({name:a,body:d,scope:"chat"});continue}let l=r[a];l!==void 0&&i.push({name:a,body:l,scope:"global"})}return i}function Xp(e,t="merged"){return Fb(e,t)}function ll(e,t){let n=t.trim().toLowerCase(),o=xt(e,!1)[n];return o!==void 0?o:xt(Rn,!1)[n]}function Hs(e,t){let n=Ct(t);if(!n.ok)return;let o=n.normalized,r=xt(e,!1)[o];if(r!==void 0)return{name:o,body:r,scope:"chat"};let s=xt(Rn,!1)[o];if(s!==void 0)return{name:o,body:s,scope:"global"}}function _t(e,t,n){let o=n.trim().toLowerCase(),r=e==="global"?Rn:t;return xt(r,!1)[o]}function Yo(e,t,n,o="chat"){let r=Ct(t);if(!r.ok)throw new Error(r.error);let s=sl(n);if(!s.ok)throw new Error(s.error);let i=o==="global"?Rn:e,a=xt(i,!0);a[r.normalized]=s.body,Us()}function Zp(e,t,n="chat"){let o=t.trim().toLowerCase(),r=n==="global"?Rn:e;Bs();let s=Ko.get(r);return!s||!(o in s)?!1:(delete s[o],Us(),!0)}function cl(e,t,n){let o=Ct(t);if(!o.ok)return{ok:!1,error:o.error};let r=o.normalized,s=e;Bs();let i=xt(s,!0),a=xt(Rn,!0),l=i[r],d=a[r];if(n==="global"){if(l!==void 0){let u=sl(l);return u.ok?(a[r]=u.body,delete i[r],Us(),{ok:!0,kind:"moved",target:"global",name:r}):{ok:!1,error:u.error}}return d!==void 0?{ok:!0,kind:"noop",message:`Shortcut "${r}" is already shared on this gateway.`}:{ok:!1,error:`No shortcut "${r}" in this chat to share. Add with /shortcut add ${r} \u2026 or make the shared copy private with /shortcut set -p ${r}.`}}if(d!==void 0){let u=sl(d);return u.ok?(i[r]=u.body,delete a[r],Us(),{ok:!0,kind:"moved",target:"chat",name:r}):{ok:!1,error:u.error}}return l!==void 0?{ok:!0,kind:"noop",message:`Shortcut "${r}" is already private to this chat.`}:{ok:!1,error:`No shared shortcut "${r}" to make private. Add with /shortcut add --global ${r} \u2026 or share from this chat with /shortcut set -g ${r}.`}}function em(e){let t=e.trim();return t.length>0&&!/\s/.test(t)&&Kp.test(t.toLowerCase())}import tm from"node:fs";var nm=64,_b=1024*1024;function om(e){return e.fileReceiveMaxBytes>0?e.fileReceiveMaxBytes:_b}function rm(e){let t;try{t=JSON.parse(e)}catch(r){return{ok:!1,error:`Invalid JSON: ${String(r)}`}}let n;if(Array.isArray(t))n=t;else if(t&&typeof t=="object"&&!Array.isArray(t)){let r=t,s=Object.keys(r);if(s.length!==1||s[0]!=="tasks")return{ok:!1,error:'JSON must be an array or a single-key object: { "tasks": [ \u2026 ] }.'};let i=r.tasks;if(!Array.isArray(i))return{ok:!1,error:'"tasks" must be an array.'};n=i}else return{ok:!1,error:'JSON must be an array or { "tasks": [ \u2026 ] }.'};if(n.length===0)return{ok:!1,error:"Queue JSON must contain at least one job."};if(n.length>nm)return{ok:!1,error:`Too many jobs (max ${nm}).`};let o=[];for(let r=0;r<n.length;r++){let s=n[r];if(!s||typeof s!="object"||Array.isArray(s))return{ok:!1,error:`Job ${r+1}: must be an object with "recipe" and "task".`};let i=s;if(Object.keys(i).length!==2||typeof i.recipe!="string"||typeof i.task!="string")return{ok:!1,error:`Job ${r+1}: must contain only "recipe" and "task" string fields.`};o.push({recipe:i.recipe,task:i.task})}return{ok:!0,jobs:o}}function sm(e,t,n){let o=[];for(let r=0;r<n.length;r++){let{recipe:s,task:i}=n[r],a=Ye(e,t,s);if(!a)return{ok:!1,error:`Job ${r+1}: unknown recipe "${s}".`};let l=a.taskEnv??"OMNISH_TASK";if(!Jn(a.command,l))return{ok:!1,error:`Job ${r+1}: recipe "${s}" command must reference "$${l}".`};let d=Kr(i,t.recipesMaxTaskChars);if(!d.ok)return{ok:!1,error:`Job ${r+1}: ${d.error}`};let u=a.promptTemplate?Yr(a.promptTemplate,l,d.task):d.task,c={[l]:u};o.push({command:a.command,extraEnv:c,recipeLabel:s})}return{ok:!0,items:o}}function ul(e,t){let n;try{n=tm.statSync(e)}catch{return{ok:!1,error:`Cannot read file: ${e}`}}if(!n.isFile())return{ok:!1,error:`Not a file: ${e}`};if(n.size>t)return{ok:!1,error:`File too large (max ${t} bytes for queue load).`};try{return{ok:!0,text:tm.readFileSync(e,"utf8")}}catch(o){return{ok:!1,error:String(o)}}}G();import im from"node:fs";import uo from"node:process";var Wb=120;function Wt(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function am(e){let t=e.trim();return t==="/service"||t.startsWith("/service ")?t.slice(8).trim():null}async function lm(e,t){let n=t.trim().split(/\s+/),o=(n[0]??"").toLowerCase();if(!t.trim()||o==="help")return X(ou(e));if(o==="status"){let s=Ot(),i=(()=>{try{return im.existsSync(de)?`gateway.pid: ${im.readFileSync(de,"utf8").trim()}`:"gateway.pid: (missing)"}catch(m){return`gateway.pid: (read error: ${String(m)})`}})(),a=uo.env.OMNISH_BACKGROUND_GATEWAY==="1"?"This process: background gateway (OMNISH_BACKGROUND_GATEWAY=1).":"This process: foreground gateway session.",l=typeof uo.env.OMNISH_HOME=="string"&&uo.env.OMNISH_HOME.trim()?`OMNISH_HOME env: ${uo.env.OMNISH_HOME.trim()}`:"OMNISH_HOME env: (not set \u2014 using default data dir)",d=s.error?s.error:`Node: ${s.nodePath}
283
- Script: ${s.scriptPath}`,u=["*Service status*","",`platform: ${uo.platform}`,a,l,`data dir: ${W}`,i,`default log: ${Ue}`,"",d,"",e.serviceInstallFromChat?"Install from chat: enabled (/service install).":"Install from chat: off \u2014 `/config set serviceInstallFromChat true` to allow /service install."].join(`
284
- `),c=["<b>Service status</b>","",`<code>${Wt(uo.platform)}</code>`,`<br/><code>${Wt(a)}</code>`,`<br/><code>${Wt(l)}</code>`,`<br/>data dir: <code>${Wt(W)}</code>`,`<br/><code>${Wt(i)}</code>`,`<br/>default log: <code>${Wt(Ue)}</code>`,"",`<pre>${Wt(d)}</pre>`,"",e.serviceInstallFromChat?"Install from chat: enabled.":"Install from chat: off \u2014 <code>/config set serviceInstallFromChat true</code>."].join(`
285
- `);return fe(u,c)}if(o==="instructions"){let s=Ot();if(s.error)return p(s.error);let i=Nr(s);return p(`*Install hints*
286
-
287
- ${i}`)}if(o==="logs"){let s=n.length>=2?Number.parseInt(n[1],10):80,i=Number.isFinite(s)&&s>0?Math.min(s,Wb):80,a=Gr(Ue,i),l=[`*Gateway log* (last ${i} lines)
288
- ${Ue}
293
+ ref: ${c}`}let n=typeof e.text=="string"?e.text:null,o=typeof e.message=="string"?e.message:null,r=typeof e.title=="string"?e.title:null,s=typeof e.status=="string"?e.status:null;if(n)return`[${t}] ${n}`;let i=[`[${t}]`];if(r&&i.push(r),o&&i.push(o),s&&i.push(`status: ${s}`),i.length===1){let a=JSON.stringify(e).slice(0,500);i.push(a)}return i.join(`
294
+ `)}var ml=null;function Hs(e,t){if(ml)return{stop:()=>{}};let n=pk.createServer((o,r)=>{if(o.method!=="POST"){r.writeHead(405,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Method not allowed"}));return}let s=o.headers.authorization,i=s?.startsWith("Bearer ")?s.slice(7):void 0,a=new URL(o.url??"/",`http://${o.headers.host??"localhost"}`).searchParams.get("token")??void 0;if(!hk(i??a,e.token)){r.writeHead(401,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Unauthorized"}));return}let c="",d=0;o.on("data",u=>{if(d+=u.length,d>mk){r.writeHead(413,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Payload too large"})),o.destroy();return}c+=u.toString("utf8")}),o.on("end",()=>{let u;try{u=JSON.parse(c)}catch{r.writeHead(400,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Invalid JSON"}));return}let h=new URL(o.url??"/",`http://${o.headers.host??"localhost"}`).searchParams.get("source")??o.headers["x-webhook-source"]??void 0;h&&(u.source=h);let f=typeof u.peerKey=="string"&&u.peerKey||t.getDefaultPeerKey();if(!f){r.writeHead(400,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"No target peer. Set peerKey in body or configure an allowlisted identity."}));return}let g=fk(u);t.sendToPeer(f,g).then(()=>{r.writeHead(200,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!0}))},y=>{M.warn({err:String(y)},"webhook: sendToPeer failed"),r.writeHead(502,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Failed to deliver message"}))})})});return n.listen(e.port,e.host,()=>{let o=n.address(),r=typeof o=="object"&&o?o.port:e.port;M.info({port:r,host:e.host},"webhook receiver listening")}),n.on("error",o=>{M.error({err:String(o)},"webhook receiver error")}),ml=n,{stop:()=>{try{n.close()}catch{}ml=null}}}pe();import Vv from"node:crypto";import Xl from"node:fs";Ce();import gk from"node:fs";function sm(e,t,n){let o=e.fileReceiveMaxBytes,r;try{r=Buffer.from(n.dataBase64,"base64")}catch{return{mediaError:"Invalid inbound media payload."}}if(o>0&&r.length>o)return{mediaError:`Media too large (max ${o} bytes).`};let s;try{s=Qt(e,t)}catch(a){return{mediaError:String(a)}}let i=mo(s,t,n.name);try{return gk.writeFileSync(i,r,{mode:384}),{mediaSavedPath:i}}catch{return{mediaError:"Could not write media to inbox."}}}q();tr();Ce();import kl from"ws";import vk from"ws";var kk=["/platform/device","/control/device"];function qs(e){let t=e.replace(/\/$/,"");return kk.map(n=>{let o=new URL(n,t);return o.protocol=o.protocol==="https:"?"wss:":"ws:",o.toString()})}var Sk=Math.ceil(Od*1.4)+512*1024;function xk(e){let t=String(e instanceof Error?e.message:e);return/Unexpected server response:\s*400/.test(t)||/Unexpected server response:\s*404/.test(t)}async function cm(e,t){let n=qs(e),o=null;for(let s of n)try{return{ws:await new Promise((a,l)=>{let c=new vk(s,{headers:{Authorization:`Bearer ${t}`},maxPayload:Sk});c.once("open",()=>a(c)),c.once("error",l)}),pathname:new URL(s).pathname}}catch(i){if(o=i instanceof Error?i:new Error(String(i)),xk(i))continue;throw o}let r="Could not open a device WebSocket. Redeploy the relay (for /control/device fallback) and ensure /platform/* or /control/* route to port 8788.";throw o&&/Unexpected server response:\s*400/.test(o.message)?new Error(`${o.message} \u2014 ${r}`):new Error(o?`${o.message} \u2014 ${r}`:r)}var Ck=1e3,Rk=6e4,zs=class{constructor(t){this.options=t}ws=null;stopped=!1;pingTimer=null;registerResolve=null;registerReject=null;registeredAccount=null;registeredDeviceId=null;reconnectAttempt=0;reconnectTimer=null;outboundQueue=[];getRegisteredAccount(){return this.registeredAccount}async connect(){return this.connectOnce()}async connectOnce(){let{env:t}=this.options,n=new Promise((a,l)=>{this.registerResolve=a,this.registerReject=l}),{ws:o,pathname:r}=await cm(t.platformUrl,t.token);this.ws=o,r!=="/platform/device"&&M.info({pathname:r},"platform device websocket connected via fallback path"),o.on("message",a=>{this.handleMessage(a.toString())}),o.on("close",()=>{this.stopped||(M.warn("platform device websocket closed; scheduling reconnect"),this.scheduleReconnect())}),o.on("error",a=>{M.warn({err:String(a)},"platform device websocket error")});let s=setTimeout(()=>{this.registerReject?.(new Error("Platform device register timeout (15s)"))},15e3);this.sendRaw({type:"register",...t.deviceId?{deviceId:t.deviceId}:{},label:process.env.OMNISH_DEVICE_LABEL?.trim()||"default"});let i=await n;return clearTimeout(s),this.registeredDeviceId=i.deviceId,this.reconnectAttempt=0,this.pingTimer&&clearInterval(this.pingTimer),this.pingTimer=setInterval(()=>{this.sendRaw({type:"ping"})},3e4),this.flushOutboundQueue(),i}scheduleReconnect(){if(this.stopped||this.reconnectTimer)return;this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null),this.ws=null;let t=Math.min(Ck*2**this.reconnectAttempt,Rk);this.reconnectAttempt+=1,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,!this.stopped&&this.connectOnce().catch(n=>{M.warn({err:String(n)},"platform device reconnect failed"),this.scheduleReconnect()})},t),this.reconnectTimer.unref?.()}async handleMessage(t){let n=Fd(t);if(n){if(n.type==="registered"&&"deviceId"in n){n.account&&(this.registeredAccount=n.account),this.registerResolve?.({deviceId:n.deviceId,...n.account?{account:n.account}:{}}),this.registerResolve=null,this.registerReject=null;return}if(n.type==="message"){await this.options.onMessage(n);return}if(n.type==="reply_error"){await this.options.onReplyError?.(n.peerKey,n.error,n.messageId);return}n.type==="error"&&(M.warn({message:n.message},"platform error"),this.registerReject?.(new Error(n.message)),this.registerResolve=null,this.registerReject=null)}}sendReply(t,n,o,r){this.enqueue({type:"reply",peerKey:t,...n?{body:n}:{},...o?{messageId:o}:{},...r?.length?{files:r}:{}})}sendRoutedReply(t,n){this.enqueue({type:"reply",peerKey:t,...n})}enqueue(t){if(this.ws?.readyState===kl.OPEN){this.sendRaw(t);return}if(this.outboundQueue.length>=Nd){M.warn("platform outbound queue full; dropping reply");return}this.outboundQueue.push(t)}flushOutboundQueue(){for(;this.outboundQueue.length>0&&this.ws?.readyState===kl.OPEN;){let t=this.outboundQueue.shift();t&&this.sendRaw(t)}}sendRaw(t){this.ws?.readyState===kl.OPEN&&this.ws.send(JSON.stringify(t))}stop(){this.stopped=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null);try{this.ws?.close()}catch{}this.ws=null,this.outboundQueue.length=0}};import Cl from"node:readline/promises";import{stdin as Rl,stdout as ei}from"node:process";pe();var Tk=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;function dm(e){let t=e.trim().toLowerCase();return t?t.length>63?"Tunnel name must be at most 63 characters.":Tk.test(t)?null:"Tunnel name may only use letters, digits, and hyphens.":"Tunnel name must not be empty."}function pm(e){let t=Number.parseInt(e,10);return!Number.isInteger(t)||t<1||t>65535?null:t}function um(e,t){let n="127.0.0.1",o,r,s=!1,i=[];for(let c=0;c<t.length;c++){let d=t[c];if(d==="--host"){let u=t[++c];if(!u)return{kind:"error",message:"--host requires an address."};n=u;continue}if(d.startsWith("--host=")){n=d.slice(7);continue}if(d==="--relay"){let u=t[++c];if(!u)return{kind:"error",message:"--relay requires a URL."};o=u;continue}if(d.startsWith("--relay=")){o=d.slice(8);continue}if(d==="--name"){let u=t[++c];if(!u)return{kind:"error",message:"--name requires a slug."};r=u;continue}if(d.startsWith("--name=")){r=d.slice(7);continue}if(d==="--background"||d==="-b"){s=!0;continue}i.push(d)}let a=i[0];if(!a)return{kind:"error",message:`Usage: omnish tunnel ${e} <port> [--host <addr>] [--name <slug>] [--relay <url>] [--background]`};let l=pm(a);if(l===null)return{kind:"error",message:"Port must be an integer between 1 and 65535."};if(r){let c=dm(r);if(c)return{kind:"error",message:c}}return{kind:"expose",options:{kind:e,port:l,host:n,relayUrl:o??"",name:r,background:s}}}function vl(e){let[t,...n]=e,o=(t??"").trim().toLowerCase();if(!o||o==="help"||o==="-h"||o==="--help")return{kind:"help"};if(o==="signup"){let r,s,i,a;for(let l=0;l<n.length;l++){let c=n[l];if(c==="--email"){r=n[++l];continue}if(c.startsWith("--email=")){r=c.slice(8);continue}if(c==="--phone"){s=n[++l];continue}if(c.startsWith("--phone=")){s=c.slice(8);continue}if(c==="--password"){i=n[++l];continue}if(c.startsWith("--password=")){i=c.slice(11);continue}if(c==="--relay"){a=n[++l];continue}if(c.startsWith("--relay=")){a=c.slice(8);continue}}return{kind:"signup",email:r,phone:s,password:i,relayUrl:a}}if(o==="login"){let r,s,i,a,l;for(let c=0;c<n.length;c++){let d=n[c];if(d==="--token"){r=n[++c];continue}if(d.startsWith("--token=")){r=d.slice(8);continue}if(d==="--email"){s=n[++c];continue}if(d.startsWith("--email=")){s=d.slice(8);continue}if(d==="--phone"){i=n[++c];continue}if(d.startsWith("--phone=")){i=d.slice(8);continue}if(d==="--password"){a=n[++c];continue}if(d.startsWith("--password=")){a=d.slice(11);continue}if(d==="--relay"){l=n[++c];continue}if(d.startsWith("--relay=")){l=d.slice(8);continue}r||(r=d)}return{kind:"login",token:r,email:s,phone:i,password:a,relayUrl:l}}if(o==="logout")return{kind:"logout"};if(o==="list")return{kind:"list"};if(o==="status"){let r;for(let s=0;s<n.length;s++){let i=n[s];if(i==="--relay"){r=n[s+1],s++;continue}i.startsWith("--relay=")&&(r=i.slice(8))}return{kind:"status",relayUrl:r}}if(o==="stop"){let r=n[0]?.trim();return r?{kind:"stop",target:r}:{kind:"error",message:"Usage: omnish tunnel stop <id|slug>"}}return o==="http"?um("http",n):o==="tcp"?um("tcp",n):{kind:"error",message:`Unknown tunnel subcommand "${t}". Try: omnish tunnel help`}}function $k(e){let t=[],n="",o=null;for(let r=0;r<e.length;r++){let s=e[r];if(o){s===o?o=null:n+=s;continue}if(s==='"'||s==="'"){o=s;continue}if(/\s/.test(s)){n.length&&(t.push(n),n="");continue}n+=s}return n.length&&t.push(n),t}function mm(e){let t=e.trim();if(!t||t==="help")return{kind:"help"};let n=$k(t),o=n[0]?.toLowerCase();if(o==="login"||o==="logout"||o==="status"||o==="signup")return vl(n);if(t.toLowerCase()==="list"||t.toLowerCase()==="ls")return{kind:"list"};let r=t.match(/^stop\s+(\S+)\s*$/i);if(r)return{kind:"stop",target:r[1]};let s=t.match(/^(http|tcp)\s+(\d+)(?:\s+--name\s+(\S+))?(?:\s+--host\s+(\S+))?\s*$/i);if(s){let i=s[1].toLowerCase(),a=pm(s[2]);if(a===null)return{kind:"error",message:"Port must be an integer between 1 and 65535."};let l=s[3],c=s[4]??"127.0.0.1";if(l){let d=dm(l);if(d)return{kind:"error",message:d}}return{kind:"expose",options:{kind:i,port:a,host:c,relayUrl:"",name:l,background:!0}}}return{kind:"error",message:"Usage: /tunnel login \u2026 | /tunnel status | /tunnel http <port> | /tunnel tcp <port> | /tunnels | /tunnel stop <id>"}}import Mk from"ws";import{URL as hm}from"node:url";function bo(e){let t=new hm(e);return t.protocol=t.protocol==="https:"?"wss:":"ws:",(!t.pathname||t.pathname==="/")&&(t.pathname="/control"),t.toString()}function fm(e){return new hm("/health",e).toString()}async function Ks(e,t,n=1e4){let o=t.trim();if(!o)return{ok:!1,healthOk:!1,controlOk:!1,error:"Tunnel token is missing."};let r=fm(e),s=!1,i;try{let c=await fetch(r,{method:"GET",signal:AbortSignal.timeout(n)});if(!c.ok)return{ok:!1,healthOk:!1,controlOk:!1,error:`Health HTTP ${c.status} (${r})`};let d=await c.json();if(!d?.ok)return{ok:!1,healthOk:!1,controlOk:!1,error:"Health JSON missing ok:true"};s=!0,typeof d.version=="string"&&(i=d.version)}catch(c){return{ok:!1,healthOk:!1,controlOk:!1,error:`Health fetch failed: ${String(c)}`}}let a=bo(e),l=!1;try{await new Promise((c,d)=>{let u=new Mk(a,{headers:{Authorization:`Bearer ${o}`}}),m=setTimeout(()=>{u.terminate(),d(new Error("WSS auth timeout"))},n);u.once("message",h=>{clearTimeout(m);try{let f=JSON.parse(h.toString());if(f.type!=="auth_ok"){d(new Error(`Expected auth_ok, got ${f.type??"?"}`));return}u.close(),c()}catch(f){d(f)}}),u.once("error",h=>{clearTimeout(m),d(h)})}),l=!0}catch(c){return{ok:!1,healthOk:!0,healthVersion:i,controlOk:!1,error:`Control WebSocket: ${String(c)}`}}return{ok:!0,healthOk:s,healthVersion:i,controlOk:!0}}bn();kn();kn();bn();import wm from"node:crypto";import Pk from"node:http";import Ek from"node:net";import pn from"ws";function Ys(e,t,n=Buffer.alloc(0)){let o=Buffer.allocUnsafe(9+n.length);return o.writeUInt8(e,0),o.writeUInt32BE(t,1),o.writeUInt32BE(n.length,5),n.copy(o,9),o}function gm(e){if(e.length<9)return null;let t=e.readUInt8(0),n=e.readUInt32BE(1),o=e.readUInt32BE(5);return e.length<9+o?null:{frameType:t,streamId:n,payload:e.subarray(9,9+o)}}function Sl(e){try{let t=JSON.parse(e);return!t||typeof t!="object"||typeof t.type!="string"?null:t}catch{return null}}function ko(e){return JSON.stringify(e)}function Ak(e){let t=wm.createHash("sha1").update(e,"utf8").digest("hex").slice(0,8);return Number.parseInt(t,16)>>>0}var Qs=class{constructor(t){this.opts=t;let n=wm.randomBytes(4).toString("hex");this.record={id:n,kind:t.expose.kind,slug:t.expose.name?.trim().toLowerCase()??"",localHost:t.expose.host,localPort:t.expose.port,publicUrl:"",status:"connecting",startedAt:new Date().toISOString()}}ws=null;record;tcpStreams=new Map;tcpStreamIds=new Map;stopped=!1;pingTimer=null;getRecord(){return{...this.record}}setStatus(t,n){this.record.status=t,this.record.error=n,this.opts.onStatus?.(this.getRecord())}async start(){if(this.stopped)throw new Error("Tunnel client already stopped.");let t=bo(this.opts.relayUrl),n=new pn(t,{headers:{Authorization:`Bearer ${this.opts.token}`}});this.ws=n,await new Promise((r,s)=>{let i=c=>{l(),s(c)},a=()=>{l(),r()},l=()=>{n.off("error",i),n.off("open",a)};n.once("error",i),n.once("open",a)}),n.on("message",(r,s)=>{if(s){this.handleBinary(Buffer.isBuffer(r)?r:Buffer.from(r));return}let i=typeof r=="string"?r:r.toString("utf8");this.handleControl(i)}),n.on("close",()=>{this.stopped||this.setStatus("error","Relay connection closed."),this.cleanupTcpStreams()}),n.on("error",r=>{this.stopped||this.setStatus("error",String(r))}),this.pingTimer=setInterval(()=>{n.readyState===pn.OPEN&&n.send(ko({type:"ping"}))},3e4),n.send(ko({type:"register",id:this.record.id,kind:this.opts.expose.kind,localHost:this.opts.expose.host,localPort:this.opts.expose.port,...this.opts.expose.name?{name:this.opts.expose.name}:{}}));let o=await this.waitForRegistered();return this.record.slug=o.slug,this.record.publicUrl=o.publicUrl,this.setStatus("active"),this.getRecord()}waitForRegistered(){let t=this.ws;return t?new Promise((n,o)=>{let r=s=>{if(typeof s!="string"&&!Buffer.isBuffer(s))return;let i=typeof s=="string"?s:s.toString("utf8"),a=Sl(i);if(a){if(a.type==="registered"&&a.id===this.record.id){t.off("message",r),n(a);return}a.type==="error"&&(t.off("message",r),o(new Error(a.message)))}};t.on("message",r)}):Promise.reject(new Error("WebSocket not connected."))}async handleControl(t){let n=Sl(t);if(n&&!(n.type==="pong"||n.type==="auth_ok")){if(n.type==="error"){this.setStatus("error",n.message);return}if(n.type==="http"){await this.handleHttpRequest(n);return}if(n.type==="tcp_open"){await this.handleTcpOpen(n.streamId);return}n.type==="tcp_close"&&this.closeTcpStream(n.streamId)}}handleBinary(t){let n=gm(t);if(!n)return;let o=[...this.tcpStreamIds.entries()].find(([,s])=>s===n.streamId)?.[0];if(!o)return;let r=this.tcpStreams.get(o);if(r){if(n.frameType===2){r.write(n.payload);return}n.frameType===3&&(r.end(),this.tcpStreams.delete(o),this.tcpStreamIds.delete(o))}}async handleHttpRequest(t){let n=this.ws;if(!n||n.readyState!==pn.OPEN)return;let o=t.bodyBase64?Buffer.from(t.bodyBase64,"base64"):void 0,r={host:this.opts.expose.host,port:this.opts.expose.port,method:t.method,path:t.path,headers:{...t.headers}};await new Promise(s=>{let i=Pk.request(r,a=>{let l=[];a.on("data",c=>l.push(Buffer.isBuffer(c)?c:Buffer.from(c))),a.on("end",()=>{let c=Buffer.concat(l);n.send(ko({type:"http_res",requestId:t.requestId,status:a.statusCode??502,headers:a.headers,...c.length>0?{bodyBase64:c.toString("base64")}:{}})),s()})});i.on("error",a=>{n.send(ko({type:"http_res",requestId:t.requestId,status:502,headers:{"content-type":"text/plain"},bodyBase64:Buffer.from(String(a)).toString("base64")})),s()}),o&&o.length>0&&i.write(o),i.end()})}async handleTcpOpen(t){let n=this.ws;if(!n||n.readyState!==pn.OPEN)return;let o=Ak(t);this.tcpStreamIds.set(t,o);let r=Ek.connect({host:this.opts.expose.host,port:this.opts.expose.port});this.tcpStreams.set(t,r),r.on("data",s=>{n.readyState===pn.OPEN&&n.send(Ys(2,o,Buffer.isBuffer(s)?s:Buffer.from(s)))}),r.on("close",()=>{n.readyState===pn.OPEN&&n.send(Ys(3,o)),this.tcpStreams.delete(t),this.tcpStreamIds.delete(t)}),r.on("error",()=>{n.readyState===pn.OPEN&&n.send(Ys(3,o)),this.tcpStreams.delete(t),this.tcpStreamIds.delete(t)})}closeTcpStream(t){let n=this.tcpStreams.get(t);n&&n.destroy(),this.tcpStreams.delete(t),this.tcpStreamIds.delete(t)}cleanupTcpStreams(){for(let t of this.tcpStreams.values())t.destroy();this.tcpStreams.clear(),this.tcpStreamIds.clear()}async stop(){if(this.stopped)return;this.stopped=!0,this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null);let t=this.ws;t&&t.readyState===pn.OPEN&&(t.send(ko({type:"unregister",id:this.record.id})),t.close()),this.cleanupTcpStreams(),this.setStatus("stopped")}};var Vs=class{clients=new Map;list(){return[...new Set(this.clients.values())].map(t=>t.getRecord())}getActiveCount(){return[...new Set(this.clients.values())].filter(t=>t.getRecord().status==="active").length}async expose(t,n){let o=St();if(!o)throw new Error("No tunnel token configured. Run `omnish tunnel login` or set OMNISH_TUNNEL_TOKEN.");let r=t.tunnelMaxActive>0?t.tunnelMaxActive:5;if(this.getActiveCount()>=r)throw new Error(`Active tunnel limit reached (${r}). Stop one with \`omnish tunnel stop <id>\`.`);let s=n.relayUrl||ct(t.tunnelRelayUrl||Ee),i=new Qs({relayUrl:s,token:o,expose:n,onStatus:l=>{(l.status==="stopped"||l.status==="error")&&this.clients.delete(l.id)}}),a=await i.start();return this.clients.set(a.id,i),a.slug&&this.clients.set(a.slug,i),a}async stop(t){let n=t.trim().toLowerCase(),o=this.clients.get(n)??this.clients.get(t.trim());if(!o)return null;let r=o.getRecord();return await o.stop(),this.clients.delete(r.id),r.slug&&this.clients.delete(r.slug),o.getRecord()}async stopAll(){let t=[...new Set(this.clients.values())];await Promise.all(t.map(n=>n.stop())),this.clients.clear()}};async function bm(e,t){let n=await fetch(e,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(t),signal:AbortSignal.timeout(15e3)}),o=await n.json();return!n.ok||!o.token?{ok:!1,error:o.error??`HTTP ${n.status}`}:{ok:!0,token:o.token}}function Xs(e,t){let n=new URL("/auth/signup",e).toString();return bm(n,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:t.password})}function Zs(e,t){let n=new URL("/auth/login",e).toString();return bm(n,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:t.password})}var vo=new Vs;function Ln(){return vo}function Ik(e){let t=[z(e,"omnish tunnel"),w(e,"Expose local HTTP or TCP ports through the omnish relay."),"",z(e,"Usage:"),` ${v(e,"omnish tunnel signup [--email <email>] [--phone <phone>] [--password <pass>] [--relay <url>]")}`,` ${v(e,"omnish tunnel login [--token <token>] [--relay <url>]")}`,` ${v(e,"omnish tunnel login --email <email> --password <pass> [--relay <url>]")}`,` ${v(e,"omnish tunnel logout")}`,` ${v(e,"omnish tunnel status [--relay <url>]")}`,` ${v(e,"omnish tunnel http <port> [--host <addr>] [--name <slug>] [--relay <url>] [--background]")}`,` ${v(e,"omnish tunnel tcp <port> [--host <addr>] [--name <slug>] [--relay <url>] [--background]")}`,` ${v(e,"omnish tunnel list")}`,` ${v(e,"omnish tunnel stop <id|slug>")}`,"",w(e,"Secrets live in ~/.omnish/tunnel-auth.json or OMNISH_TUNNEL_TOKEN."),w(e,`Default relay: ${Ee}`),""];console.log(t.join(`
295
+ `))}async function Lk(){let e=Cl.createInterface({input:Rl,output:ei});try{return(await e.question("Tunnel token: ")).trim()}finally{e.close()}}async function km(e){let t=vl(e),n=ei,o=process.stderr;if(t.kind==="help"){Ik(n);return}if(t.kind==="error"){console.error(C(o,t.message)),process.exitCode=1;return}let r=S();if(t.kind==="signup"){let s=t.relayUrl||ct(r.tunnelRelayUrl||Ee),i=Cl.createInterface({input:Rl,output:ei});try{let a=t.email?.trim()||(await i.question("Email (or leave empty for phone): ")).trim(),l=t.phone?.trim()||"";if(a||(l=l||(await i.question("Phone: ")).trim()),!a&&!l){console.error(C(o,"Email or phone is required.")),process.exitCode=1;return}let c=t.password||(await i.question("Password (min 8 chars): ")).trim();if(c.length<8){console.error(C(o,"Password must be at least 8 characters.")),process.exitCode=1;return}let d=await Xs(s,{...a?{email:a}:{},...l?{phone:l}:{},password:c});if(!d.ok){console.error(C(o,d.error)),process.exitCode=1;return}vt({token:d.token,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(U(n,"Account created. Token saved."))}finally{i.close()}return}if(t.kind==="login"){if(t.email||t.phone){let a=t.relayUrl||ct(r.tunnelRelayUrl||Ee),l=Cl.createInterface({input:Rl,output:ei});try{let c=t.password||(await l.question("Password: ")).trim(),d=await Zs(a,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:c});if(!d.ok){console.error(C(o,d.error)),process.exitCode=1;return}vt({token:d.token,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(U(n,"Logged in. Token saved."))}finally{l.close()}return}let i=t.token?.trim()||await Lk();if(!i){console.error(C(o,"Tunnel token is required.")),process.exitCode=1;return}vt({token:i,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(U(n,"Tunnel token saved."));return}if(t.kind==="logout"){Br(),console.log(U(n,"Tunnel token removed."));return}if(t.kind==="status"){let s=t.relayUrl||ct(r.tunnelRelayUrl||Ee),i=St(),a=!!process.env.OMNISH_TUNNEL_TOKEN?.trim(),l=await Ks(s,i);console.log(`${w(n,"relay:")} ${v(n,s)}`),console.log(`${w(n,"token:")} ${v(n,i?`configured${a?" (OMNISH_TUNNEL_TOKEN)":""}`:C(o,"missing"))}`),console.log(`${w(n,"health:")} ${l.healthOk?v(n,`ok${l.healthVersion?` (${l.healthVersion})`:""}`):C(o,"fail")}`),console.log(`${w(n,"control:")} ${l.controlOk?v(n,"auth ok"):C(o,"fail")}${l.error&&!l.ok?` \u2014 ${l.error}`:""}`),console.log(`${w(n,"active:")} ${v(n,String(vo.getActiveCount()))}`);return}if(t.kind==="list"){let s=vo.list();if(s.length===0){console.log(U(n,"(no active tunnels)"));return}for(let i of s)console.log(`${v(n,i.id)} ${i.kind} ${i.status} ${i.publicUrl||"(pending)"}
296
+ ${w(n,`${i.localHost}:${i.localPort}`)}`);return}if(t.kind==="stop"){let s=await vo.stop(t.target);if(!s){console.error(C(o,`No active tunnel matched "${t.target}".`)),process.exitCode=1;return}console.log(U(n,`Stopped tunnel ${s.id}.`));return}if(t.kind==="expose"){let s=t.options.relayUrl||ct(r.tunnelRelayUrl||Ee),i=await vo.expose(r,{...t.options,relayUrl:s});console.log(U(n,`${i.kind.toUpperCase()} tunnel active`)),console.log(`${w(n,"public:")} ${v(n,i.publicUrl)}`),console.log(`${w(n,"local:")} ${v(n,`${i.localHost}:${i.localPort}`)}`),console.log(`${w(n,"id:")} ${v(n,i.id)}`),t.options.background||(console.log(w(n,"Press Ctrl+C to stop.")),await new Promise(a=>{let l=async()=>{await vo.stop(i.id),a()};process.once("SIGINT",l),process.once("SIGTERM",l)}))}}function vm(e){let t=e.trim().match(/^(\d+)\.(\d+)\.(\d+)/);return t?[Number(t[1]),Number(t[2]),Number(t[3])]:null}function Sm(e,t){let n=vm(e),o=vm(t);if(!n||!o)return e.trim().localeCompare(t.trim());for(let r=0;r<3;r++)if(n[r]!==o[r])return n[r]<o[r]?-1:1;return 0}var Ok="https://registry.npmjs.org",xm=null,Tl=0;function nr(){return xm}function Nk(e){let t=e.trim();return t.length<1||t.length>214?!1:!/\s/.test(t)}function Cm(e){let t=e.trim();if(!t||t.length>2048)return null;try{let n=new URL(t);return n.protocol!=="https:"?null:n.href}catch{return null}}async function Fk(e){let t=e.trim(),n=`${Ok}/${encodeURIComponent(t)}/latest`;try{let o=await fetch(n,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(2e4)});if(!o.ok)return{error:`npm registry: HTTP ${o.status}`};let r=await o.json(),s=typeof r.version=="string"?r.version.trim():"";return s?{version:s}:{error:"npm registry: missing version in response"}}catch(o){return{error:`npm registry: ${String(o)}`}}}async function Wk(e){try{let t=await fetch(e,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(15e3)});if(!t.ok)return{error:`updateInfoUrl: HTTP ${t.status}`};let n=await t.arrayBuffer(),o=n.byteLength>65536?n.slice(0,65536):n,r=new TextDecoder("utf-8",{fatal:!1}).decode(o),s;try{s=JSON.parse(r)}catch{return{error:"updateInfoUrl: body is not JSON"}}if(!s||typeof s!="object"||Array.isArray(s))return{error:"updateInfoUrl: JSON must be an object"};let i=s,a=null;typeof i.message=="string"&&i.message.trim()&&(a=i.message.trim().slice(0,1500));let l=null,c=i.link??i.url;return typeof c=="string"&&c.trim()&&(l=Cm(c)),{message:a,link:l}}catch(t){return{error:`updateInfoUrl: ${String(t)}`}}}function _k(e){return!Number.isFinite(e)||e<36e5?36e5:e>6048e5?6048e5:Math.floor(e)}async function or(e,t){let n=Nk(t.updateCheckPackageName)?t.updateCheckPackageName.trim():"omnish",o=await Fk(n),r="version"in o?o.version:null,s="error"in o?o.error:null,i=!1;r&&!s&&(i=Sm(e,r)<0);let a=null,l=null,c=null,d=Cm(t.updateInfoUrl);if(d){let m=await Wk(d);"error"in m?c=m.error:(a=m.message,l=m.link)}let u={runningVersion:e,checkedAtIso:new Date().toISOString(),registryPackage:n,registryLatest:r,registryError:s,updateAvailable:i,infoMessage:a,infoLink:l,infoError:c};return xm=u,u}function ti(e){if(!e)return;let t=[];if(e.registryError?t.push(`npm check: ${e.registryError}`):e.registryLatest&&t.push(e.updateAvailable?`npm latest *${e.registryLatest}* (running ${e.runningVersion})`:`npm latest ${e.registryLatest} (up to date)`),e.infoError?t.push(`info URL: ${e.infoError}`):e.infoMessage&&t.push(`notice: ${e.infoMessage}`),t.length!==0)return t.join(" \xB7 ")}function ni(e){let t=!1,n=async()=>{if(t)return;let s=e.getConfig();if(!s.updateCheckEnabled)return;let i=_k(s.updateCheckIntervalMs),a=Date.now();if(!(Tl!==0&&a-Tl<i)){Tl=a;try{let l=await or(e.getRunningVersion(),s);l.updateAvailable?e.log.info({updateAvailable:!0,running:l.runningVersion,npmLatest:l.registryLatest,pkg:l.registryPackage},"omnish update check: newer npm version available"):e.log.info({running:l.runningVersion,npmLatest:l.registryLatest,pkg:l.registryPackage},"omnish update check ok"),l.infoMessage&&e.log.info({len:l.infoMessage.length},"omnish update info message present")}catch(l){e.log.warn({err:String(l)},"omnish update check failed")}}},o=setInterval(()=>void n(),6e4),r=setTimeout(()=>void n(),3e4);return()=>{t=!0,clearInterval(o),clearTimeout(r)}}pe();Ve();import Rm from"node:path";import{glob as Dk,stat as Uk}from"node:fs/promises";function oi(e){let t=e.trim();if(!t||t.startsWith("-- ")||t==="--")return null;let n=t.indexOf(" -- "),o,r;return n!==-1?(o=t.slice(0,n).trim(),r=t.slice(n+4).trim()||void 0):o=t,(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'"))&&(o=o.slice(1,-1)),o.trim()?{selectorPart:o,caption:r}:null}function Bk(e){return/[*?[]/.test(e)}function Hk(e){return e.split(",").map(t=>t.trim()).filter(t=>t.length>0)}async function rr(e,t){let n=Hk(t),o=new Set,r=[];for(let s of n){if(Bk(s)){for await(let a of Dk(s,{cwd:e,withFileTypes:!1})){let l=Rm.resolve(e,a);o.has(l)||(o.add(l),r.push(l))}continue}let i=Rm.resolve(e,s);o.has(i)||(o.add(i),r.push(i))}return r}async function sr(e){for(let t of e)try{if(!(await Uk(t)).isFile())return{ok:!1,error:`Not a file: ${t}`}}catch{return{ok:!1,error:`File not found: ${t}`}}return{ok:!0}}q();import Mm from"node:fs";import jk from"node:path";var On="__omnish_shortcuts_global__",Tm=500,Pm=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,Gk=new Set(["help","apps","bg","jobs","log","tail","kill","send","file","files","receive","reload","restart","updates","gateway","gw","mode","allow","deny","allowlist","whatsapp","wa","telegram","tg","cowork","cw","shortcut","shortcuts","alias","aliases"]),ir=new Map,$m=!1;function Jk(){try{let e=Mm.readFileSync(yr,"utf8"),t=JSON.parse(e);if(t&&typeof t=="object")for(let[n,o]of Object.entries(t)){if(!o||typeof o!="object")continue;let r={};for(let[s,i]of Object.entries(o)){let a=String(s).toLowerCase();typeof i=="string"&&i.length>0&&(r[a]=i.trim())}ir.set(n,r)}}catch{}}function ri(){H(jk.dirname(yr));let e={};for(let[t,n]of ir)Object.entries(n).length>0&&(e[t]={...n});Mm.writeFileSync(yr,JSON.stringify(e,null,2)+`
297
+ `,{mode:384})}function si(){$m||(Jk(),$m=!0)}function qk(e){return Gk.has(e.trim().toLowerCase())}function Tt(e){let t=e.trim();if(!t)return{ok:!1,error:"Name is empty."};let n=t.toLowerCase();return Pm.test(n)?qk(n)?{ok:!1,error:`Reserved name: ${n}`}:{ok:!0,normalized:n}:{ok:!1,error:`Invalid name (use letters, digits, _ or -; max 32 chars): ${t}`}}function $l(e){let t=e.replace(/\r\n/g,`
298
+ `).replace(/\n/g," ").trim();return t?t.length>Tm?{ok:!1,error:`Body too long (max ${Tm} characters).`}:{ok:!0,body:t}:{ok:!1,error:"Body is empty."}}function Rt(e,t){si();let n=ir.get(e);return!n&&t&&(n={},ir.set(e,n)),n??{}}function Em(e){let t=e.trim();if(/^--global$/i.test(t))return{mode:"global",remainder:""};if(/^-g$/i.test(t))return{mode:"global",remainder:""};if(/^--chat$/i.test(t))return{mode:"chat",remainder:""};if(/^-p$/i.test(t))return{mode:"chat",remainder:""};let n=/^--global\s+/i.exec(t);if(n)return{mode:"global",remainder:t.slice(n[0].length).trimStart()};let o=/^-g\s+/i.exec(t);if(o)return{mode:"global",remainder:t.slice(o[0].length).trimStart()};let r=/^--chat\s+/i.exec(t);if(r)return{mode:"chat",remainder:t.slice(r[0].length).trimStart()};let s=/^-p\s+/i.exec(t);return s?{mode:"chat",remainder:t.slice(s[0].length).trimStart()}:{mode:"resolved",remainder:t}}function Ml(e){let t=e.trim();if(/^--global$/i.test(t))return{scope:"global",remainder:"",explicit:!0};if(/^-g$/i.test(t))return{scope:"global",remainder:"",explicit:!0};if(/^--chat$/i.test(t))return{scope:"chat",remainder:"",explicit:!0};if(/^-p$/i.test(t))return{scope:"chat",remainder:"",explicit:!0};let n=/^--global\s+([\s\S]*)$/i.exec(t);if(n?.[1]!==void 0)return{scope:"global",remainder:n[1].trimStart(),explicit:!0};let o=/^-g\s+([\s\S]*)$/i.exec(t);if(o?.[1]!==void 0)return{scope:"global",remainder:o[1].trimStart(),explicit:!0};let r=/^--chat\s+([\s\S]*)$/i.exec(t);if(r?.[1]!==void 0)return{scope:"chat",remainder:r[1].trimStart(),explicit:!0};let s=/^-p\s+([\s\S]*)$/i.exec(t);return s?.[1]!==void 0?{scope:"chat",remainder:s[1].trimStart(),explicit:!0}:{scope:"chat",remainder:t,explicit:!1}}function Pl(e){let t=Ml(e);return{scope:t.scope,remainder:t.remainder}}function Am(e){let t=e.trim();return!t||/^-+$/i.test(t)?{filter:"merged"}:/^(?:--global|-g)$/i.test(t)?{filter:"global"}:/^(?:--chat|-p)$/i.test(t)?{filter:"chat"}:{filter:"merged",bad:t}}function zk(e){let t=e.trim().toLowerCase();if(t==="--global"||t==="-g")return"global";if(t==="--chat"||t==="-p")return"chat"}function Im(e){let t=e.trim().match(/^(\S+)\s+(--global|-g|--chat|-p)\s*$/i);if(!t?.[1]||!t[2])return;let n=zk(t[2]);if(n)return{name:t[1],target:n}}function Kk(e,t){si();let n=e,o=Rt(n,!1),r=Rt(On,!1);if(t==="chat")return Object.entries(o).map(([a,l])=>({name:a,body:l,scope:"chat"})).sort((a,l)=>a.name.localeCompare(l.name));if(t==="global")return Object.entries(r).map(([a,l])=>({name:a,body:l,scope:"global"})).sort((a,l)=>a.name.localeCompare(l.name));let s=new Set([...Object.keys(o),...Object.keys(r)]),i=[];for(let a of[...s].sort()){if(a in o){let c=o[a];c!==void 0&&i.push({name:a,body:c,scope:"chat"});continue}let l=r[a];l!==void 0&&i.push({name:a,body:l,scope:"global"})}return i}function Lm(e,t="merged"){return Kk(e,t)}function El(e,t){let n=t.trim().toLowerCase(),o=Rt(e,!1)[n];return o!==void 0?o:Rt(On,!1)[n]}function ii(e,t){let n=Tt(t);if(!n.ok)return;let o=n.normalized,r=Rt(e,!1)[o];if(r!==void 0)return{name:o,body:r,scope:"chat"};let s=Rt(On,!1)[o];if(s!==void 0)return{name:o,body:s,scope:"global"}}function Ut(e,t,n){let o=n.trim().toLowerCase(),r=e==="global"?On:t;return Rt(r,!1)[o]}function ar(e,t,n,o="chat"){let r=Tt(t);if(!r.ok)throw new Error(r.error);let s=$l(n);if(!s.ok)throw new Error(s.error);let i=o==="global"?On:e,a=Rt(i,!0);a[r.normalized]=s.body,ri()}function Om(e,t,n="chat"){let o=t.trim().toLowerCase(),r=n==="global"?On:e;si();let s=ir.get(r);return!s||!(o in s)?!1:(delete s[o],ri(),!0)}function Al(e,t,n){let o=Tt(t);if(!o.ok)return{ok:!1,error:o.error};let r=o.normalized,s=e;si();let i=Rt(s,!0),a=Rt(On,!0),l=i[r],c=a[r];if(n==="global"){if(l!==void 0){let d=$l(l);return d.ok?(a[r]=d.body,delete i[r],ri(),{ok:!0,kind:"moved",target:"global",name:r}):{ok:!1,error:d.error}}return c!==void 0?{ok:!0,kind:"noop",message:`Shortcut "${r}" is already shared on this gateway.`}:{ok:!1,error:`No shortcut "${r}" in this chat to share. Add with /shortcut add ${r} \u2026 or make the shared copy private with /shortcut set -p ${r}.`}}if(c!==void 0){let d=$l(c);return d.ok?(i[r]=d.body,delete a[r],ri(),{ok:!0,kind:"moved",target:"chat",name:r}):{ok:!1,error:d.error}}return l!==void 0?{ok:!0,kind:"noop",message:`Shortcut "${r}" is already private to this chat.`}:{ok:!1,error:`No shared shortcut "${r}" to make private. Add with /shortcut add --global ${r} \u2026 or share from this chat with /shortcut set -g ${r}.`}}function Nm(e){let t=e.trim();return t.length>0&&!/\s/.test(t)&&Pm.test(t.toLowerCase())}import Fm from"node:fs";var Wm=64,Yk=1024*1024;function _m(e){return e.fileReceiveMaxBytes>0?e.fileReceiveMaxBytes:Yk}function Dm(e){let t;try{t=JSON.parse(e)}catch(r){return{ok:!1,error:`Invalid JSON: ${String(r)}`}}let n;if(Array.isArray(t))n=t;else if(t&&typeof t=="object"&&!Array.isArray(t)){let r=t,s=Object.keys(r);if(s.length!==1||s[0]!=="tasks")return{ok:!1,error:'JSON must be an array or a single-key object: { "tasks": [ \u2026 ] }.'};let i=r.tasks;if(!Array.isArray(i))return{ok:!1,error:'"tasks" must be an array.'};n=i}else return{ok:!1,error:'JSON must be an array or { "tasks": [ \u2026 ] }.'};if(n.length===0)return{ok:!1,error:"Queue JSON must contain at least one job."};if(n.length>Wm)return{ok:!1,error:`Too many jobs (max ${Wm}).`};let o=[];for(let r=0;r<n.length;r++){let s=n[r];if(!s||typeof s!="object"||Array.isArray(s))return{ok:!1,error:`Job ${r+1}: must be an object with "recipe" and "task".`};let i=s;if(Object.keys(i).length!==2||typeof i.recipe!="string"||typeof i.task!="string")return{ok:!1,error:`Job ${r+1}: must contain only "recipe" and "task" string fields.`};o.push({recipe:i.recipe,task:i.task})}return{ok:!0,jobs:o}}function Um(e,t,n){let o=[];for(let r=0;r<n.length;r++){let{recipe:s,task:i}=n[r],a=Ye(e,t,s);if(!a)return{ok:!1,error:`Job ${r+1}: unknown recipe "${s}".`};let l=a.taskEnv??"OMNISH_TASK";if(!ao(a.command,l))return{ok:!1,error:`Job ${r+1}: recipe "${s}" command must reference "$${l}".`};let c=ms(i,t.recipesMaxTaskChars);if(!c.ok)return{ok:!1,error:`Job ${r+1}: ${c.error}`};let d=a.promptTemplate?hs(a.promptTemplate,l,c.task):c.task,u={[l]:d};o.push({command:a.command,extraEnv:u,recipeLabel:s})}return{ok:!0,items:o}}function Il(e,t){let n;try{n=Fm.statSync(e)}catch{return{ok:!1,error:`Cannot read file: ${e}`}}if(!n.isFile())return{ok:!1,error:`Not a file: ${e}`};if(n.size>t)return{ok:!1,error:`File too large (max ${t} bytes for queue load).`};try{return{ok:!0,text:Fm.readFileSync(e,"utf8")}}catch(o){return{ok:!1,error:String(o)}}}q();import Bm from"node:fs";import So from"node:process";var Qk=120;function Bt(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function Hm(e){let t=e.trim();return t==="/service"||t.startsWith("/service ")?t.slice(8).trim():null}async function jm(e,t){let n=t.trim().split(/\s+/),o=(n[0]??"").toLowerCase();if(!t.trim()||o==="help")return ee(Tu(e));if(o==="status"){let s=Wt(),i=(()=>{try{return Bm.existsSync(ge)?`gateway.pid: ${Bm.readFileSync(ge,"utf8").trim()}`:"gateway.pid: (missing)"}catch(m){return`gateway.pid: (read error: ${String(m)})`}})(),a=So.env.OMNISH_BACKGROUND_GATEWAY==="1"?"This process: background gateway (OMNISH_BACKGROUND_GATEWAY=1).":"This process: foreground gateway session.",l=typeof So.env.OMNISH_HOME=="string"&&So.env.OMNISH_HOME.trim()?`OMNISH_HOME env: ${So.env.OMNISH_HOME.trim()}`:"OMNISH_HOME env: (not set \u2014 using default data dir)",c=s.error?s.error:`Node: ${s.nodePath}
299
+ Script: ${s.scriptPath}`,d=["*Service status*","",`platform: ${So.platform}`,a,l,`data dir: ${D}`,i,`default log: ${Be}`,"",c,"",e.serviceInstallFromChat?"Install from chat: enabled (/service install).":"Install from chat: off \u2014 `/config set serviceInstallFromChat true` to allow /service install."].join(`
300
+ `),u=["<b>Service status</b>","",`<code>${Bt(So.platform)}</code>`,`<br/><code>${Bt(a)}</code>`,`<br/><code>${Bt(l)}</code>`,`<br/>data dir: <code>${Bt(D)}</code>`,`<br/><code>${Bt(i)}</code>`,`<br/>default log: <code>${Bt(Be)}</code>`,"",`<pre>${Bt(c)}</pre>`,"",e.serviceInstallFromChat?"Install from chat: enabled.":"Install from chat: off \u2014 <code>/config set serviceInstallFromChat true</code>."].join(`
301
+ `);return we(d,u)}if(o==="instructions"){let s=Wt();if(s.error)return p(s.error);let i=qr(s);return p(`*Install hints*
302
+
303
+ ${i}`)}if(o==="logs"){let s=n.length>=2?Number.parseInt(n[1],10):80,i=Number.isFinite(s)&&s>0?Math.min(s,Qk):80,a=cs(Be,i),l=[`*Gateway log* (last ${i} lines)
304
+ ${Be}
289
305
  `,"```",a,"```"].join(`
290
- `),d=`<b>Gateway log</b> (last ${i} lines)<br/><code>${Wt(Ue)}</code><pre>${Wt(a)}</pre>`;return fe(l,d)}if(o==="install"){if(!e.serviceInstallFromChat)return p("Install from chat is disabled. Same trust as shell \u2014 enable with:\n`/config set serviceInstallFromChat true`\nThen `/service install` again.");let s=Br();return p(s.ok?`*Installed*
306
+ `),c=`<b>Gateway log</b> (last ${i} lines)<br/><code>${Bt(Be)}</code><pre>${Bt(a)}</pre>`;return we(l,c)}if(o==="install"){if(!e.serviceInstallFromChat)return p("Install from chat is disabled. Same trust as shell \u2014 enable with:\n`/config set serviceInstallFromChat true`\nThen `/service install` again.");let s=is();return p(s.ok?`*Installed*
291
307
  ${s.detail}`:`*Install failed*
292
- ${s.detail}`)}if(o==="uninstall"){if(!e.serviceInstallFromChat)return p("Uninstall from chat is disabled. Enable with `/config set serviceInstallFromChat true` or remove files on the host manually.");let s=Hr();return p(`*Uninstall*
293
- ${s.detail}`)}return p("Unknown /service command. Try /service help")}st();function cm(e){let t=e.trim();return t==="/pull"||t.startsWith("/pull ")?t.slice(5).trim():null}var dl=new Set(["video","audio","subs","transcript","all"]);function Db(e){let t=e.trim(),n=!1,o=!1,r=!0;for(;r;){if(r=!1,/^--bg\b/i.test(t)||/^--background\b/i.test(t)){n=!0,t=t.replace(/^--(?:bg|background)\s*/i,"").trim(),r=!0;continue}(/^--notify\b/i.test(t)||/^-N\b/i.test(t))&&(o=!0,t=t.replace(/^--notify\s*/i,"").replace(/^-N\s*/i,"").trim(),r=!0)}let s=t.split(/\s+/),i=(s[0]??"").toLowerCase();if(dl.has(i)){let a=s.slice(1).join(" ").trim();return{mode:i,url:a,background:n,notify:o}}return ha(t)||/^https?:\/\//i.test(t)?{mode:null,url:t.trim(),background:n,notify:o}:{mode:null,url:t,background:n,notify:o}}function Ub(e,t,n){let o=Ot();if(o.error)throw new Error(o.error);let r=o.nodePath,s=o.scriptPath,i=a=>JSON.stringify(a);return`${r} ${s} pull-exec ${e} ${i(t)} ${i(n)}`}async function pl(e,t,n,o){let r=t.trim(),s=(r.split(/\s+/)[0]??"").toLowerCase();if(!r||s==="help")return{kind:"text",body:X(Wi(e))};if(s==="doctor"){let{text:u}=Dr(e);return{kind:"text",body:p(`*Pull doctor*
294
-
295
- ${u}`)}}if(s==="setup"||s==="instructions")return{kind:"text",body:p(Fr())};if(s==="install"){if(!e.pullInstallFromChat)return{kind:"text",body:p(`Install from chat is off. Enable: /config set pullInstallFromChat true
296
- Or on the host: omnish pull install`)};let u=/\b--whisper\b/i.test(r);try{let c=await Ur({whisper:u});return{kind:"text",body:p(`*Pull install*
297
-
298
- ${c.messages.join(`
299
- `)}`)}}catch(c){return{kind:"text",body:p(`Install failed: ${String(c)}`)}}}if(!e.pullEnabled)return{kind:"text",body:p("Pull is disabled. Enable: /config set pullEnabled true")};let i=Db(r);if(i.error)return{kind:"text",body:p(i.error)};let a=i.mode??e.pullDefaultMode??"audio";if(!dl.has(a))return{kind:"text",body:p(`Unknown mode. Try: ${[...dl].join(", ")}`)};if(!i.url)return{kind:"text",body:X(Wi(e))};try{Io(i.url)}catch(u){return{kind:"text",body:p(String(u))}}let l=Ao(e);if(!l.ytDlp)return{kind:"text",body:p("yt-dlp missing. Run: omnish pull install \u2014 or /pull setup")};if((a==="video"||a==="all")&&!l.ffmpeg)return{kind:"text",body:p("ffmpeg missing (needed for video). Run: omnish pull install")};if(a==="transcript"&&!l.whisper)return{kind:"text",body:p("whisper missing. Run: omnish pull install --whisper")};let d=ie(n).cwd;if(i.background||a==="all"||a==="transcript"){let u=Ub(a,i.url,d),{id:c,meta:m}=o.spawnJob(e.shell,u,{cwd:d,name:"pull",notifyPeerKey:i.notify?n:null}),h=i.notify?`
300
- Notify on completion: on`:"";return{kind:"text",body:p(`Pull job ${c} started (${a}).
301
- [cwd: ${d}]
302
- /log ${m.name??c}
303
- /tail ${m.name??c}${h}`)}}try{let u=await fa({cfg:e,mode:a,url:i.url,sessionCwd:d}),c=Bb(e,u.files);if(e.pullAutoSend&&c.length>0){let f=u.summary.slice(0,900);return c[0]={...c[0],caption:f},c.length===1?{kind:"file",spec:c[0]}:{kind:"files",specs:c}}let m=u.files.length-c.length,h=m>0?`
304
-
305
- (${m} file(s) skipped \u2014 over fileSendMaxBytes or classify error)`:"";return{kind:"text",body:p(u.summary+h)}}catch(u){return{kind:"text",body:p(`Pull failed: ${String(u)}`)}}}function Bb(e,t){let n=ae()?oo(e):e.fileSendMaxBytes,o=[];for(let r of t){let s=St(r,n);"error"in s||o.push(s)}return o}async function um(e,t,n,o){if(!e.pullEnabled||!e.pullUrlAutoDetect||!ha(t))return null;let r=e.pullDefaultMode||"audio";return pl(e,`${r} ${t.trim()}`,n,o)}pe();st();mn();function js(){let e=ae();return e?.platformUrl?e.platformUrl.replace(/\/$/,""):(S().tunnelRelayUrl||Ee).trim().replace(/\/$/,"")}function Gs(){let e=ae();return e?.token?{Authorization:`Bearer ${e.token}`}:{}}async function Qo(e){try{return await e.json()}catch{return{error:`HTTP ${e.status}`}}}function Hb(){return`Set platform URL: omnish config add tunnelRelayUrl ${Ee} \u2014 publish: omnish platform login`}function ml(){return ae()?.token?{ok:!0}:{ok:!1,error:`Publish requires a platform account token. ${Hb()}`}}async function dm(e){let t=new URLSearchParams;e?.kind&&t.set("kind",e.kind),e?.limit&&t.set("limit",String(e.limit));let n=t.toString(),o=`${js()}/v1/catalog/trending${n?`?${n}`:""}`,r=await fetch(o,{headers:Gs()}),s=await Qo(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}async function pm(e,t){let n=new URLSearchParams;n.set("q",e),t?.kind&&n.set("kind",t.kind),t?.category&&n.set("category",t.category),t?.limit&&n.set("limit",String(t.limit));let o=`${js()}/v1/catalog/search?${n}`,r=await fetch(o,{headers:Gs()}),s=await Qo(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}async function mm(e){let t=encodeURIComponent(e.trim()),n=`${js()}/v1/catalog/${t}`,o=await fetch(n,{headers:Gs()}),r=await Qo(o);return o.ok?r:{error:r.error??`HTTP ${o.status}`}}async function hm(e){let t=encodeURIComponent(e.trim()),n=`${js()}/v1/catalog/${t}/download`,o=await fetch(n,{method:"POST",headers:Gs()}),r=await Qo(o);return o.ok?r:{error:r.error??`HTTP ${o.status}`}}async function fm(e){let t=ml();if(!t.ok)return{error:t.error};let n=ae(),o=`${n.platformUrl.replace(/\/$/,"")}/v1/catalog`,r=await fetch(o,{method:"POST",headers:{"content-type":"application/json",Authorization:`Bearer ${n.token}`},body:JSON.stringify(e)}),s=await Qo(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}function gm(e){return e==="global"?"global":"chat"}function ym(e,t,n,o="global"){switch(e.kind){case"recipe":{let r=e.payload,s=Ke({...r,dangerous:void 0}),i=yn(s);if(!i.ok)return{ok:!1,error:i.error};let a=vt(e.name);if(!a.ok)return{ok:!1,error:a.error};try{zn(t,a.normalized,s,gm(o))}catch(d){return{ok:!1,error:String(d.message??d)}}let l=o==="global"?`/run ${a.normalized} <task>`:`/run ${a.normalized} <task> (this chat)`;return{ok:!0,message:`Installed recipe "${a.normalized}" (${o}). ${l}`}}case"app":{let r=e.payload;if(!r.command?.trim())return{ok:!1,error:"App payload has no command."};let s=Ke({command:r.command.trim(),label:r.label??e.title,description:r.description??e.description,category:e.category||"app"}),i=vt(e.name);if(!i.ok)return{ok:!1,error:i.error};try{zn(t,i.normalized,s,gm(o),{skipCommandValidation:!0})}catch(a){return{ok:!1,error:String(a.message??a)}}return{ok:!0,message:`Installed app template "${i.normalized}" (${o}). /apps start <session> ${r.command.trim()}`}}case"shortcut":{let r=e.payload,s=Ct(e.name);if(!s.ok)return{ok:!1,error:s.error};try{Yo(t,s.normalized,r.body,o==="global"?"global":"chat")}catch(i){return{ok:!1,error:String(i.message??i)}}return{ok:!0,message:`Installed shortcut "${s.normalized}" (${o}). Type ${s.normalized} to expand.`}}case"cowork":{let r=e.payload,s=Pe();if(Ze(s,e.name,t))return{ok:!1,error:`Cowork task "${e.name}" already exists for this chat. Remove it first or rename.`};let i=Date.now(),a={id:ko(),name:e.name,ownerPeerKey:t,command:r.command,cwd:r.cwd??"",outputDir:r.outputDir??"",schedule:r.schedule,enabled:r.enabled??!0,notify:r.notify??"self",notifyWhen:r.notifyWhen??"always",attachLog:r.attachLog??!1,attachFiles:r.attachFiles??[],lastCompletedSlotMs:null,createdAtMs:i};return s.push(a),We(s),{ok:!0,message:`Installed cowork task "${e.name}" for this chat. /cowork show ${e.name}`}}default:return{ok:!1,error:"Unknown catalog kind."}}}function wm(e){return{name:e.name,command:e.command,cwd:e.cwd,outputDir:e.outputDir,schedule:e.schedule,enabled:e.enabled,notify:e.notify,notifyWhen:e.notifyWhen,attachLog:e.attachLog,attachFiles:e.attachFiles}}function bm(e){let{dangerous:t,...n}=e;return Ke(n)}var hl=new Map;function fl(e,t){return`${e}:${t}`}function gl(e,t,n){hl.set(fl(e,t),n)}function km(e,t){return hl.get(fl(e,t))}function qs(e,t,n){let o=Number.parseInt(n,10);if(!Number.isFinite(o)||o<1)return null;let r=hl.get(fl(e,t));return!r||o>r.length?null:r[o-1].publicId}function Tn(e){return!!(e&&typeof e=="object"&&typeof e.error=="string")}function Js(e,t){return`${e.commandPrefix} online ${t}`}function vm(e){let t=r=>Js(e,r),n=e.defaultKind===void 0?" [recipe|app|cowork|shortcut]":` (${e.defaultKind} only)`,o=[`Online catalog \u2014 ${e.defaultKind??"all kinds"}`,"",t(`trending${e.defaultKind===void 0?" [kind]":""}`),t(`search <query>${n}`),t("show <publicId>"),t("<publicId> download [--chat|-p for this chat only]"),t("trending <n> download \u2014 install #n from last list"),t("list \u2014 repeat last trending/search list")];return e.commandPrefix==="/run"&&o.push("","Also: /apps online \u2026 \xB7 /cowork online \u2026 \xB7 /shortcut online \u2026","","Publish (platform account):","/run <recipe> publish [--title \u2026] [--category \u2026]","/apps <session> publish","/cowork <name> publish","/shortcut <name> publish"),p(o.join(`
306
- `))}function zs(e,t,n){if(e.length===0)return p(`${t}
307
- (no results)`);let o=[t,""];return e.forEach((r,s)=>{let i=r.category?` \xB7 ${r.category}`:"";o.push(`${s+1}. ${r.publicId} (${r.kind}${i}) \u2193${r.downloadCount} \u2014 ${r.title||r.name}`),r.description&&o.push(` ${r.description.slice(0,120)}`)}),o.push("",`Install: ${Js(n,"<publicId> download")} \u2014 or ${Js(n,"trending <n> download")}`),p(o.join(`
308
- `))}function Sm(e,t){let n=[`${e.publicId} (${e.kind})`,`title: ${e.title||e.name}`,e.description?`description: ${e.description}`:"",e.category?`category: ${e.category}`:"",e.tags?.length?`tags: ${e.tags.join(", ")}`:"",`downloads: ${e.downloadCount}`,`author: ${e.authorLabel||"(unknown)"}`,"","payload:"].filter(Boolean),o=e.payload;if(e.kind==="recipe"||e.kind==="app"){let r=o;if(n.push(` command: ${r.command??""}`),r.promptTemplate&&n.push(` template: ${r.promptTemplate.slice(0,400)}${r.promptTemplate.length>400?"\u2026":""}`),Array.isArray(r.steps)&&r.steps.length>0){n.push(` steps: ${r.steps.length}`);for(let[s,i]of r.steps.entries()){let a=typeof i=="string"?i:i.cmd;n.push(` ${s+1}. ${a??""}`)}}}else if(e.kind==="shortcut")n.push(` body: ${o.body}`);else if(e.kind==="cowork"){let r=o;n.push(` command: ${r.command??""}`),n.push(` schedule: ${r.schedule?.kind??"?"}`)}return n.push("",Js(t,`${e.publicId} download`)),p(n.join(`
309
- `))}var yl=new Set(["recipe","app","cowork","shortcut"]),jb={commandPrefix:"/run",listScope:"run"},xm={commandPrefix:"/apps",defaultKind:"app",listScope:"apps"},Cm={commandPrefix:"/cowork",defaultKind:"cowork",listScope:"cowork"},Rm={commandPrefix:"/shortcut",defaultKind:"shortcut",listScope:"shortcut"};function Gb(e){if(!e)return;let t=e.toLowerCase();return yl.has(t)?t:void 0}function Dt(e,t){return`${e.commandPrefix} online ${t}`}function qb(e,t){if(e.defaultKind)return t&&t.toLowerCase()!==e.defaultKind?{error:`This command only lists ${e.defaultKind} entries. Omit the kind suffix.`}:e.defaultKind;let n=Gb(t);return t&&!n?{error:`Unknown kind "${t}". Use: recipe, app, cowork, shortcut`}:n}function Jb(e,t){if(e.defaultKind){if(t.length>1&&yl.has(t[t.length-1].toLowerCase())){if(t[t.length-1].toLowerCase()!==e.defaultKind)return{error:`This command only searches ${e.defaultKind} entries. Omit the kind suffix.`};t.pop()}return{kind:e.defaultKind,query:t.join(" ")}}let n;return t.length>1&&yl.has(t[t.length-1].toLowerCase())&&(n=t.pop().toLowerCase()),{kind:n,query:t.join(" ")}}async function po(e,t,n,o){let r=e.trim();if(!r||/^help$/i.test(r))return vm(o);let s=r.match(/^(.+?)\s+download(?:\s+(.*))?$/i);if(s){let d=s[1].trim(),u=(s[2]??"").trim(),c=/--chat|-p/i.test(u)?"chat":"global",m=null,h=/^trending\s+(\d+)$/i.exec(d),f=/^search\s+(\S+)\s+(\d+)$/i.exec(d);if(h){if(m=qs(t,o.listScope,h[1]),!m)return p(`No trending list in this chat \u2014 run ${Dt(o,"trending")} first, or use ${Dt(o,"<publicId> download")}.`)}else if(f){if(m=qs(t,o.listScope,f[2]),!m)return p(`No search list in this chat \u2014 run ${Dt(o,"search <query>")} first, or use ${Dt(o,"<publicId> download")}.`)}else if(/^\d+$/.test(d)){if(m=qs(t,o.listScope,d),!m)return p(`No list item #${d}. Run trending or search first, or use ${Dt(o,"<publicId> download")}.`)}else m=d;let g=await hm(m);if(Tn(g))return p(`Download failed: ${g.error}`);if(o.defaultKind&&g.kind!==o.defaultKind)return p(`Entry "${m}" is kind=${g.kind}, not ${o.defaultKind}. Use /run online show ${m} from /run.`);let y=ym(g,t,n,c);return y.ok?p(y.message):p(`Download failed: ${y.error}`)}let i=/^show\s+(\S+)\s*$/i.exec(r);if(i){let d=await mm(i[1]);return Tn(d)?p(`Not found: ${d.error}`):o.defaultKind&&d.kind!==o.defaultKind?p(`Entry "${i[1]}" is kind=${d.kind}, not ${o.defaultKind}. Use /run online show ${i[1]}.`):Sm(d,o)}let a=/^trending(?:\s+(\S+))?\s*$/i.exec(r);if(a){let d=qb(o,a[1]);if(typeof d=="object"&&"error"in d)return p(d.error);let u=await dm(d?{kind:d}:void 0);if(Tn(u))return p(`Trending failed: ${u.error}`);gl(t,o.listScope,u.items);let c=o.defaultKind?`Trending (${o.defaultKind})`:"Trending";return zs(u.items,c,o)}let l=/^search\s+([\s\S]+)$/i.exec(r);if(l){let d=l[1].trim().split(/\s+/),u=Jb(o,d);if("error"in u)return p(u.error);let{kind:c,query:m}=u;if(!m)return p(`Usage: ${Dt(o,"search <query>")}`);let h=await pm(m,c?{kind:c}:void 0);return Tn(h)?p(`Search failed: ${h.error}`):(gl(t,o.listScope,h.items),zs(h.items,`Search: ${m}`,o))}if(/^list\s*$/i.test(r)){let d=km(t,o.listScope);return d?.length?zs(d,"Last list",o):p(`No cached list. ${Dt(o,"trending")} or ${Dt(o,"search <query>")}`)}return p(`Unknown ${o.commandPrefix} online command. ${Dt(o,"help")}`)}async function Tm(e,t,n){return po(e,t,n,jb)}function Vo(e){let t=e.trim(),n=[],o,r,s,i=a=>{let l=t.match(a);if(!l?.[1])return;let d=l[1].trim();if(t=(t.slice(0,l.index)+t.slice(l.index+l[0].length)).trim(),a.source.includes("title"))o=d.slice(0,120);else if(a.source.includes("description"))r=d.slice(0,500);else if(a.source.includes("category"))s=d.slice(0,40);else if(a.source.includes("tag"))for(let u of d.split(/[,;]/)){let c=u.trim();c&&n.push(c.slice(0,32))}};for(let a=0;a<8;a+=1){let l=t;if(i(/--title\s+"([^"]+)"/i),i(/--title\s+(\S+)/i),i(/--description\s+"([^"]+)"/i),i(/--description\s+(\S+)/i),i(/--category\s+"([^"]+)"/i),i(/--category\s+(\S+)/i),i(/--tag\s+"([^"]+)"/i),i(/--tag\s+(\S+)/i),t===l)break}return{title:o,description:r,category:s,tags:n,remainder:t.trim()}}function zb(e,t,n){return{kind:"recipe",name:e,title:n.title??t.label??e,description:n.description??t.description,category:n.category??t.category,tags:n.tags,payload:bm(t)}}function $m(e,t,n,o){let r=Vo(o),s=n.trim().toLowerCase(),i=Ye(e,t,s);return i?i.source==="builtin"?{ok:!1,error:"Built-in recipes cannot be published. Copy with /run add first."}:{ok:!0,body:zb(i.name,i,r)}:{ok:!1,error:`Unknown recipe "${n}".`}}function Mm(e,t,n){let o=Vo(n),r=Hs(e,t);return r?{ok:!0,body:{kind:"shortcut",name:r.name,title:o.title??r.name,description:o.description,category:o.category??"shortcut",tags:o.tags,payload:{body:r.body}}}:{ok:!1,error:`Unknown shortcut "${t}".`}}function Pm(e,t,n){let o=Vo(n),r=Pe(),s=Ze(r,t,e);return s?{ok:!0,body:{kind:"cowork",name:s.name,title:o.title??s.name,description:o.description,category:o.category??"cowork",tags:o.tags,payload:wm(s)}}:{ok:!1,error:`Unknown cowork task "${t}" for this chat.`}}function Em(e,t,n){let o=Vo(n),r=e.trim().toLowerCase();return t.trim()?{ok:!0,body:{kind:"app",name:r,title:o.title??r,description:o.description,category:o.category??"app",tags:o.tags,payload:{command:t.trim(),label:o.title}}}:{ok:!1,error:"Session has no command to publish."}}async function mo(e){let t=ml();if(!t.ok)return{ok:!1,error:t.error};let n=await fm(e);if(Tn(n))return{ok:!1,error:n.error};let o=n;return{ok:!0,message:`${o.updated?"Updated":"Published"}: ${o.publicId} (${e.kind}). Others: /run online ${o.publicId} download`}}function Kb(){return p(["Cowork \u2014 scheduled shell tasks (gateway must be running).","","add <name> <schedule> -- <command\u2026>"," schedule: ondemand | hourly [:MM] | daily HH:MM | weekdays HH:MM | weekly <mon\u2026sun> HH:MM","add <name> heartbeat <interval> [grace] \u2014 dead-man's-switch (no command needed)"," e.g. /cowork add backup heartbeat 1h 10m","set <name> cmd -- <command\u2026> | schedule <\u2026> | out <path> | cwd <path>"," notify self|wa|tg|all|none"," when always|failure|state-change \u2014 notify condition (default: always)"," attach on|off \u2014 send run log as a file with notify messages"," files clear | <glob/path \u2026> \u2014 optional artifacts (basename * ?); quote paths with spaces","list | show <name> | run <name> | checkin <name> | enable <name> | disable <name> | remove <name>","publish <name> \u2014 share task to online catalog (platform login)","online trending | show <publicId> | <publicId> download \u2014 cowork tasks only","","Output logs: task outputDir (default ~/Cowork/<name>). Missed times catch up when the gateway is back."].join(`
310
- `))}function Am(e){let t=[],n=0;for(;n<e.length;){for(;n<e.length&&/\s/.test(e[n]);)n+=1;if(n>=e.length)break;if(e[n]==='"'){let r=n+1,s=e.indexOf('"',r);if(s===-1){t.push(e.slice(r));break}t.push(e.slice(r,s)),n=s+1;continue}let o=n;for(;o<e.length&&!/\s/.test(e[o]);)o+=1;t.push(e.slice(n,o)),n=o}return t}function Yb(e){let t=" -- ",n=e.indexOf(t);if(n===-1)return{error:'Missing " -- " before the command. Example: /cowork add tick weekdays 09:00 -- date'};let o=e.slice(0,n).trim(),r=e.slice(n+t.length).trim();if(!r)return{error:"Command after -- is empty."};let s=o.split(/\s+/).filter(Boolean);if(s.length<2)return{error:"Usage: /cowork add <name> <schedule\u2026> -- <command\u2026>"};let i=s[0],a=s.slice(1);return{name:i,scheduleWords:a,command:r}}async function Lm(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return Kb();let r=/^online\b([\s\S]*)$/i.exec(o);if(r)return po((r[1]??"").trim(),t,n,Cm);let s=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(o);if(s){let h=Pm(t,s[1],s[2]??"");if(!h.ok)return p(h.error);let f=await mo(h.body);return p(f.ok?f.message:f.error)}let i=o.split(/\s+/)[0].toLowerCase();if(/^list$/i.test(i)){let h=Pe().filter(g=>g.ownerPeerKey===t);if(h.length===0)return p("(no cowork tasks for this chat)");let f=h.map(g=>{let y=g.enabled?"":" (disabled)",b=g.notifyWhen&&g.notifyWhen!=="always"?` when=${g.notifyWhen}`:"";return`${Ae}${g.name} ${Kn(g.schedule)} notify=${g.notify}${b}${y}`});return p(f.join(`
311
- `))}let a=o.match(/^show\s+(\S+)\s*$/i);if(a){let h=a[1],f=Pe();bn(f);let g=Ze(f,h,t);if(!g)return p(`Unknown task "${h}". /cowork list`);let y=is(g),b=[`name: ${g.name}`,`id: ${g.id}`,`schedule: ${Kn(g.schedule)}`,`enabled: ${g.enabled}`,`notify: ${g.notify}`,`notifyWhen: ${g.notifyWhen??"always"}`];if(g.schedule.kind==="heartbeat"){let k=ls(g.id);b.push(`lastCheckin: ${k?new Date(k).toLocaleString():"(never \u2014 send /cowork checkin "+g.name+")"}`)}else b.push(`attachLog: ${g.attachLog}`),b.push(`files: ${g.attachFiles.length?g.attachFiles.join(", "):"(none)"}`),b.push(`cwd: ${g.cwd||"(session cwd)"}`),b.push(`out: ${g.outputDir}`),b.push(`cmd: ${g.command}`),b.push(`last slot: ${y?new Date(y).toLocaleString():"(never)"}`);return p(b.join(`
312
- `))}if(/^add$/i.test(i)){let h=o.slice(3).trim(),f=h.match(/^(\S+)\s+(heartbeat\s+.+)$/i);if(f){let $=vi(f[1]);if(!$.ok)return p($.error);let N=f[2].split(/\s+/).filter(Boolean),A=ss(N);if(!A.ok)return p(A.error);let z=Pe();if(Ze(z,$.name,t))return p(`Task "${$.name}" already exists. Remove it first or pick another name.`);let Z=wr($.name),ce={id:ko(),name:$.name,ownerPeerKey:t,command:"",cwd:"",outputDir:Z,schedule:A.schedule,enabled:!0,notify:"self",notifyWhen:"always",attachLog:!1,attachFiles:[],lastCompletedSlotMs:null,createdAtMs:Date.now()};return z.push(ce),We(z),bn(z),Ea(ce.id),p([`Saved heartbeat task "${$.name}" (${Kn(A.schedule)}).`,`Send /cowork checkin ${$.name} to record a heartbeat.`,"Alerts if no check-in arrives within the expected interval + grace period."].join(`
313
- `))}let g=Yb(h);if("error"in g)return p(g.error);let y=vi(g.name);if(!y.ok)return p(y.error);let b=ss(g.scheduleWords);if(!b.ok)return p(b.error);let k=Pe();if(Ze(k,y.name,t))return p(`Task "${y.name}" already exists. Remove it first or pick another name.`);let M=ie(t),R=wr(y.name),L={id:ko(),name:y.name,ownerPeerKey:t,command:g.command,cwd:"",outputDir:R,schedule:b.schedule,enabled:!0,notify:"self",notifyWhen:"always",attachLog:!1,attachFiles:[],lastCompletedSlotMs:null,createdAtMs:Date.now()};return k.push(L),We(k),p([`Saved cowork task "${y.name}" (${Kn(b.schedule)}).`,`Output: ${R}`,`Notify: self \u2014 change with /cowork set ${y.name} notify wa|tg|all|none`].join(`
314
- `))}let l=o.match(/^run\s+(\S+)\s*$/i);if(l){let h=l[1].toLowerCase(),f=Ze(Pe(),h,t);return f?f.enabled?(cc({ownerPeerKey:t,name:f.name,at:Date.now()}),p(`On-demand run queued for "${f.name}" (runs within ~30s while omnish run is active).`)):p(`Cowork "${f.name}" is disabled. /cowork enable ${f.name}`):p(`Unknown task "${l[1]}". /cowork list`)}let d=o.match(/^checkin\s+(\S+)\s*$/i);if(d){let h=d[1].toLowerCase(),f=Pe();bn(f);let g=Ze(f,h,t);return g?g.schedule.kind!=="heartbeat"?p(`"${g.name}" is not a heartbeat task.`):(Ea(g.id),p(`Heartbeat recorded for "${g.name}".`)):p(`Unknown task "${d[1]}". /cowork list`)}let u=o.match(/^(?:remove|rm|del)\s+(\S+)\s*$/i);if(u){let h=u[1],f=Pe(),g=f.findIndex(y=>y.name===h.toLowerCase()&&y.ownerPeerKey===t);return g===-1?p(`Unknown task "${h}".`):(f.splice(g,1),We(f),p(`Removed cowork task "${h.toLowerCase()}".`))}let c=o.match(/^enable\s+(\S+)\s*$/i);if(c)return Im(c[1],t,!0);let m=o.match(/^disable\s+(\S+)\s*$/i);return m?Im(m[1],t,!1):/^set$/i.test(i)?Zb(o.slice(3).trim(),t):p("Unknown /cowork command. Try /cowork help")}function Im(e,t,n){let o=Pe(),r=Ze(o,e,t);return r?(r.enabled=n,We(o),p(`Cowork "${r.name}" ${n?"enabled":"disabled"}.`)):p(`Unknown task "${e}".`)}function Qb(e){let t=e.toLowerCase();return t==="self"?"self":t==="wa"||t==="whatsapp"?"wa":t==="tg"||t==="telegram"?"tg":t==="all"?"all":t==="none"?"none":null}function Vb(e){let t=e.toLowerCase();return t==="always"||t==="all"?"always":t==="failure"||t==="fail"||t==="failures"?"failure":t==="state-change"||t==="statechange"||t==="change"||t==="transition"?"state-change":null}function Xb(e){let t=e.toLowerCase();return t==="on"||t==="true"||t==="1"||t==="yes"?!0:t==="off"||t==="false"||t==="0"||t==="no"?!1:null}function Zb(e,t){let n=e.match(/^(\S+)\s+([\s\S]+)$/);if(!n)return p("Usage: /cowork set <name> cmd -- \u2026 | schedule \u2026 | out <path> | cwd <path> | notify \u2026 | when \u2026 | attach <on|off> | files \u2026");let o=n[1],r=n[2].trim(),s=Pe(),i=Ze(s,o,t);if(!i)return p(`Unknown task "${o}".`);if(r.toLowerCase().startsWith("cmd ")){let a=r.slice(4).trim(),l=" -- ",d=a.indexOf(l),u;if(d!==-1)u=a.slice(d+l.length).trim();else if(a.startsWith("--"))u=a.slice(2).trim();else return p("Usage: /cowork set <name> cmd -- <command\u2026>");return u?(i.command=u,We(s),p(`Updated command for "${i.name}".`)):p("Command is empty.")}if(r.toLowerCase().startsWith("schedule ")){let a=r.slice(9).trim().split(/\s+/).filter(Boolean),l=ss(a);return l.ok?(i.schedule=l.schedule,We(s),p(`Schedule for "${i.name}": ${Kn(l.schedule)}`)):p(l.error)}if(r.toLowerCase().startsWith("out ")){let a=r.slice(4).trim(),l=ie(t);return i.outputDir=Qe(a,l.cwd),We(s),p(`outputDir: ${i.outputDir}`)}if(r.toLowerCase().startsWith("cwd ")){let a=r.slice(4).trim(),l=ie(t);return i.cwd=a?Qe(a,l.cwd):"",We(s),p(`cwd: ${i.cwd||"(session cwd at run time)"}`)}if(r.toLowerCase().startsWith("notify ")){let a=r.slice(7).trim(),l=Qb(a);return l?(i.notify=l,We(s),p(`notify: ${l}`)):p("notify must be self, wa, tg, all, or none.")}if(r.toLowerCase().startsWith("when ")){let a=r.slice(5).trim(),l=Vb(a);return l?(i.notifyWhen=l,We(s),p(`notifyWhen: ${l}`)):p("when must be always, failure, or state-change.")}if(r.toLowerCase().startsWith("attach ")){let a=r.slice(7).trim(),l=Am(a);if(l.length!==1)return p("Usage: /cowork set <name> attach on|off");let d=Xb(l[0]);return d===null?p("attach must be on or off"):(i.attachLog=d,We(s),p(`attachLog: ${d}`))}if(r.toLowerCase().startsWith("files ")){let a=r.slice(6).trim(),l=Am(a);return l.length===1&&l[0].toLowerCase()==="clear"?(i.attachFiles=[],We(s),p("files: (cleared)")):l.length===0?p("Usage: /cowork set <name> files clear | <pattern \u2026> \u2014 quote paths with spaces"):(i.attachFiles=l,We(s),p(`files: ${i.attachFiles.join(", ")}`))}return p("Unknown set field. Try: cmd, schedule, out, cwd, notify, when, attach, files")}pe();import kk from"node:fs";G();import Om from"node:os";import ek from"node:path";var tk=new Set(["create","delete","rename","update"]);function nk(e){let t=[],n=0;for(;n<e.length;){for(;n<e.length&&/\s/.test(e[n]);)n+=1;if(n>=e.length)break;if(e[n]==='"'){let r=n+1,s=e.indexOf('"',r);if(s===-1){t.push(e.slice(r));break}t.push(e.slice(r,s)),n=s+1;continue}let o=n;for(;o<e.length&&!/\s/.test(e[o]);)o+=1;t.push(e.slice(n,o)),n=o}return t}function ok(e){let t=e.split(",").map(n=>n.trim().toLowerCase());return t.length===0?!1:t.every(n=>tk.has(n))}function rk(e){return e.split(",").map(t=>t.trim().toLowerCase())}function wl(e,t,n){let o=e.trim();if(o.startsWith("-")&&(o=o.slice(1)),!o)return{};let r=Qe(o,n);return o.includes("*")||o.includes("?")?{glob:o.replace(/\\/g,"/")}:ek.isAbsolute(r)||o.startsWith("~")||o.startsWith("./")||o.startsWith("../")?{path:r}:o.startsWith("/")?{path:r}:{glob:o.includes("/")?o:`**/${o}`}}function Nm(e,t=Om.homedir()){let n=e.replace(/\s+&&\s+/g," ").trim(),o=nk(n);if(o.length===0)return{ok:!1,error:"Usage: /watch add fs <name> <path> [events] [-exclude \u2026] [--exclude \u2026]"};let r=0,s=o[r];r+=1;let i=["create","delete","rename"],a=[],l=[];for(;r<o.length;){let u=o[r];if(u==="--exclude"){if(r+=1,r>=o.length)return{ok:!1,error:"--exclude requires a pattern."};let c=o[r];r+=1;let m=Qe(s,t),h=wl(c.startsWith("-")?c:`-${c}`,m,t);h.path?a.push(h.path):h.glob&&l.push(h.glob);continue}if(u.startsWith("--"))return{ok:!1,error:`Unknown flag: ${u}`};if(u.startsWith("-")){let c=Qe(s,t),m=wl(u,c,t);m.path?a.push(m.path):m.glob&&l.push(m.glob),r+=1;continue}if(ok(u)){i=rk(u),r+=1;continue}return{ok:!1,error:`Unexpected token "${u}". Put path first, then events or -excludes.`}}return{ok:!0,value:{rootPath:Qe(s,t),events:i,excludePaths:a,excludeGlobs:l}}}function bl(e,t,n=Om.homedir()){let o=wl(e.startsWith("-")?e:`-${e}`,t,n);return!o.path&&!o.glob?{error:"Empty exclude pattern."}:o}import{spawnSync as Ks}from"node:child_process";import Fm from"node:fs";import sk from"node:os";import ik from"node:path";var ak=40,lk=10,Ys=15e3,ck=["~/Projects","~/deploy","~/Downloads","~/src","/var/www","/srv"],_m={linux:"/var/log/dpkg.log (also /var/log/apt/history.log)",darwin:"/var/log/install.log",win32:"Windows Application event log (install/remove)"};function uk(e){return e.startsWith("~/")?ik.join(sk.homedir(),e.slice(2)):e}function dk(e,t){return t?.trim()?e.toLowerCase().includes(t.trim().toLowerCase()):!0}function pk(e){let t=n=>n==="running"||n==="active";return[...e].sort((n,o)=>{let r=t(n.state),s=t(o.state);return r!==s?r?-1:1:n.name.localeCompare(o.name)})}function mk(e){let t=[];for(let n of e.split(`
315
- `)){let o=n.trim();if(!o)continue;let r=o.split(/\s+/);if(r.length<4)continue;let s=r[0];if(!s.endsWith(".service"))continue;let i=r[2].toLowerCase(),a=s.slice(0,-8);t.push({name:a,state:i})}return t}function hk(e){let t=[],n=e.split(`
316
- `);for(let o=0;o<n.length;o++){let r=n[o].trim();if(!r||r.startsWith("PID"))continue;let s=r.split(/\s+/);if(s.length<3)continue;let i=s[s.length-1];if(!i.includes("."))continue;let l=s[0]==="-"?"stopped":"running";t.push({name:i,state:l})}return t}function fk(e){let t=[],n="";for(let o of e.split(`
317
- `)){let r=o.match(/SERVICE_NAME:\s*(.+)/i);if(r){n=r[1].trim();continue}let s=o.match(/^\s*STATE\s*:\s*\d+\s+(\S+)/i);if(s&&n){let i=s[1].toUpperCase(),a=i.toLowerCase();i==="RUNNING"?a="running":i==="STOPPED"&&(a="stopped"),t.push({name:n,state:a}),n=""}}return t}function gk(){let e=Ks("systemctl",["list-units","--type=service","--all","--no-pager","--plain","--no-legend"],{encoding:"utf8",timeout:Ys});return e.error||e.status!==0?{entries:[],error:"systemctl unavailable \u2014 install systemd or run on Linux."}:{entries:mk(e.stdout??"")}}function yk(){let e=Ks("launchctl",["list"],{encoding:"utf8",timeout:Ys});return e.error||e.status!==0?{entries:[],error:"launchctl unavailable."}:{entries:hk(e.stdout??"")}}function wk(){let e=Ks("sc",["query","type=","service","state=","all"],{encoding:"utf8",timeout:Ys,windowsHide:!0});if(!e.error&&e.status===0&&(e.stdout??"").includes("SERVICE_NAME"))return{entries:fk(e.stdout??"")};let t=Ks("powershell",["-NoProfile","-Command","Get-Service | ForEach-Object { $_.Name + ' ' + $_.Status }"],{encoding:"utf8",timeout:Ys,windowsHide:!0});if(t.error||t.status!==0)return{entries:[],error:"sc query and Get-Service unavailable."};let n=[];for(let o of(t.stdout??"").split(`
318
- `)){let r=o.trim();if(!r)continue;let s=r.lastIndexOf(" ");if(s<=0)continue;let i=r.slice(0,s).trim(),a=r.slice(s+1).trim().toLowerCase();n.push({name:i,state:a})}return{entries:n}}function Qs(e){let t=e?.limit??ak,n=e?.filter,o,r=process.platform;if(r==="linux")o=gk();else if(r==="darwin")o=yk();else if(r==="win32")o=wk();else return{entries:[],truncated:!1,totalMatched:0,error:`Service discovery not supported on ${r}.`};if(o.error)return{entries:[],truncated:!1,totalMatched:0,error:o.error};let s=pk(o.entries.filter(l=>dk(l.name,n))),i=s.length,a=i>t;return{entries:s.slice(0,t),truncated:a,totalMatched:i}}function bk(e,t=lk){let n=[];for(let o=0;o<e.length;o+=t)n.push(e.slice(o,o+t));return n}function kl(e,t){if(e.error)return p(e.error);if(e.entries.length===0){let r=t?.trim()?`No services match "${t}". Try /watch svc list without a filter.`:"No services found on this host.";return p(r)}let n=e.entries.map(r=>`${Ae}${r.name} \u2014 ${r.state}`),o=t?.trim()?`Services matching "${t}" (${e.entries.length} shown):`:`Services on ${process.platform} (${e.entries.length} shown):`;if(n.unshift(o),e.truncated){let r=e.totalMatched-e.entries.length;n.push("",`${r} more \u2014 narrow with /watch svc list <filter>`)}return n.push("","Copy-paste templates follow in the next message."),p(n.join(`
319
- `))}function vl(e,t){let n=t?.trim()||"<name>";if(e.length===0)return p("No services to template \u2014 run /watch svc list first.");let o=e.map(i=>i.name),r=bk(o),s=[t?.trim()?`Copy-paste (/watch add svc ${n} \u2026):`:"Copy-paste (replace <name>):",""];for(let i of r)s.push(`/watch add svc ${n} ${i.join(" ")}`);return p(s.join(`
320
- `))}function Wm(){let e=process.platform,t=e==="linux"?"linux":e==="darwin"?"darwin":"win32",n=["Watch hints",""];n.push("Package log (pkg watches):"),n.push(` ${_m[t]??_m.linux}`),n.push("","Filesystem roots that exist on this host:");let o=[];for(let r of ck){let s=uk(r);try{Fm.existsSync(s)&&Fm.statSync(s).isDirectory()&&o.push(r)}catch{}}if(o.length===0)n.push(" (none of the usual paths found \u2014 use any directory you own)");else for(let r of o)n.push(` ${r}`),n.push(` /watch add fs <name> ${r} create,delete,rename`);return n.push("","Service names: /watch svc list [filter]"),p(n.join(`
321
- `))}function U(e){return{replies:[e]}}function vk(...e){return{replies:e}}function Sk(e){let t=e.trim();if(!t)return{};let n=t.split(/\s+/);if(n.length>=2){let s=vr(n[0]);return s.ok?{ruleName:s.name,filter:n.slice(1).join(" ")}:{filter:t}}let o=n[0],r=vr(o);if(r.ok){let s=Qs({filter:o,limit:1});if(s.totalMatched===0&&!s.error)return{ruleName:r.name}}return{filter:o}}function xk(){return p(["Watch \u2014 OS event eye (watchEnabled + omnish run). Docs: docs/features/watch.md","","add fs <name> <path> [events] [-/exclude \u2026] [--exclude glob]",' e.g. /watch add fs home ~/Projects create,delete -~/Projects/tmp --exclude "**/.keyfolder"',"add pkg <name> | add svc <name> <unit\u2026>","svc list [filter] \u2014 services on this host (+ templates in next message)","svc templates [ruleName] [filter] \u2014 copy-paste /watch add svc lines only","hints \u2014 suggested FS paths and package log location","list | status | reload | show <name> | recent [N]","pause|stop <name> \u2014 stop alerts, keep rule","resume <name> | enable <name> | disable <name> | rm <name>","exclude <name> list|add <pattern>|rm <pattern>","set <name> notify self|wa|tg|all|none","set <name> when always|state-change","on | off \u2014 global watchEnabled in config","Rules are device-wide (~/.omnish/watch/rules.json); any allowlisted peer can manage.","notify:self alerts the peer who created the rule."].join(`
322
- `))}function Dm(){return{excludePaths:[],excludeGlobs:[]}}function Ck(e){return e.notify==="self"?`notify=self (${e.ownerPeerKey})`:`notify=${e.notify}`}function Rk(e,t){Ti(e.id),t==="pause"||t==="stop"?e.paused=!0:t==="resume"?e.paused=!1:t==="enable"?(e.enabled=!0,e.paused=!1):t==="disable"&&(e.enabled=!1,e.paused=!1)}function Um(e,t){let n=e.trim();if(!n||/^help$/i.test(n))return U(xk());let o=n.split(/\s+/)[0].toLowerCase();if(br(),/^hints$/i.test(o))return U(Wm());if(/^svc$/i.test(o)){let u=n.slice(3).trim(),m=(u.split(/\s+/)[0]??"").toLowerCase();if(m==="list"){let h=u.slice(4).trim()||void 0,f=Qs({filter:h});return f.error?U(p(f.error)):f.entries.length===0?U(kl(f,h)):vk(kl(f,h),vl(f.entries))}if(m==="templates"){let h=u.slice(9).trim(),{ruleName:f,filter:g}=Sk(h),y=Qs({filter:g});return y.error?U(p(y.error)):U(vl(y.entries,f))}return U(p("svc subcommands: list [filter] | templates [ruleName] [filter]"))}if(/^status$/i.test(o)){let u=S(),{summary:c}=So(),m=[`watchEnabled: ${u.watchEnabled} watchAutoRestore: ${u.watchAutoRestore}`,`debounce: ${u.watchDebounceMs}ms max/min: ${u.watchMaxEventsPerMinute}`,`rules file: ${Ci()} (${c.total} saved, ${c.active} active, ${c.paused} paused, ${c.disabled} disabled)`,`events db: ${cr}`];c.total>0&&!u.watchEnabled?m.push("Rules are on disk; adapters stopped \u2014 /watch on to start."):c.total>0&&u.watchEnabled&&!u.watchAutoRestore?m.push("watchAutoRestore is off \u2014 /watch reload to start adapters without restarting gateway."):c.paused>0&&m.push(`${c.paused} paused rule(s) stay stopped until /watch resume <name>.`);let h=Wc();return h?(m.push("","Adapters:"),m.push(...h.getStatusLines())):u.watchEnabled&&u.watchAutoRestore?m.push("","(manager starting \u2014 try /watch status again)"):u.watchEnabled?m.push("","(manager idle \u2014 /watch reload)"):m.push("","(manager off \u2014 /watch on)"),U(p(m.join(`
323
- `)))}if(/^reload$/i.test(o)){let{summary:u}=So();return S().watchEnabled?(Dc(),U(p(`Reloading from ${Ci()}: ${u.total} rule(s) on disk, ${u.active} eligible to run. /watch status for adapters.`))):U(p(`Rules on disk: ${u.total} (${u.active} active). watchEnabled is false \u2014 /watch on first.`))}if(/^list$/i.test(o)){let u=De();if(u.length===0)return U(p("(no watch rules on this device)"));let c=u.map(m=>{let h=m.enabled?m.paused?"paused":"on":"disabled",f=m.kind==="fs"?m.path:m.kind==="svc"?m.units.join(","):"system";return`${Ae}${m.name} [${m.kind}] ${h} ${Ck(m)} when=${m.notifyWhen} \u2014 ${f}`});return U(p(c.join(`
324
- `)))}let r=n.match(/^recent(?:\s+(\d+))?\s*$/i);if(r){let u=r[1]?Number(r[1]):15,c=De(),m=new Set(c.map(g=>g.id)),h=fc(u,m);if(h.length===0)return U(p("(no recent watch events)"));let f=h.map(g=>`${new Date(g.tsMs).toLocaleString()} ${g.ruleName} ${g.summary}`);return U(p(f.join(`
325
- `)))}let s=n.match(/^show\s+(\S+)\s*$/i);if(s){let u=un(De(),s[1]);if(!u)return U(p(`Unknown rule "${s[1]}". /watch list`));let c=[`name: ${u.name}`,`kind: ${u.kind}`,`creator: ${u.ownerPeerKey}`,`enabled: ${u.enabled}`,`paused: ${u.paused}`,`notify: ${u.notify}`,`notifyWhen: ${u.notifyWhen}`,`adapter: ${u.adapterStatus||"(unknown)"}`];return u.kind==="fs"&&(c.push(`path: ${u.path}`),c.push(`events: ${u.events.join(",")}`),c.push(`excludePaths: ${u.excludePaths.length?u.excludePaths.join(", "):"(none)"}`),c.push(`excludeGlobs: ${u.excludeGlobs.length?u.excludeGlobs.join(", "):"(none)"}`)),u.kind==="svc"&&c.push(`units: ${u.units.join(", ")||"(none)"}`),U(p(c.join(`
326
- `)))}if(/^on$/i.test(o))return O({watchEnabled:!0}),qe(),U(p("watchEnabled: true (gateway picks up watchers when running)."));if(/^off$/i.test(o))return O({watchEnabled:!1}),qe(),U(p("watchEnabled: false \u2014 all adapters stopped."));let i=n.match(/^exclude\s+(\S+)\s+(\S+)(?:\s+([\s\S]+))?\s*$/i);if(i){let u=i[1],c=i[2].toLowerCase(),m=(i[3]??"").trim(),h=De(),f=un(h,u);if(!f)return U(p(`Unknown rule "${u}".`));if(f.kind!=="fs")return U(p("exclude applies to filesystem rules only."));if(c==="list"){let g=[`excludePaths: ${f.excludePaths.length?f.excludePaths.join(`
308
+ ${s.detail}`)}if(o==="uninstall"){if(!e.serviceInstallFromChat)return p("Uninstall from chat is disabled. Enable with `/config set serviceInstallFromChat true` or remove files on the host manually.");let s=as();return p(`*Uninstall*
309
+ ${s.detail}`)}return p("Unknown /service command. Try /service help")}function ai(e,t){let n=e.trim(),o=`/${t}`;return n===o||n.startsWith(`${o} `)?n.slice(o.length).trim():null}var Gm=e=>ai(e,"dl"),Jm=e=>ai(e,"tr"),qm=e=>ai(e,"edit"),zm=e=>ai(e,"pull");function Ll(e){let t=e.trim(),n=!1,o=!0;for(;o;){if(o=!1,/^--bg\b/i.test(t)||/^--background\b/i.test(t)){t=t.replace(/^--(?:bg|background)\s*/i,"").trim(),o=!0;continue}(/^--notify\b/i.test(t)||/^-N\b/i.test(t))&&(n=!0,t=t.replace(/^--notify\s*/i,"").replace(/^-N\s*/i,"").trim(),o=!0)}return{body:t,notify:n}}function Vk(e,t,n,o){let r=Wt();if(r.error)throw new Error(r.error);let s=i=>JSON.stringify(i);return`OMNISH_PEER_KEY=${s(o)} ${r.nodePath} ${r.scriptPath} media-exec ${e} ${s(t)} ${s(n)}`}function Xk(e,t,n){return Kr({peerKey:t,enabled:e.progressUpdates,sendToPeer:n?.sendToPeer})}function Ol(e,t,n){let o=ae(n.peerKey).cwd,r=Vk(n.sub,n.body,o,n.peerKey),{id:s,meta:i}=t.spawnJob(e.shell,r,{cwd:o,name:n.jobName,notifyPeerKey:n.notify?n.peerKey:null}),a=n.notify?`
310
+ Notify on completion: on`:"";return{kind:"text",body:p(`${n.label} job ${s} started.
311
+ /log ${i.name??s}
312
+ /tail ${i.name??s}${a}`)}}async function Zk(e,t,n,o){if(!e.mediaInstallFromChat)return{kind:"text",body:p("Install from chat is off. Host: omnish pull install \xB7 or /config set mediaInstallFromChat true")};let r=/\b--whisper\b/i.test(t),s=Xk(e,n,o);try{let i=await Xr({whisper:r,progress:{stepStart:(a,l,c)=>s.stepStart(a,l,{label:c})}});return{kind:"text",body:p(`*Install*
313
+
314
+ ${i.messages.join(`
315
+ `)}`)}}catch(i){return{kind:"text",body:p(`Install failed: ${String(i)}`)}}}async function Nn(e,t,n,o,r){let s=t.trim(),i=(s.split(/\s+/)[0]??"").toLowerCase();if(!s||i==="help")return{kind:"text",body:ee(oa(e))};if(i==="doctor"){let{text:c}=Vr(e);return{kind:"text",body:p(`*Media tools*
316
+
317
+ ${c}`)}}if(i==="setup"||i==="instructions")return{kind:"text",body:p(zr())};if(i==="install")return Zk(e,s,n,r);let{body:a,notify:l}=Ll(s);return a?Ol(e,o,{sub:"dl",body:a,peerKey:n,jobName:"dl",label:"Download",notify:l}):{kind:"text",body:ee(oa(e))}}async function Nl(e,t,n,o,r){let s=t.trim(),i=(s.split(/\s+/)[0]??"").toLowerCase();if(!s||i==="help")return{kind:"text",body:p(`*Transcribe (/tr)*
318
+
319
+ /tr <url|path>
320
+
321
+ URL: downloads video, transcribes, sends text + .srt + video.
322
+ Path: transcribes local file, sends text + .srt.
323
+
324
+ Runs in background. Use /tr --notify for a completion ping when the shell job ends.`)};if(i==="doctor"||i==="setup"||i==="install")return Nn(e,s,n,o,r);let{body:a,notify:l}=Ll(s);return a?dt(e).whisper?Ol(e,o,{sub:"tr",body:a,peerKey:n,jobName:"tr",label:"Transcribe",notify:l}):{kind:"text",body:p("whisper missing. Run: omnish pull install --whisper")}:{kind:"text",body:p("Usage: /tr <url|filepath>")}}async function Km(e,t,n,o,r){let s=t.trim(),i=(s.split(/\s+/)[0]??"").toLowerCase();if(!s||i==="help")return{kind:"text",body:p(["*Edit (/edit)*","","/edit <url|path> [--from 1:30] [--to 2:00] [-t 30]","/edit clip.mp4 --format mp3 --audio-only","","Flags: --from/--start, --to/--end, -t/--duration, --format/-f, --audio-only","","Runs in background. Use /edit --notify for a completion ping when the job ends."].join(`
325
+ `))};if(i==="doctor"||i==="setup"||i==="install")return Nn(e,s,n,o,r);let{body:a,notify:l}=Ll(s);return a?Ol(e,o,{sub:"edit",body:a,peerKey:n,jobName:"edit",label:"Edit",notify:l}):{kind:"text",body:p("Usage: /edit <url|path> [flags]")}}async function Ym(e,t,n,o,r){let s=t.trim(),i=(s.split(/\s+/)[0]??"").toLowerCase(),a=s.slice(i.length).trim();return i==="video"||i==="audio"||i==="all"||!i&&Ho(s)?Nn(e,a||s,n,o,r):i==="transcript"||i==="subs"?Nl(e,a,n,o,r):i==="doctor"||i==="setup"||i==="install"||i==="help"||!s?Nn(e,s,n,o,r):{kind:"text",body:p(`/pull is deprecated. Use:
326
+ /dl <url> \u2014 download video
327
+ /tr <url|path> \u2014 transcribe
328
+ /edit <url|path> \u2014 trim/convert
329
+
330
+ /dl help for full usage.`)}}async function Qm(e,t,n,o,r){return!e.mediaUrlAutoDl||!Ho(t)?null:Nn(e,t.trim(),n,o,r)}pe();Ve();kn();function li(){let e=re();return e?.platformUrl?e.platformUrl.replace(/\/$/,""):(S().tunnelRelayUrl||Ee).trim().replace(/\/$/,"")}function ci(){let e=re();return e?.token?{Authorization:`Bearer ${e.token}`}:{}}async function lr(e){try{return await e.json()}catch{return{error:`HTTP ${e.status}`}}}function ev(){return`Set platform URL: omnish config add tunnelRelayUrl ${Ee} \u2014 publish: omnish platform login`}function Fl(){return re()?.token?{ok:!0}:{ok:!1,error:`Publish requires a platform account token. ${ev()}`}}async function Vm(e){let t=new URLSearchParams;e?.kind&&t.set("kind",e.kind),e?.limit&&t.set("limit",String(e.limit));let n=t.toString(),o=`${li()}/v1/catalog/trending${n?`?${n}`:""}`,r=await fetch(o,{headers:ci()}),s=await lr(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}async function Xm(e,t){let n=new URLSearchParams;n.set("q",e),t?.kind&&n.set("kind",t.kind),t?.category&&n.set("category",t.category),t?.limit&&n.set("limit",String(t.limit));let o=`${li()}/v1/catalog/search?${n}`,r=await fetch(o,{headers:ci()}),s=await lr(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}async function Zm(e){let t=encodeURIComponent(e.trim()),n=`${li()}/v1/catalog/${t}`,o=await fetch(n,{headers:ci()}),r=await lr(o);return o.ok?r:{error:r.error??`HTTP ${o.status}`}}async function eh(e){let t=encodeURIComponent(e.trim()),n=`${li()}/v1/catalog/${t}/download`,o=await fetch(n,{method:"POST",headers:ci()}),r=await lr(o);return o.ok?r:{error:r.error??`HTTP ${o.status}`}}async function th(e){let t=Fl();if(!t.ok)return{error:t.error};let n=re(),o=`${n.platformUrl.replace(/\/$/,"")}/v1/catalog`,r=await fetch(o,{method:"POST",headers:{"content-type":"application/json",Authorization:`Bearer ${n.token}`},body:JSON.stringify(e)}),s=await lr(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}function nh(e){return e==="global"?"global":"chat"}function oh(e,t,n,o="global"){switch(e.kind){case"recipe":{let r=e.payload,s=Ke({...r,dangerous:void 0}),i=Tn(s);if(!i.ok)return{ok:!1,error:i.error};let a=Ct(e.name);if(!a.ok)return{ok:!1,error:a.error};try{lo(t,a.normalized,s,nh(o))}catch(c){return{ok:!1,error:String(c.message??c)}}let l=o==="global"?`/run ${a.normalized} <task>`:`/run ${a.normalized} <task> (this chat)`;return{ok:!0,message:`Installed recipe "${a.normalized}" (${o}). ${l}`}}case"app":{let r=e.payload;if(!r.command?.trim())return{ok:!1,error:"App payload has no command."};let s=Ke({command:r.command.trim(),label:r.label??e.title,description:r.description??e.description,category:e.category||"app"}),i=Ct(e.name);if(!i.ok)return{ok:!1,error:i.error};try{lo(t,i.normalized,s,nh(o),{skipCommandValidation:!0})}catch(a){return{ok:!1,error:String(a.message??a)}}return{ok:!0,message:`Installed app template "${i.normalized}" (${o}). /apps start <session> ${r.command.trim()}`}}case"shortcut":{let r=e.payload,s=Tt(e.name);if(!s.ok)return{ok:!1,error:s.error};try{ar(t,s.normalized,r.body,o==="global"?"global":"chat")}catch(i){return{ok:!1,error:String(i.message??i)}}return{ok:!0,message:`Installed shortcut "${s.normalized}" (${o}). Type ${s.normalized} to expand.`}}case"cowork":{let r=e.payload,s=Pe();if(et(s,e.name,t))return{ok:!1,error:`Cowork task "${e.name}" already exists for this chat. Remove it first or rename.`};let i=Date.now(),a={id:Ao(),name:e.name,ownerPeerKey:t,command:r.command,cwd:r.cwd??"",outputDir:r.outputDir??"",schedule:r.schedule,enabled:r.enabled??!0,notify:r.notify??"self",notifyWhen:r.notifyWhen??"always",attachLog:r.attachLog??!1,attachFiles:r.attachFiles??[],lastCompletedSlotMs:null,createdAtMs:i};return s.push(a),De(s),{ok:!0,message:`Installed cowork task "${e.name}" for this chat. /cowork show ${e.name}`}}default:return{ok:!1,error:"Unknown catalog kind."}}}function rh(e){return{name:e.name,command:e.command,cwd:e.cwd,outputDir:e.outputDir,schedule:e.schedule,enabled:e.enabled,notify:e.notify,notifyWhen:e.notifyWhen,attachLog:e.attachLog,attachFiles:e.attachFiles}}function sh(e){let{dangerous:t,...n}=e;return Ke(n)}var Wl=new Map;function _l(e,t){return`${e}:${t}`}function Dl(e,t,n){Wl.set(_l(e,t),n)}function ih(e,t){return Wl.get(_l(e,t))}function ui(e,t,n){let o=Number.parseInt(n,10);if(!Number.isFinite(o)||o<1)return null;let r=Wl.get(_l(e,t));return!r||o>r.length?null:r[o-1].publicId}function Fn(e){return!!(e&&typeof e=="object"&&typeof e.error=="string")}function di(e,t){return`${e.commandPrefix} online ${t}`}function ah(e){let t=r=>di(e,r),n=e.defaultKind===void 0?" [recipe|app|cowork|shortcut]":` (${e.defaultKind} only)`,o=[`Online catalog \u2014 ${e.defaultKind??"all kinds"}`,"",t(`trending${e.defaultKind===void 0?" [kind]":""}`),t(`search <query>${n}`),t("show <publicId>"),t("<publicId> download [--chat|-p for this chat only]"),t("trending <n> download \u2014 install #n from last list"),t("list \u2014 repeat last trending/search list")];return e.commandPrefix==="/run"&&o.push("","Also: /apps online \u2026 \xB7 /cowork online \u2026 \xB7 /shortcut online \u2026","","Publish (platform account):","/run <recipe> publish [--title \u2026] [--category \u2026]","/apps <session> publish","/cowork <name> publish","/shortcut <name> publish"),p(o.join(`
331
+ `))}function pi(e,t,n){if(e.length===0)return p(`${t}
332
+ (no results)`);let o=[t,""];return e.forEach((r,s)=>{let i=r.category?` \xB7 ${r.category}`:"";o.push(`${s+1}. ${r.publicId} (${r.kind}${i}) \u2193${r.downloadCount} \u2014 ${r.title||r.name}`),r.description&&o.push(` ${r.description.slice(0,120)}`)}),o.push("",`Install: ${di(n,"<publicId> download")} \u2014 or ${di(n,"trending <n> download")}`),p(o.join(`
333
+ `))}function lh(e,t){let n=[`${e.publicId} (${e.kind})`,`title: ${e.title||e.name}`,e.description?`description: ${e.description}`:"",e.category?`category: ${e.category}`:"",e.tags?.length?`tags: ${e.tags.join(", ")}`:"",`downloads: ${e.downloadCount}`,`author: ${e.authorLabel||"(unknown)"}`,"","payload:"].filter(Boolean),o=e.payload;if(e.kind==="recipe"||e.kind==="app"){let r=o;if(n.push(` command: ${r.command??""}`),r.promptTemplate&&n.push(` template: ${r.promptTemplate.slice(0,400)}${r.promptTemplate.length>400?"\u2026":""}`),Array.isArray(r.steps)&&r.steps.length>0){n.push(` steps: ${r.steps.length}`);for(let[s,i]of r.steps.entries()){let a=typeof i=="string"?i:i.cmd;n.push(` ${s+1}. ${a??""}`)}}}else if(e.kind==="shortcut")n.push(` body: ${o.body}`);else if(e.kind==="cowork"){let r=o;n.push(` command: ${r.command??""}`),n.push(` schedule: ${r.schedule?.kind??"?"}`)}return n.push("",di(t,`${e.publicId} download`)),p(n.join(`
334
+ `))}var Ul=new Set(["recipe","app","cowork","shortcut"]),tv={commandPrefix:"/run",listScope:"run"},ch={commandPrefix:"/apps",defaultKind:"app",listScope:"apps"},uh={commandPrefix:"/cowork",defaultKind:"cowork",listScope:"cowork"},dh={commandPrefix:"/shortcut",defaultKind:"shortcut",listScope:"shortcut"};function nv(e){if(!e)return;let t=e.toLowerCase();return Ul.has(t)?t:void 0}function Ht(e,t){return`${e.commandPrefix} online ${t}`}function ov(e,t){if(e.defaultKind)return t&&t.toLowerCase()!==e.defaultKind?{error:`This command only lists ${e.defaultKind} entries. Omit the kind suffix.`}:e.defaultKind;let n=nv(t);return t&&!n?{error:`Unknown kind "${t}". Use: recipe, app, cowork, shortcut`}:n}function rv(e,t){if(e.defaultKind){if(t.length>1&&Ul.has(t[t.length-1].toLowerCase())){if(t[t.length-1].toLowerCase()!==e.defaultKind)return{error:`This command only searches ${e.defaultKind} entries. Omit the kind suffix.`};t.pop()}return{kind:e.defaultKind,query:t.join(" ")}}let n;return t.length>1&&Ul.has(t[t.length-1].toLowerCase())&&(n=t.pop().toLowerCase()),{kind:n,query:t.join(" ")}}async function xo(e,t,n,o){let r=e.trim();if(!r||/^help$/i.test(r))return ah(o);let s=r.match(/^(.+?)\s+download(?:\s+(.*))?$/i);if(s){let c=s[1].trim(),d=(s[2]??"").trim(),u=/--chat|-p/i.test(d)?"chat":"global",m=null,h=/^trending\s+(\d+)$/i.exec(c),f=/^search\s+(\S+)\s+(\d+)$/i.exec(c);if(h){if(m=ui(t,o.listScope,h[1]),!m)return p(`No trending list in this chat \u2014 run ${Ht(o,"trending")} first, or use ${Ht(o,"<publicId> download")}.`)}else if(f){if(m=ui(t,o.listScope,f[2]),!m)return p(`No search list in this chat \u2014 run ${Ht(o,"search <query>")} first, or use ${Ht(o,"<publicId> download")}.`)}else if(/^\d+$/.test(c)){if(m=ui(t,o.listScope,c),!m)return p(`No list item #${c}. Run trending or search first, or use ${Ht(o,"<publicId> download")}.`)}else m=c;let g=await eh(m);if(Fn(g))return p(`Download failed: ${g.error}`);if(o.defaultKind&&g.kind!==o.defaultKind)return p(`Entry "${m}" is kind=${g.kind}, not ${o.defaultKind}. Use /run online show ${m} from /run.`);let y=oh(g,t,n,u);return y.ok?p(y.message):p(`Download failed: ${y.error}`)}let i=/^show\s+(\S+)\s*$/i.exec(r);if(i){let c=await Zm(i[1]);return Fn(c)?p(`Not found: ${c.error}`):o.defaultKind&&c.kind!==o.defaultKind?p(`Entry "${i[1]}" is kind=${c.kind}, not ${o.defaultKind}. Use /run online show ${i[1]}.`):lh(c,o)}let a=/^trending(?:\s+(\S+))?\s*$/i.exec(r);if(a){let c=ov(o,a[1]);if(typeof c=="object"&&"error"in c)return p(c.error);let d=await Vm(c?{kind:c}:void 0);if(Fn(d))return p(`Trending failed: ${d.error}`);Dl(t,o.listScope,d.items);let u=o.defaultKind?`Trending (${o.defaultKind})`:"Trending";return pi(d.items,u,o)}let l=/^search\s+([\s\S]+)$/i.exec(r);if(l){let c=l[1].trim().split(/\s+/),d=rv(o,c);if("error"in d)return p(d.error);let{kind:u,query:m}=d;if(!m)return p(`Usage: ${Ht(o,"search <query>")}`);let h=await Xm(m,u?{kind:u}:void 0);return Fn(h)?p(`Search failed: ${h.error}`):(Dl(t,o.listScope,h.items),pi(h.items,`Search: ${m}`,o))}if(/^list\s*$/i.test(r)){let c=ih(t,o.listScope);return c?.length?pi(c,"Last list",o):p(`No cached list. ${Ht(o,"trending")} or ${Ht(o,"search <query>")}`)}return p(`Unknown ${o.commandPrefix} online command. ${Ht(o,"help")}`)}async function ph(e,t,n){return xo(e,t,n,tv)}function cr(e){let t=e.trim(),n=[],o,r,s,i=a=>{let l=t.match(a);if(!l?.[1])return;let c=l[1].trim();if(t=(t.slice(0,l.index)+t.slice(l.index+l[0].length)).trim(),a.source.includes("title"))o=c.slice(0,120);else if(a.source.includes("description"))r=c.slice(0,500);else if(a.source.includes("category"))s=c.slice(0,40);else if(a.source.includes("tag"))for(let d of c.split(/[,;]/)){let u=d.trim();u&&n.push(u.slice(0,32))}};for(let a=0;a<8;a+=1){let l=t;if(i(/--title\s+"([^"]+)"/i),i(/--title\s+(\S+)/i),i(/--description\s+"([^"]+)"/i),i(/--description\s+(\S+)/i),i(/--category\s+"([^"]+)"/i),i(/--category\s+(\S+)/i),i(/--tag\s+"([^"]+)"/i),i(/--tag\s+(\S+)/i),t===l)break}return{title:o,description:r,category:s,tags:n,remainder:t.trim()}}function sv(e,t,n){return{kind:"recipe",name:e,title:n.title??t.label??e,description:n.description??t.description,category:n.category??t.category,tags:n.tags,payload:sh(t)}}function mh(e,t,n,o){let r=cr(o),s=n.trim().toLowerCase(),i=Ye(e,t,s);return i?i.source==="builtin"?{ok:!1,error:"Built-in recipes cannot be published. Copy with /run add first."}:{ok:!0,body:sv(i.name,i,r)}:{ok:!1,error:`Unknown recipe "${n}".`}}function hh(e,t,n){let o=cr(n),r=ii(e,t);return r?{ok:!0,body:{kind:"shortcut",name:r.name,title:o.title??r.name,description:o.description,category:o.category??"shortcut",tags:o.tags,payload:{body:r.body}}}:{ok:!1,error:`Unknown shortcut "${t}".`}}function fh(e,t,n){let o=cr(n),r=Pe(),s=et(r,t,e);return s?{ok:!0,body:{kind:"cowork",name:s.name,title:o.title??s.name,description:o.description,category:o.category??"cowork",tags:o.tags,payload:rh(s)}}:{ok:!1,error:`Unknown cowork task "${t}" for this chat.`}}function gh(e,t,n){let o=cr(n),r=e.trim().toLowerCase();return t.trim()?{ok:!0,body:{kind:"app",name:r,title:o.title??r,description:o.description,category:o.category??"app",tags:o.tags,payload:{command:t.trim(),label:o.title}}}:{ok:!1,error:"Session has no command to publish."}}async function Co(e){let t=Fl();if(!t.ok)return{ok:!1,error:t.error};let n=await th(e);if(Fn(n))return{ok:!1,error:n.error};let o=n;return{ok:!0,message:`${o.updated?"Updated":"Published"}: ${o.publicId} (${e.kind}). Others: /run online ${o.publicId} download`}}function iv(){return p(["Cowork \u2014 scheduled shell tasks (gateway must be running).","","add <name> <schedule> -- <command\u2026>"," schedule: ondemand | hourly [:MM] | daily HH:MM | weekdays HH:MM | weekly <mon\u2026sun> HH:MM","add <name> heartbeat <interval> [grace] \u2014 dead-man's-switch (no command needed)"," e.g. /cowork add backup heartbeat 1h 10m","set <name> cmd -- <command\u2026> | schedule <\u2026> | out <path> | cwd <path>"," notify self|wa|tg|all|none"," when always|failure|state-change \u2014 notify condition (default: always)"," attach on|off \u2014 send run log as a file with notify messages"," files clear | <glob/path \u2026> \u2014 optional artifacts (basename * ?); quote paths with spaces","list | show <name> | run <name> | checkin <name> | enable <name> | disable <name> | remove <name>","publish <name> \u2014 share task to online catalog (platform login)","online trending | show <publicId> | <publicId> download \u2014 cowork tasks only","","Output logs: task outputDir (default ~/Cowork/<name>). Missed times catch up when the gateway is back."].join(`
335
+ `))}function yh(e){let t=[],n=0;for(;n<e.length;){for(;n<e.length&&/\s/.test(e[n]);)n+=1;if(n>=e.length)break;if(e[n]==='"'){let r=n+1,s=e.indexOf('"',r);if(s===-1){t.push(e.slice(r));break}t.push(e.slice(r,s)),n=s+1;continue}let o=n;for(;o<e.length&&!/\s/.test(e[o]);)o+=1;t.push(e.slice(n,o)),n=o}return t}function av(e){let t=" -- ",n=e.indexOf(t);if(n===-1)return{error:'Missing " -- " before the command. Example: /cowork add tick weekdays 09:00 -- date'};let o=e.slice(0,n).trim(),r=e.slice(n+t.length).trim();if(!r)return{error:"Command after -- is empty."};let s=o.split(/\s+/).filter(Boolean);if(s.length<2)return{error:"Usage: /cowork add <name> <schedule\u2026> -- <command\u2026>"};let i=s[0],a=s.slice(1);return{name:i,scheduleWords:a,command:r}}async function bh(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return iv();let r=/^online\b([\s\S]*)$/i.exec(o);if(r)return xo((r[1]??"").trim(),t,n,uh);let s=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(o);if(s){let h=fh(t,s[1],s[2]??"");if(!h.ok)return p(h.error);let f=await Co(h.body);return p(f.ok?f.message:f.error)}let i=o.split(/\s+/)[0].toLowerCase();if(/^list$/i.test(i)){let h=Pe().filter(g=>g.ownerPeerKey===t);if(h.length===0)return p("(no cowork tasks for this chat)");let f=h.map(g=>{let y=g.enabled?"":" (disabled)",b=g.notifyWhen&&g.notifyWhen!=="always"?` when=${g.notifyWhen}`:"";return`${Ae}${g.name} ${co(g.schedule)} notify=${g.notify}${b}${y}`});return p(f.join(`
336
+ `))}let a=o.match(/^show\s+(\S+)\s*$/i);if(a){let h=a[1],f=Pe();Mn(f);let g=et(f,h,t);if(!g)return p(`Unknown task "${h}". /cowork list`);let y=Cs(g),b=[`name: ${g.name}`,`id: ${g.id}`,`schedule: ${co(g.schedule)}`,`enabled: ${g.enabled}`,`notify: ${g.notify}`,`notifyWhen: ${g.notifyWhen??"always"}`];if(g.schedule.kind==="heartbeat"){let k=Ts(g.id);b.push(`lastCheckin: ${k?new Date(k).toLocaleString():"(never \u2014 send /cowork checkin "+g.name+")"}`)}else b.push(`attachLog: ${g.attachLog}`),b.push(`files: ${g.attachFiles.length?g.attachFiles.join(", "):"(none)"}`),b.push(`cwd: ${g.cwd||"(session cwd)"}`),b.push(`out: ${g.outputDir}`),b.push(`cmd: ${g.command}`),b.push(`last slot: ${y?new Date(y).toLocaleString():"(never)"}`);return p(b.join(`
337
+ `))}if(/^add$/i.test(i)){let h=o.slice(3).trim(),f=h.match(/^(\S+)\s+(heartbeat\s+.+)$/i);if(f){let x=Ui(f[1]);if(!x.ok)return p(x.error);let O=f[2].split(/\s+/).filter(Boolean),A=xs(O);if(!A.ok)return p(A.error);let Y=Pe();if(et(Y,x.name,t))return p(`Task "${x.name}" already exists. Remove it first or pick another name.`);let te=Pr(x.name),ue={id:Ao(),name:x.name,ownerPeerKey:t,command:"",cwd:"",outputDir:te,schedule:A.schedule,enabled:!0,notify:"self",notifyWhen:"always",attachLog:!1,attachFiles:[],lastCompletedSlotMs:null,createdAtMs:Date.now()};return Y.push(ue),De(Y),Mn(Y),nl(ue.id),p([`Saved heartbeat task "${x.name}" (${co(A.schedule)}).`,`Send /cowork checkin ${x.name} to record a heartbeat.`,"Alerts if no check-in arrives within the expected interval + grace period."].join(`
338
+ `))}let g=av(h);if("error"in g)return p(g.error);let y=Ui(g.name);if(!y.ok)return p(y.error);let b=xs(g.scheduleWords);if(!b.ok)return p(b.error);let k=Pe();if(et(k,y.name,t))return p(`Task "${y.name}" already exists. Remove it first or pick another name.`);let R=ae(t),T=Pr(y.name),L={id:Ao(),name:y.name,ownerPeerKey:t,command:g.command,cwd:"",outputDir:T,schedule:b.schedule,enabled:!0,notify:"self",notifyWhen:"always",attachLog:!1,attachFiles:[],lastCompletedSlotMs:null,createdAtMs:Date.now()};return k.push(L),De(k),p([`Saved cowork task "${y.name}" (${co(b.schedule)}).`,`Output: ${T}`,`Notify: self \u2014 change with /cowork set ${y.name} notify wa|tg|all|none`].join(`
339
+ `))}let l=o.match(/^run\s+(\S+)\s*$/i);if(l){let h=l[1].toLowerCase(),f=et(Pe(),h,t);return f?f.enabled?(Ic({ownerPeerKey:t,name:f.name,at:Date.now()}),p(`On-demand run queued for "${f.name}" (runs within ~30s while omnish run is active).`)):p(`Cowork "${f.name}" is disabled. /cowork enable ${f.name}`):p(`Unknown task "${l[1]}". /cowork list`)}let c=o.match(/^checkin\s+(\S+)\s*$/i);if(c){let h=c[1].toLowerCase(),f=Pe();Mn(f);let g=et(f,h,t);return g?g.schedule.kind!=="heartbeat"?p(`"${g.name}" is not a heartbeat task.`):(nl(g.id),p(`Heartbeat recorded for "${g.name}".`)):p(`Unknown task "${c[1]}". /cowork list`)}let d=o.match(/^(?:remove|rm|del)\s+(\S+)\s*$/i);if(d){let h=d[1],f=Pe(),g=f.findIndex(y=>y.name===h.toLowerCase()&&y.ownerPeerKey===t);return g===-1?p(`Unknown task "${h}".`):(f.splice(g,1),De(f),p(`Removed cowork task "${h.toLowerCase()}".`))}let u=o.match(/^enable\s+(\S+)\s*$/i);if(u)return wh(u[1],t,!0);let m=o.match(/^disable\s+(\S+)\s*$/i);return m?wh(m[1],t,!1):/^set$/i.test(i)?dv(o.slice(3).trim(),t):p("Unknown /cowork command. Try /cowork help")}function wh(e,t,n){let o=Pe(),r=et(o,e,t);return r?(r.enabled=n,De(o),p(`Cowork "${r.name}" ${n?"enabled":"disabled"}.`)):p(`Unknown task "${e}".`)}function lv(e){let t=e.toLowerCase();return t==="self"?"self":t==="wa"||t==="whatsapp"?"wa":t==="tg"||t==="telegram"?"tg":t==="all"?"all":t==="none"?"none":null}function cv(e){let t=e.toLowerCase();return t==="always"||t==="all"?"always":t==="failure"||t==="fail"||t==="failures"?"failure":t==="state-change"||t==="statechange"||t==="change"||t==="transition"?"state-change":null}function uv(e){let t=e.toLowerCase();return t==="on"||t==="true"||t==="1"||t==="yes"?!0:t==="off"||t==="false"||t==="0"||t==="no"?!1:null}function dv(e,t){let n=e.match(/^(\S+)\s+([\s\S]+)$/);if(!n)return p("Usage: /cowork set <name> cmd -- \u2026 | schedule \u2026 | out <path> | cwd <path> | notify \u2026 | when \u2026 | attach <on|off> | files \u2026");let o=n[1],r=n[2].trim(),s=Pe(),i=et(s,o,t);if(!i)return p(`Unknown task "${o}".`);if(r.toLowerCase().startsWith("cmd ")){let a=r.slice(4).trim(),l=" -- ",c=a.indexOf(l),d;if(c!==-1)d=a.slice(c+l.length).trim();else if(a.startsWith("--"))d=a.slice(2).trim();else return p("Usage: /cowork set <name> cmd -- <command\u2026>");return d?(i.command=d,De(s),p(`Updated command for "${i.name}".`)):p("Command is empty.")}if(r.toLowerCase().startsWith("schedule ")){let a=r.slice(9).trim().split(/\s+/).filter(Boolean),l=xs(a);return l.ok?(i.schedule=l.schedule,De(s),p(`Schedule for "${i.name}": ${co(l.schedule)}`)):p(l.error)}if(r.toLowerCase().startsWith("out ")){let a=r.slice(4).trim(),l=ae(t);return i.outputDir=Qe(a,l.cwd),De(s),p(`outputDir: ${i.outputDir}`)}if(r.toLowerCase().startsWith("cwd ")){let a=r.slice(4).trim(),l=ae(t);return i.cwd=a?Qe(a,l.cwd):"",De(s),p(`cwd: ${i.cwd||"(session cwd at run time)"}`)}if(r.toLowerCase().startsWith("notify ")){let a=r.slice(7).trim(),l=lv(a);return l?(i.notify=l,De(s),p(`notify: ${l}`)):p("notify must be self, wa, tg, all, or none.")}if(r.toLowerCase().startsWith("when ")){let a=r.slice(5).trim(),l=cv(a);return l?(i.notifyWhen=l,De(s),p(`notifyWhen: ${l}`)):p("when must be always, failure, or state-change.")}if(r.toLowerCase().startsWith("attach ")){let a=r.slice(7).trim(),l=yh(a);if(l.length!==1)return p("Usage: /cowork set <name> attach on|off");let c=uv(l[0]);return c===null?p("attach must be on or off"):(i.attachLog=c,De(s),p(`attachLog: ${c}`))}if(r.toLowerCase().startsWith("files ")){let a=r.slice(6).trim(),l=yh(a);return l.length===1&&l[0].toLowerCase()==="clear"?(i.attachFiles=[],De(s),p("files: (cleared)")):l.length===0?p("Usage: /cowork set <name> files clear | <pattern \u2026> \u2014 quote paths with spaces"):(i.attachFiles=l,De(s),p(`files: ${i.attachFiles.join(", ")}`))}return p("Unknown set field. Try: cmd, schedule, out, cwd, notify, when, attach, files")}pe();import Iv from"node:fs";q();import kh from"node:os";import pv from"node:path";var mv=new Set(["create","delete","rename","update"]);function hv(e){let t=[],n=0;for(;n<e.length;){for(;n<e.length&&/\s/.test(e[n]);)n+=1;if(n>=e.length)break;if(e[n]==='"'){let r=n+1,s=e.indexOf('"',r);if(s===-1){t.push(e.slice(r));break}t.push(e.slice(r,s)),n=s+1;continue}let o=n;for(;o<e.length&&!/\s/.test(e[o]);)o+=1;t.push(e.slice(n,o)),n=o}return t}function fv(e){let t=e.split(",").map(n=>n.trim().toLowerCase());return t.length===0?!1:t.every(n=>mv.has(n))}function gv(e){return e.split(",").map(t=>t.trim().toLowerCase())}function Bl(e,t,n){let o=e.trim();if(o.startsWith("-")&&(o=o.slice(1)),!o)return{};let r=Qe(o,n);return o.includes("*")||o.includes("?")?{glob:o.replace(/\\/g,"/")}:pv.isAbsolute(r)||o.startsWith("~")||o.startsWith("./")||o.startsWith("../")?{path:r}:o.startsWith("/")?{path:r}:{glob:o.includes("/")?o:`**/${o}`}}function vh(e,t=kh.homedir()){let n=e.replace(/\s+&&\s+/g," ").trim(),o=hv(n);if(o.length===0)return{ok:!1,error:"Usage: /watch add fs <name> <path> [events] [-exclude \u2026] [--exclude \u2026]"};let r=0,s=o[r];r+=1;let i=["create","delete","rename"],a=[],l=[];for(;r<o.length;){let d=o[r];if(d==="--exclude"){if(r+=1,r>=o.length)return{ok:!1,error:"--exclude requires a pattern."};let u=o[r];r+=1;let m=Qe(s,t),h=Bl(u.startsWith("-")?u:`-${u}`,m,t);h.path?a.push(h.path):h.glob&&l.push(h.glob);continue}if(d.startsWith("--"))return{ok:!1,error:`Unknown flag: ${d}`};if(d.startsWith("-")){let u=Qe(s,t),m=Bl(d,u,t);m.path?a.push(m.path):m.glob&&l.push(m.glob),r+=1;continue}if(fv(d)){i=gv(d),r+=1;continue}return{ok:!1,error:`Unexpected token "${d}". Put path first, then events or -excludes.`}}return{ok:!0,value:{rootPath:Qe(s,t),events:i,excludePaths:a,excludeGlobs:l}}}function Hl(e,t,n=kh.homedir()){let o=Bl(e.startsWith("-")?e:`-${e}`,t,n);return!o.path&&!o.glob?{error:"Empty exclude pattern."}:o}import{spawnSync as mi}from"node:child_process";import Sh from"node:fs";import yv from"node:os";import wv from"node:path";var bv=40,kv=10,hi=15e3,vv=["~/Projects","~/deploy","~/Downloads","~/src","/var/www","/srv"],xh={linux:"/var/log/dpkg.log (also /var/log/apt/history.log)",darwin:"/var/log/install.log",win32:"Windows Application event log (install/remove)"};function Sv(e){return e.startsWith("~/")?wv.join(yv.homedir(),e.slice(2)):e}function xv(e,t){return t?.trim()?e.toLowerCase().includes(t.trim().toLowerCase()):!0}function Cv(e){let t=n=>n==="running"||n==="active";return[...e].sort((n,o)=>{let r=t(n.state),s=t(o.state);return r!==s?r?-1:1:n.name.localeCompare(o.name)})}function Rv(e){let t=[];for(let n of e.split(`
340
+ `)){let o=n.trim();if(!o)continue;let r=o.split(/\s+/);if(r.length<4)continue;let s=r[0];if(!s.endsWith(".service"))continue;let i=r[2].toLowerCase(),a=s.slice(0,-8);t.push({name:a,state:i})}return t}function Tv(e){let t=[],n=e.split(`
341
+ `);for(let o=0;o<n.length;o++){let r=n[o].trim();if(!r||r.startsWith("PID"))continue;let s=r.split(/\s+/);if(s.length<3)continue;let i=s[s.length-1];if(!i.includes("."))continue;let l=s[0]==="-"?"stopped":"running";t.push({name:i,state:l})}return t}function $v(e){let t=[],n="";for(let o of e.split(`
342
+ `)){let r=o.match(/SERVICE_NAME:\s*(.+)/i);if(r){n=r[1].trim();continue}let s=o.match(/^\s*STATE\s*:\s*\d+\s+(\S+)/i);if(s&&n){let i=s[1].toUpperCase(),a=i.toLowerCase();i==="RUNNING"?a="running":i==="STOPPED"&&(a="stopped"),t.push({name:n,state:a}),n=""}}return t}function Mv(){let e=mi("systemctl",["list-units","--type=service","--all","--no-pager","--plain","--no-legend"],{encoding:"utf8",timeout:hi});return e.error||e.status!==0?{entries:[],error:"systemctl unavailable \u2014 install systemd or run on Linux."}:{entries:Rv(e.stdout??"")}}function Pv(){let e=mi("launchctl",["list"],{encoding:"utf8",timeout:hi});return e.error||e.status!==0?{entries:[],error:"launchctl unavailable."}:{entries:Tv(e.stdout??"")}}function Ev(){let e=mi("sc",["query","type=","service","state=","all"],{encoding:"utf8",timeout:hi,windowsHide:!0});if(!e.error&&e.status===0&&(e.stdout??"").includes("SERVICE_NAME"))return{entries:$v(e.stdout??"")};let t=mi("powershell",["-NoProfile","-Command","Get-Service | ForEach-Object { $_.Name + ' ' + $_.Status }"],{encoding:"utf8",timeout:hi,windowsHide:!0});if(t.error||t.status!==0)return{entries:[],error:"sc query and Get-Service unavailable."};let n=[];for(let o of(t.stdout??"").split(`
343
+ `)){let r=o.trim();if(!r)continue;let s=r.lastIndexOf(" ");if(s<=0)continue;let i=r.slice(0,s).trim(),a=r.slice(s+1).trim().toLowerCase();n.push({name:i,state:a})}return{entries:n}}function fi(e){let t=e?.limit??bv,n=e?.filter,o,r=process.platform;if(r==="linux")o=Mv();else if(r==="darwin")o=Pv();else if(r==="win32")o=Ev();else return{entries:[],truncated:!1,totalMatched:0,error:`Service discovery not supported on ${r}.`};if(o.error)return{entries:[],truncated:!1,totalMatched:0,error:o.error};let s=Cv(o.entries.filter(l=>xv(l.name,n))),i=s.length,a=i>t;return{entries:s.slice(0,t),truncated:a,totalMatched:i}}function Av(e,t=kv){let n=[];for(let o=0;o<e.length;o+=t)n.push(e.slice(o,o+t));return n}function jl(e,t){if(e.error)return p(e.error);if(e.entries.length===0){let r=t?.trim()?`No services match "${t}". Try /watch svc list without a filter.`:"No services found on this host.";return p(r)}let n=e.entries.map(r=>`${Ae}${r.name} \u2014 ${r.state}`),o=t?.trim()?`Services matching "${t}" (${e.entries.length} shown):`:`Services on ${process.platform} (${e.entries.length} shown):`;if(n.unshift(o),e.truncated){let r=e.totalMatched-e.entries.length;n.push("",`${r} more \u2014 narrow with /watch svc list <filter>`)}return n.push("","Copy-paste templates follow in the next message."),p(n.join(`
344
+ `))}function Gl(e,t){let n=t?.trim()||"<name>";if(e.length===0)return p("No services to template \u2014 run /watch svc list first.");let o=e.map(i=>i.name),r=Av(o),s=[t?.trim()?`Copy-paste (/watch add svc ${n} \u2026):`:"Copy-paste (replace <name>):",""];for(let i of r)s.push(`/watch add svc ${n} ${i.join(" ")}`);return p(s.join(`
345
+ `))}function Ch(){let e=process.platform,t=e==="linux"?"linux":e==="darwin"?"darwin":"win32",n=["Watch hints",""];n.push("Package log (pkg watches):"),n.push(` ${xh[t]??xh.linux}`),n.push("","Filesystem roots that exist on this host:");let o=[];for(let r of vv){let s=Sv(r);try{Sh.existsSync(s)&&Sh.statSync(s).isDirectory()&&o.push(r)}catch{}}if(o.length===0)n.push(" (none of the usual paths found \u2014 use any directory you own)");else for(let r of o)n.push(` ${r}`),n.push(` /watch add fs <name> ${r} create,delete,rename`);return n.push("","Service names: /watch svc list [filter]"),p(n.join(`
346
+ `))}function B(e){return{replies:[e]}}function Lv(...e){return{replies:e}}function Ov(e){let t=e.trim();if(!t)return{};let n=t.split(/\s+/);if(n.length>=2){let s=Ir(n[0]);return s.ok?{ruleName:s.name,filter:n.slice(1).join(" ")}:{filter:t}}let o=n[0],r=Ir(o);if(r.ok){let s=fi({filter:o,limit:1});if(s.totalMatched===0&&!s.error)return{ruleName:r.name}}return{filter:o}}function Nv(){return p(["Watch \u2014 OS event eye (watchEnabled + omnish run). Docs: docs/features/watch.md","","add fs <name> <path> [events] [-/exclude \u2026] [--exclude glob]",' e.g. /watch add fs home ~/Projects create,delete -~/Projects/tmp --exclude "**/.keyfolder"',"add pkg <name> | add svc <name> <unit\u2026>","svc list [filter] \u2014 services on this host (+ templates in next message)","svc templates [ruleName] [filter] \u2014 copy-paste /watch add svc lines only","hints \u2014 suggested FS paths and package log location","list | status | reload | show <name> | recent [N]","pause|stop <name> \u2014 stop alerts, keep rule","resume <name> | enable <name> | disable <name> | rm <name>","exclude <name> list|add <pattern>|rm <pattern>","set <name> notify self|wa|tg|all|none","set <name> when always|state-change","on | off \u2014 global watchEnabled in config","Rules are device-wide (~/.omnish/watch/rules.json); any allowlisted peer can manage.","notify:self alerts the peer who created the rule."].join(`
347
+ `))}function Rh(){return{excludePaths:[],excludeGlobs:[]}}function Fv(e){return e.notify==="self"?`notify=self (${e.ownerPeerKey})`:`notify=${e.notify}`}function Wv(e,t){Ji(e.id),t==="pause"||t==="stop"?e.paused=!0:t==="resume"?e.paused=!1:t==="enable"?(e.enabled=!0,e.paused=!1):t==="disable"&&(e.enabled=!1,e.paused=!1)}function Th(e,t){let n=e.trim();if(!n||/^help$/i.test(n))return B(Nv());let o=n.split(/\s+/)[0].toLowerCase();if(Er(),/^hints$/i.test(o))return B(Ch());if(/^svc$/i.test(o)){let d=n.slice(3).trim(),m=(d.split(/\s+/)[0]??"").toLowerCase();if(m==="list"){let h=d.slice(4).trim()||void 0,f=fi({filter:h});return f.error?B(p(f.error)):f.entries.length===0?B(jl(f,h)):Lv(jl(f,h),Gl(f.entries))}if(m==="templates"){let h=d.slice(9).trim(),{ruleName:f,filter:g}=Ov(h),y=fi({filter:g});return y.error?B(p(y.error)):B(Gl(y.entries,f))}return B(p("svc subcommands: list [filter] | templates [ruleName] [filter]"))}if(/^status$/i.test(o)){let d=S(),{summary:u}=Lo(),m=[`watchEnabled: ${d.watchEnabled} watchAutoRestore: ${d.watchAutoRestore}`,`debounce: ${d.watchDebounceMs}ms max/min: ${d.watchMaxEventsPerMinute}`,`rules file: ${ji()} (${u.total} saved, ${u.active} active, ${u.paused} paused, ${u.disabled} disabled)`,`events db: ${kr}`];u.total>0&&!d.watchEnabled?m.push("Rules are on disk; adapters stopped \u2014 /watch on to start."):u.total>0&&d.watchEnabled&&!d.watchAutoRestore?m.push("watchAutoRestore is off \u2014 /watch reload to start adapters without restarting gateway."):u.paused>0&&m.push(`${u.paused} paused rule(s) stay stopped until /watch resume <name>.`);let h=au();return h?(m.push("","Adapters:"),m.push(...h.getStatusLines())):d.watchEnabled&&d.watchAutoRestore?m.push("","(manager starting \u2014 try /watch status again)"):d.watchEnabled?m.push("","(manager idle \u2014 /watch reload)"):m.push("","(manager off \u2014 /watch on)"),B(p(m.join(`
348
+ `)))}if(/^reload$/i.test(o)){let{summary:d}=Lo();return S().watchEnabled?(lu(),B(p(`Reloading from ${ji()}: ${d.total} rule(s) on disk, ${d.active} eligible to run. /watch status for adapters.`))):B(p(`Rules on disk: ${d.total} (${d.active} active). watchEnabled is false \u2014 /watch on first.`))}if(/^list$/i.test(o)){let d=Ue();if(d.length===0)return B(p("(no watch rules on this device)"));let u=d.map(m=>{let h=m.enabled?m.paused?"paused":"on":"disabled",f=m.kind==="fs"?m.path:m.kind==="svc"?m.units.join(","):"system";return`${Ae}${m.name} [${m.kind}] ${h} ${Fv(m)} when=${m.notifyWhen} \u2014 ${f}`});return B(p(u.join(`
349
+ `)))}let r=n.match(/^recent(?:\s+(\d+))?\s*$/i);if(r){let d=r[1]?Number(r[1]):15,u=Ue(),m=new Set(u.map(g=>g.id)),h=_c(d,m);if(h.length===0)return B(p("(no recent watch events)"));let f=h.map(g=>`${new Date(g.tsMs).toLocaleString()} ${g.ruleName} ${g.summary}`);return B(p(f.join(`
350
+ `)))}let s=n.match(/^show\s+(\S+)\s*$/i);if(s){let d=yn(Ue(),s[1]);if(!d)return B(p(`Unknown rule "${s[1]}". /watch list`));let u=[`name: ${d.name}`,`kind: ${d.kind}`,`creator: ${d.ownerPeerKey}`,`enabled: ${d.enabled}`,`paused: ${d.paused}`,`notify: ${d.notify}`,`notifyWhen: ${d.notifyWhen}`,`adapter: ${d.adapterStatus||"(unknown)"}`];return d.kind==="fs"&&(u.push(`path: ${d.path}`),u.push(`events: ${d.events.join(",")}`),u.push(`excludePaths: ${d.excludePaths.length?d.excludePaths.join(", "):"(none)"}`),u.push(`excludeGlobs: ${d.excludeGlobs.length?d.excludeGlobs.join(", "):"(none)"}`)),d.kind==="svc"&&u.push(`units: ${d.units.join(", ")||"(none)"}`),B(p(u.join(`
351
+ `)))}if(/^on$/i.test(o))return N({watchEnabled:!0}),Je(),B(p("watchEnabled: true (gateway picks up watchers when running)."));if(/^off$/i.test(o))return N({watchEnabled:!1}),Je(),B(p("watchEnabled: false \u2014 all adapters stopped."));let i=n.match(/^exclude\s+(\S+)\s+(\S+)(?:\s+([\s\S]+))?\s*$/i);if(i){let d=i[1],u=i[2].toLowerCase(),m=(i[3]??"").trim(),h=Ue(),f=yn(h,d);if(!f)return B(p(`Unknown rule "${d}".`));if(f.kind!=="fs")return B(p("exclude applies to filesystem rules only."));if(u==="list"){let g=[`excludePaths: ${f.excludePaths.length?f.excludePaths.join(`
327
352
  `):"(none)"}`,`excludeGlobs: ${f.excludeGlobs.length?f.excludeGlobs.join(`
328
- `):"(none)"}`];return U(p(g.join(`
329
- `)))}if(c==="add"){if(!m)return U(p("Usage: /watch exclude <name> add <pattern>"));let g=bl(m,f.path);if("error"in g)return U(p(g.error));if(g.path){if(et(g.path))return U(p("That path is blocked."));f.excludePaths.includes(g.path)||f.excludePaths.push(g.path)}return g.glob&&!f.excludeGlobs.includes(g.glob)&&f.excludeGlobs.push(g.glob),yt(Nn(h,f)),qe(),U(p(`Added exclude to "${f.name}".`))}if(c==="rm"||c==="remove"){if(!m)return U(p("Usage: /watch exclude <name> rm <pattern>"));let g=bl(m,f.path);return"error"in g?U(p(g.error)):(g.path&&(f.excludePaths=f.excludePaths.filter(y=>y!==g.path)),g.glob&&(f.excludeGlobs=f.excludeGlobs.filter(y=>y!==g.glob)),yt(Nn(h,f)),qe(),U(p(`Removed exclude from "${f.name}".`)))}return U(p("exclude subcommands: list | add | rm"))}if(/^add$/i.test(o)){let u=n.slice(3).trim(),c=u.split(/\s+/);if(c.length<2)return U(p("Usage: /watch add fs|pkg|svc <name> \u2026"));let m=c[0].toLowerCase(),h=m==="fs"||m==="pkg"||m==="svc"?m:null;if(!h)return U(p("Kind must be fs, pkg, or svc."));let f=c[1],g=vr(f);if(!g.ok)return U(p(g.error));let y=De();if(y.length>=xi)return U(p(`Max ${xi} watch rules on this device.`));if(un(y,g.name))return U(p(`Rule "${g.name}" already exists.`));let b;if(h==="fs"){let R=u.slice(m.length+1+f.length).trim(),L=Nm(R);if(!L.ok)return U(p(L.error));let{rootPath:$,events:N,excludePaths:A,excludeGlobs:z}=L.value;if(et($))return U(p("That path is blocked (sensitive). Choose another directory."));if(!kk.existsSync($))return U(p(`Path not found: ${$}`));for(let Z of A)if(et(Z))return U(p(`Excluded path blocked: ${Z}`));b={id:Sr(),name:g.name,ownerPeerKey:t,kind:"fs",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:$,events:N,units:[],excludePaths:A,excludeGlobs:z,adapterStatus:"",createdAtMs:Date.now()}}else if(h==="pkg")b={id:Sr(),name:g.name,ownerPeerKey:t,kind:"pkg",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:"",events:[],units:[],...Dm(),adapterStatus:"",createdAtMs:Date.now()};else{let R=c.slice(2);if(R.length===0)return U(p("Usage: /watch add svc <name> <unit\u2026>"));b={id:Sr(),name:g.name,ownerPeerKey:t,kind:"svc",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:"",events:[],units:R,...Dm(),adapterStatus:"",createdAtMs:Date.now()}}yt(Nn(y,b)),qe();let M=S().watchEnabled?"":" Rule saved to disk. /watch on to start adapters.";return U(p(`Watch rule "${b.name}" added (${b.kind}).${M}`))}let a=n.match(/^set\s+(\S+)\s+(\S+)\s+(\S+)\s*$/i);if(a){let[,u,c,m]=a,h=De(),f=un(h,u);if(!f)return U(p(`Unknown rule "${u}".`));let g=c.toLowerCase(),y=m.toLowerCase();if(g==="notify"){let b=y==="wa"||y==="whatsapp"?"wa":y==="tg"||y==="telegram"?"tg":y==="all"?"all":y==="none"?"none":y==="self"?"self":null;if(!b)return U(p("notify must be self, wa, tg, all, or none."));f.notify=b}else if(g==="when"){let b=y==="state-change"?"state-change":y==="always"?"always":null;if(!b)return U(p("when must be always or state-change."));f.notifyWhen=b}else return U(p("set supports: notify, when"));return yt(Nn(h,f)),qe(),U(p(`Updated ${f.name} ${g}=${y}.`))}let l=n.match(/^(?:rm|remove)\s+(\S+)\s*$/i);if(l){let u=l[1],c=De(),m=un(c,u);return m?(Ti(m.id),yt(vc(c,u)),qe(),U(p(`Removed watch rule "${u}".`))):U(p(`Unknown rule "${u}".`))}let d=n.match(/^(pause|stop|resume|enable|disable)\s+(\S+)\s*$/i);if(d){let u=d[1].toLowerCase(),c=d[2],m=De(),h=un(m,c);if(!h)return U(p(`Unknown rule "${c}".`));Rk(h,u==="stop"?"pause":u),yt(Nn(m,h)),qe();let g=u==="stop"?"paused (stop)":`${u}d`;return U(p(`${h.name}: ${g}.`))}return U(p("Unknown /watch command. Try /watch help"))}pe();pn();mn();function Bm(){return p(["Tunnel commands:",'/tunnel signup --email "you@example.com" --password "yourpass"','/tunnel signup --phone "+1234567890" --password "yourpass"','/tunnel login --email "you@example.com" --password "yourpass"','/tunnel login --token "<token>" [--relay <url>]',"/tunnel logout","/tunnel status [--relay <url>]","/tunnel http <port> [--name <slug>] [--host <addr>]","/tunnel tcp <port> [--name <slug>] [--host <addr>]","/tunnels","/tunnel stop <id|slug>"].join(`
330
- `))}async function Sl(e,t,n){let o=Ip(e);if(o.kind==="help")return Bm();if(o.kind==="error")return p(o.message);if(o.kind==="signup"){let r=o.email?.trim()||"",s=o.phone?.trim()||"",i=o.password||"";if(!r&&!s)return p('Usage: /tunnel signup --email "you@example.com" --password "yourpass"');if(i.length<8)return p("Password must be at least 8 characters.");let a=S(),l=o.relayUrl?.trim()||ct(a.tunnelRelayUrl||Ee),d=await Os(l,{...r?{email:r}:{},...s?{phone:s}:{},password:i});return d.ok?(bt({token:d.token,...o.relayUrl?.trim()?{relayUrl:o.relayUrl.trim()}:{}}),p("Account created. Token saved.")):p(`Signup failed: ${d.error}`)}if(o.kind==="login"){if(o.email||o.phone){let i=o.password||"";if(!i)return p('Usage: /tunnel login --email "you@example.com" --password "yourpass"');let a=S(),l=o.relayUrl?.trim()||ct(a.tunnelRelayUrl||Ee),d=await Ns(l,{...o.email?{email:o.email}:{},...o.phone?{phone:o.phone}:{},password:i});return d.ok?(bt({token:d.token,...o.relayUrl?.trim()?{relayUrl:o.relayUrl.trim()}:{}}),p("Logged in. Token saved.")):p(`Login failed: ${d.error}`)}let s=o.token?.trim();return s?(bt({token:s,...o.relayUrl?.trim()?{relayUrl:o.relayUrl.trim()}:{}}),p("Tunnel token saved on this host (not shown). If OMNISH_TUNNEL_TOKEN is set in the gateway environment, it overrides the file until unset.")):p('Usage: /tunnel login --token "<token>" [--relay <url>]')}if(o.kind==="logout")return Er(),p("Tunnel token file removed. Unset OMNISH_TUNNEL_TOKEN in the gateway environment if you rely on it.");if(o.kind==="status"){let r=S(),s=o.relayUrl?.trim()||ct(r.tunnelRelayUrl||Ee),i=kt(),a=!!process.env.OMNISH_TUNNEL_TOKEN?.trim(),d=!!Pt()?.token?.trim(),u=await Es(s,i),c=[`Relay: ${s}`,`Token: ${i?"configured":"missing"}${a?" (OMNISH_TUNNEL_TOKEN)":d?" (tunnel-auth.json)":""}`,`Health: ${u.healthOk?`ok${u.healthVersion?` (version ${u.healthVersion})`:""}`:"fail"}`,`Control (WSS): ${u.controlOk?"auth ok":"fail"}`,`Active tunnels: ${n.getActiveCount()}`];return!u.ok&&u.error&&c.push(`Detail: ${u.error}`),p(c.join(`
353
+ `):"(none)"}`];return B(p(g.join(`
354
+ `)))}if(u==="add"){if(!m)return B(p("Usage: /watch exclude <name> add <pattern>"));let g=Hl(m,f.path);if("error"in g)return B(p(g.error));if(g.path){if(tt(g.path))return B(p("That path is blocked."));f.excludePaths.includes(g.path)||f.excludePaths.push(g.path)}return g.glob&&!f.excludeGlobs.includes(g.glob)&&f.excludeGlobs.push(g.glob),bt(qn(h,f)),Je(),B(p(`Added exclude to "${f.name}".`))}if(u==="rm"||u==="remove"){if(!m)return B(p("Usage: /watch exclude <name> rm <pattern>"));let g=Hl(m,f.path);return"error"in g?B(p(g.error)):(g.path&&(f.excludePaths=f.excludePaths.filter(y=>y!==g.path)),g.glob&&(f.excludeGlobs=f.excludeGlobs.filter(y=>y!==g.glob)),bt(qn(h,f)),Je(),B(p(`Removed exclude from "${f.name}".`)))}return B(p("exclude subcommands: list | add | rm"))}if(/^add$/i.test(o)){let d=n.slice(3).trim(),u=d.split(/\s+/);if(u.length<2)return B(p("Usage: /watch add fs|pkg|svc <name> \u2026"));let m=u[0].toLowerCase(),h=m==="fs"||m==="pkg"||m==="svc"?m:null;if(!h)return B(p("Kind must be fs, pkg, or svc."));let f=u[1],g=Ir(f);if(!g.ok)return B(p(g.error));let y=Ue();if(y.length>=Hi)return B(p(`Max ${Hi} watch rules on this device.`));if(yn(y,g.name))return B(p(`Rule "${g.name}" already exists.`));let b;if(h==="fs"){let T=d.slice(m.length+1+f.length).trim(),L=vh(T);if(!L.ok)return B(p(L.error));let{rootPath:x,events:O,excludePaths:A,excludeGlobs:Y}=L.value;if(tt(x))return B(p("That path is blocked (sensitive). Choose another directory."));if(!Iv.existsSync(x))return B(p(`Path not found: ${x}`));for(let te of A)if(tt(te))return B(p(`Excluded path blocked: ${te}`));b={id:Lr(),name:g.name,ownerPeerKey:t,kind:"fs",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:x,events:O,units:[],excludePaths:A,excludeGlobs:Y,adapterStatus:"",createdAtMs:Date.now()}}else if(h==="pkg")b={id:Lr(),name:g.name,ownerPeerKey:t,kind:"pkg",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:"",events:[],units:[],...Rh(),adapterStatus:"",createdAtMs:Date.now()};else{let T=u.slice(2);if(T.length===0)return B(p("Usage: /watch add svc <name> <unit\u2026>"));b={id:Lr(),name:g.name,ownerPeerKey:t,kind:"svc",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:"",events:[],units:T,...Rh(),adapterStatus:"",createdAtMs:Date.now()}}bt(qn(y,b)),Je();let R=S().watchEnabled?"":" Rule saved to disk. /watch on to start adapters.";return B(p(`Watch rule "${b.name}" added (${b.kind}).${R}`))}let a=n.match(/^set\s+(\S+)\s+(\S+)\s+(\S+)\s*$/i);if(a){let[,d,u,m]=a,h=Ue(),f=yn(h,d);if(!f)return B(p(`Unknown rule "${d}".`));let g=u.toLowerCase(),y=m.toLowerCase();if(g==="notify"){let b=y==="wa"||y==="whatsapp"?"wa":y==="tg"||y==="telegram"?"tg":y==="all"?"all":y==="none"?"none":y==="self"?"self":null;if(!b)return B(p("notify must be self, wa, tg, all, or none."));f.notify=b}else if(g==="when"){let b=y==="state-change"?"state-change":y==="always"?"always":null;if(!b)return B(p("when must be always or state-change."));f.notifyWhen=b}else return B(p("set supports: notify, when"));return bt(qn(h,f)),Je(),B(p(`Updated ${f.name} ${g}=${y}.`))}let l=n.match(/^(?:rm|remove)\s+(\S+)\s*$/i);if(l){let d=l[1],u=Ue(),m=yn(u,d);return m?(Ji(m.id),bt(Gc(u,d)),Je(),B(p(`Removed watch rule "${d}".`))):B(p(`Unknown rule "${d}".`))}let c=n.match(/^(pause|stop|resume|enable|disable)\s+(\S+)\s*$/i);if(c){let d=c[1].toLowerCase(),u=c[2],m=Ue(),h=yn(m,u);if(!h)return B(p(`Unknown rule "${u}".`));Wv(h,d==="stop"?"pause":d),bt(qn(m,h)),Je();let g=d==="stop"?"paused (stop)":`${d}d`;return B(p(`${h.name}: ${g}.`))}return B(p("Unknown /watch command. Try /watch help"))}pe();bn();kn();function $h(){return p(["Tunnel commands:",'/tunnel signup --email "you@example.com" --password "yourpass"','/tunnel signup --phone "+1234567890" --password "yourpass"','/tunnel login --email "you@example.com" --password "yourpass"','/tunnel login --token "<token>" [--relay <url>]',"/tunnel logout","/tunnel status [--relay <url>]","/tunnel http <port> [--name <slug>] [--host <addr>]","/tunnel tcp <port> [--name <slug>] [--host <addr>]","/tunnels","/tunnel stop <id|slug>"].join(`
355
+ `))}async function Jl(e,t,n){let o=mm(e);if(o.kind==="help")return $h();if(o.kind==="error")return p(o.message);if(o.kind==="signup"){let r=o.email?.trim()||"",s=o.phone?.trim()||"",i=o.password||"";if(!r&&!s)return p('Usage: /tunnel signup --email "you@example.com" --password "yourpass"');if(i.length<8)return p("Password must be at least 8 characters.");let a=S(),l=o.relayUrl?.trim()||ct(a.tunnelRelayUrl||Ee),c=await Xs(l,{...r?{email:r}:{},...s?{phone:s}:{},password:i});return c.ok?(vt({token:c.token,...o.relayUrl?.trim()?{relayUrl:o.relayUrl.trim()}:{}}),p("Account created. Token saved.")):p(`Signup failed: ${c.error}`)}if(o.kind==="login"){if(o.email||o.phone){let i=o.password||"";if(!i)return p('Usage: /tunnel login --email "you@example.com" --password "yourpass"');let a=S(),l=o.relayUrl?.trim()||ct(a.tunnelRelayUrl||Ee),c=await Zs(l,{...o.email?{email:o.email}:{},...o.phone?{phone:o.phone}:{},password:i});return c.ok?(vt({token:c.token,...o.relayUrl?.trim()?{relayUrl:o.relayUrl.trim()}:{}}),p("Logged in. Token saved.")):p(`Login failed: ${c.error}`)}let s=o.token?.trim();return s?(vt({token:s,...o.relayUrl?.trim()?{relayUrl:o.relayUrl.trim()}:{}}),p("Tunnel token saved on this host (not shown). If OMNISH_TUNNEL_TOKEN is set in the gateway environment, it overrides the file until unset.")):p('Usage: /tunnel login --token "<token>" [--relay <url>]')}if(o.kind==="logout")return Br(),p("Tunnel token file removed. Unset OMNISH_TUNNEL_TOKEN in the gateway environment if you rely on it.");if(o.kind==="status"){let r=S(),s=o.relayUrl?.trim()||ct(r.tunnelRelayUrl||Ee),i=St(),a=!!process.env.OMNISH_TUNNEL_TOKEN?.trim(),c=!!It()?.token?.trim(),d=await Ks(s,i),u=[`Relay: ${s}`,`Token: ${i?"configured":"missing"}${a?" (OMNISH_TUNNEL_TOKEN)":c?" (tunnel-auth.json)":""}`,`Health: ${d.healthOk?`ok${d.healthVersion?` (version ${d.healthVersion})`:""}`:"fail"}`,`Control (WSS): ${d.controlOk?"auth ok":"fail"}`,`Active tunnels: ${n.getActiveCount()}`];return!d.ok&&d.error&&u.push(`Detail: ${d.error}`),p(u.join(`
331
356
  `))}if(!t.tunnelEnabled)return p("Chat tunneling is disabled. Use /config set tunnelEnabled true (then expose with /tunnel http \u2026).");if(o.kind==="list"){let r=n.list();return r.length===0?p("(no active tunnels)"):p(r.map(s=>`${s.id} ${s.kind} ${s.status}
332
357
  ${s.publicUrl||"(pending)"}
333
358
  ${s.localHost}:${s.localPort}`).join(`
@@ -335,73 +360,71 @@ Notify on completion: on`:"";return{kind:"text",body:p(`Pull job ${c} started ($
335
360
  `))}if(o.kind==="stop"){let r=await n.stop(o.target);return r?p(`Stopped tunnel ${r.id}.`):p(`No active tunnel matched "${o.target}".`)}if(o.kind==="expose"){let r=await n.expose(t,o.options);return p(`${r.kind.toUpperCase()} tunnel active
336
361
  public: ${r.publicUrl}
337
362
  local: ${r.localHost}:${r.localPort}
338
- id: ${r.id}`)}return Bm()}var Hm={version:1,generatedAt:"2026-05-23T05:44:55.535Z",repoUrl:"https://github.com/eligapris/omnish/blob/main",entries:[{id:"docs-advanced-implementation",path:"docs/advanced/implementation.md",title:"Implementation Guide - omnish",summary:"For contributors and developers who want to understand and extend omnish.",sections:[{title:"Development Setup",body:"Prerequisites Node.js >= 20 pnpm package manager Native build tools (make, g++, etc.) Getting Started ```bash git clone https://github.com/labKnowledge/whatsLive.git cd whatsLive pnpm install"}],keywords:["implementation","guide","omnish","for","contributors","and","developers","who","want","to","understand","extend","development","setup"],relatedCommands:["/omnish","/github","/labknowledge","/whatslive","/wa","/inbound","/tg","/gateway","/signal","/outbound","/index","/config"]},{id:"docs-advanced-troubleshooting",path:"docs/advanced/troubleshooting.md",title:"Troubleshooting Guide - omnish",summary:"Common issues and solutions for omnish.",sections:[{title:"Quick Reference",body:`Symptom Common Cause Solution --------- ------------- ---------- Connection failed Network issues Check connectivity "Not in allowlist" Wrong format Use E.164 format Sessions won't start Resource limits Check session limits Messages cut off Size limits Adjust config Telegram not working Bot token Verify token Attached: probe fails / WS 400 Proxy routes to wrong port Route , to control port 8788; Attached: online but no commands Platform allowlist / routing Dashboard allowFrom; relay logs Message yourself: complete silence Older builds dropped all traffic Update omnish + relay; send after upgrade`},{title:"Attached / platform mode",body:"Symptom: fails or errors with WebSocket Solutions: Reverse proxy must forward , , , , to relay control port (8788), not the HTTP edge (8787). See tunnel relay README. Run before . Confirm URL and token: . Symptom: Device shows online on dashboard but WhatsApp commands do nothing Solutions: Set allowFrom on the platform dashboard (attached mode uses platform policy, not local ). Check relay logs for with your device id. Only one should hold the device WebSocket per token. WhatsApp LID chats: ensure relay and CLI are up to date; use on the device and look for vs . with correct in logs but lists that number: the platform stores allowlist phones as digits only (CLI shows for display). Older CLI builds compared to raw digits and always denied \u2014 update omnish so normalizes platform entries. Symptom: Message yourself (or self-chat) \u2014 commands get no reply at all (not even \u201CNot allowlisted\u201D) Cause: WhatsApp marks messages you send from your phone as on linked devices. Older omnish builds ignored all upserts, so the documented Message yourself flow never reached the gateway. Solutions: Upgrade both the CLI ( ) and the platform relay ( ) to a version that tracks outbound message ids and routes allowlisted non-echo traffic. Confirm your number is on the platform allowlist (dashboard \u2192 Save allowlists). Run , then ; send or in Message yourself. Relay logs should show after you send a command. If you see nothing, WhatsApp may still be disconnected on the platform \u2014 check dashboard WhatsApp status. Standalone mode: same behavior applies; use and on the linked host. Symptom: warning at attach Solutions: Fix token, URL, or platform database (MongoDB on self-hosted relay). Until works, attached mode falls back to local allowlists. Full setup: Platform attached mode. Complete API/CLI/dashboard: Platform reference. WhatsApp link / unlink on platform Symptom Fix --------- ----- QR never appears Dashboard Link WhatsApp or ; check relay logs Stuck after logout Reconnect on dashboard or then link again Import fails Stop local ; use Linked but dashboard shows idle with autostart; ensure volume persists Allowlists and persistence Symptom Fix --------- ----- Dashboard does not remember allowlists Relay needs ; click Save allowlists CLI changes not reflected on device Wait up to 5 min or restart (policy refresh) Telegram token missing in UI Expected \u2014 token is not returned; leave field empty and Link to reuse saved token"},{title:"Installation Issues",body:"Native Module Build Failures Symptom: Build errors during Solutions: ```bash"}],keywords:["troubleshooting","guide","omnish","common","issues","and","solutions","for","quick","reference","attached","platform","mode","installation"],relatedCommands:["/help","/wa help","/tg help","/platform","/v1","/control","/dashboard","/auth","/contrib","/tunnel-relay","/readme","/me failed","/me","/guides","/platform-attached-mode"]},{id:"docs-architecture-communication-layer-model",path:"docs/architecture/communication-layer-model.md",title:"Communication layer and omnish CLI \u2014 unified model",summary:"Single source of truth for how omnish works today and how the optional hosted communication layer fits. Implementation sketches: Communication layer \u2014 API & ops, Gateway config precedence.",sections:[{title:"Summary",body:"CLI installs anywhere ( ) and works without the hosted layer (standalone mode). Optional hosted communication layer exposes WhatsApp, Telegram, and future surfaces through an API. Users link messengers once on the platform. Many devices (Docker, VM, VPS, bare metal) run the CLI with a device token and talk to that layer over a long-lived channel. One messenger connection \u2192 many devices: the platform routes chat to the right attached CLI; replies return through the same layer. That single connection is what makes the platform useful for fleets and containers. Shell execution stays on each device. The communication layer is not a remote shell; the CLI is the agent (shell, PTY, files, tunnels) on the box."},{title:"Two modes",body:"Mode When Where messengers connect CLI role ------ ------ -------------------------- ---------- Standalone No platform URL + token (default) On the same host as Full gateway: , , Attached + (or config / aliases) On the hosted communication layer Local executor + WebSocket client: register device, receive routed messages, send replies Both modes: allowlist and shell authority on the device. The platform may store policy in a dashboard, but the CLI enforces who may trigger commands before running shell on that host. Standalone (today) Documented in the README and Quick start. Baileys / Telegram bot clients run inside the gateway process; credentials live under . Attached (implemented) User links WhatsApp/Telegram on the platform dashboard (or ) \u2014 once per account. User sets platform URL + account token on each machine ( or env). On the target: , then . CLI opens WebSocket (fallback ), handles inbound messages like the standalone router, executes locally, posts replies. Setup: Platform attached mode. Full API/CLI/dashboard: Platform reference. No per-container WhatsApp QR is required for the default path."},{title:"Docker example (attached mode)",body:"A team runs an app in Docker and wants chat-driven ops inside that container without running Baileys in the image: Messengers stay on the communication layer; the container only needs outbound HTTPS to the layer API. See Docker gateway golden path."},{title:"Environment contract",body:"Purpose Canonical Also accepted --------- ----------- --------------- Platform base URL , Account token (tunnel + attached) , Optional device id in config Config file keys: ( ), ( ), ( ). Precedence: env \u2192 \u2192 tunnel auth file. Implementation: ."},{title:"Security boundaries",body:"Concern Standalone Attached --------- ------------ ---------- Who runs shell Local gateway Local CLI on device Messenger secrets on gateway host Communication layer (connectors) Stolen device token N/A Risk to that attach point; revoke in dashboard Platform sees message content N/A (direct to gateway) Yes, for routing \u2014 document retention and policy Remote shell from platform alone No No \u2014 requires valid device token + allowlisted sender on device Never echo full tokens in chat replies (same as tunnel tokens today)."},{title:"Relation to this repository",body:"Component Status ----------- -------- Standalone gateway ( , ) Implemented in Attached mode client ( , platform WebSocket) Implemented in , Hosted communication layer (relay + dashboard) Implemented in (deploy separately)"},{title:"See also",body:"Platform attached mode \u2014 connect, link, configure, run Platform reference \u2014 complete API, CLI, dashboard, persistence Vision \u2014 hosted communication layer \u2014 short product summary Platform layer spec \u2014 API sketch, tokens, threat model Gateway config precedence \u2014 config merge in attached mode Relay operator README \u2014 deploy, paths, dashboard docs/ideas/ \u2014 historical voice-note inputs"}],keywords:["communication","layer","and","omnish","cli","unified","model","single","source","of","truth","for","how","works","today","the","optional","hosted","fits","implementation","sketches","api","ops","gateway","config","precedence","summary","two","modes","docker","example","attached","mode","environment","contract","security","boundaries","relation","to","this","repository","see","also"],relatedCommands:["/readme","/guides","/quick-start","/telegram on","/platform","/device","/control","/platform-attached-mode","/cli","/dashboard","/platform-reference","/tunnel"]},{id:"docs-architecture-gateway-config-precedence",path:"docs/architecture/gateway-config-precedence.md",title:"Gateway configuration precedence",summary:"How effective configuration is computed. See Communication layer model for standalone vs attached modes.",sections:[{title:"Sources (ordered low \u2192 high priority)",body:"Standalone ( without platform token) Compiled defaults \u2014 values baked into the binary when a key is unset. Local \u2014 under . Environment variables \u2014 e.g. , . Chat \u2014 allowlisted senders; same trust as shell. Higher layers win per key. Attached ( with platform URL + token) Compiled defaults Local \u2014 host paths, shell, tunnel, jobs, etc. Platform account ( ) \u2014 wins for policy keys: , , (derived from linked connectors on the platform). Not written back to disk by default. Environment variables \u2014 still override local file for host keys; do not replace platform allowlists unless you also change them on the dashboard. Chat \u2014 updates local file only in attached mode; platform allowlists remain authoritative for inbound from messengers on the layer. If fails at connect, the CLI logs a warning and uses local for allowlists (or partial data from the WebSocket ack: + connectors only)."},{title:"Keys that stay host-only (never taken from platform)",body:", job limits, sync paths, webhook ports/tokens on the device Baileys auth paths, local tunnel client settings WhatsApp session blobs and Telegram bot tokens in attached mode (connectors run on the platform)"},{title:"Keys owned by the platform in attached mode",body:", \u2014 set on the dashboard; device enforces the platform copy on inbound WebSocket messages. \u2014 derived from which connectors are linked on the platform ( , , or )."},{title:"Merge algorithm (attached inbound)",body:"```text effective = loadConfig() # local host + defaults if platform snapshot loaded: effective.allowFrom = platform.allowFrom effective.telegramAllowFrom = platform.telegramAllowFrom effective.gatewayMode = platform.gatewayMode"}],keywords:["gateway","configuration","precedence","how","effective","is","computed","see","communication","layer","model","for","standalone","vs","attached","modes","sources","ordered","low","high","priority","keys","that","stay","host-only","never","taken","from","platform","owned","by","the","in","mode","merge","algorithm","inbound"],relatedCommands:["/v1","/me","/config","/guides","/platform-reference","/config set","/tokens on","/platform","/account-sync","/default-device"]},{id:"docs-architecture-overview",path:"docs/architecture/overview.md",title:"Architecture Overview - omnish",summary:"",sections:[{title:"Executive Summary",body:"omnish is a secure, deterministic messaging-to-shell gateway that bridges messaging platforms (WhatsApp, Telegram) to system shell access. The architecture follows a thin transport adapter pattern with a unified core, explicit security controls, and comprehensive session management. Local control plane: When the gateway process ( ) is up, it may expose a localhost-only control endpoint (metadata in ) so a separate process can request outbound file sends that reuse the same Baileys/grammY sessions\u2014avoiding a second login. This is optional machinery for CLI ergonomics; inbound chat traffic still flows through the transports above."},{title:"Key Architectural Principles",body:"Thin Transport Adapter Pattern Transport Layer: Platform-specific implementations (WhatsApp, Telegram) Adapter Layer: Normalizes all inputs to format Core Layer: Unified business logic and message routing Persistence Layer: Configuration and state management Deterministic Behavior No AI or agent layer Rule-based message routing Predictable command precedence Explicit state management Security-First Design Explicit allowlists required No anonymous access Per-peer session isolation Minimal attack surface"},{title:"System Architecture",body:"High-Level Diagram Component Responsibilities Layer Component Responsibility ------- ----------- ---------------- Transport WhatsApp WebSocket lifecycle, QR auth, media handling Telegram Long polling, bot API, formatting Adapter Inbound Normalize messages to Outbound Platform-specific message sending Core Gateway Transport multiplexing, lifecycle management Router Command dispatch with precedence rules Session Manager Per-peer working directories and state Apps Manager Interactive PTY session lifecycle Job Manager Background process execution Persistence Config Configuration loading and validation Sessions JSON-based state persistence Files Log files, session output, job output"},{title:"Data Flow Architecture",body:"Message Processing Pipeline Command Precedence Flow Messages are processed in strict precedence order: Free Shell Toggle: / Sync Commands: (e.g., ) System Commands: (e.g., , ) App Shorthand: (e.g., ) Free Shell Mode: Plain text execution Attached App: Forward to focused PTY session Help Fallback: Display help message"},{title:"Security Architecture",body:"Allowlist System Peer Key Model WhatsApp: or normalized phone number Telegram: Hashed for storage: SHA-1 prefix for log directories Session isolation: Each peer has separate state Security Boundaries Transport Boundary: Platform-specific authentication Authorization Boundary: Allowlist validation Execution Boundary: Command and process isolation Data Boundary: Per-peer state separation"},{title:"Session Management Architecture",body:"Session Context Persistence Session Lifecycle Load: From JSON storage or create default Update: Persist changes immediately Isolate: Per-peer separation with migration support Cleanup: Remove on session destruction"},{title:"Transport Architecture",body:"Transport Interface All transports implement the same core interface: Transport-Specific Features Transport Authentication Message Limit Features ----------- --------------- -------------- ---------- WhatsApp QR code 3500 chars Media download, auto-reconnect Telegram Bot token 4096 chars HTML formatting, webhook support"},{title:"Output Processing Architecture",body:"Output Flow Output Processing Pipeline Buffer: Collect output chunks Debounce: Wait for output completion ( ) Chunk: Split into transport-sized pieces Format: Strip ANSI, add prefixes, etc. Send: Deliver via appropriate transport"},{title:"State Management Architecture",body:"State Types Configuration: Global settings Session Context: Per-peer state Job State: Background job metadata App State: Interactive session state Persistence Strategy State Type Format Location Access Pattern ------------ -------- ---------- --------------- Config JSON Load at startup, update on change Sessions JSON Load on demand, append updates Jobs Files Streaming write, read on demand Apps Files Streaming write, read on demand"},{title:"Error Handling Architecture",body:"Error Categories Transport Errors: Connection issues, timeouts Validation Errors: Invalid input, malformed commands Execution Errors: Command failures, process timeouts Resource Errors: Memory limits, session limits Error Handling Flow Capture: Error detected at source Log: Structured logging with context Notify: User-friendly error message Recover: Graceful degradation where possible"},{title:"Extension Points",body:"Transport Extension Command Extension Configuration Extension"},{title:"Performance Considerations",body:"Memory Management Session Limits: Prevent unbounded growth Output Buffering: Size-limited buffers Cleanup: Proper resource disposal Network Optimization Message Chunking: Minimize API calls Connection Reuse: Persistent connections Backoff Strategy: Exponential backoff for failures I/O Optimization Async Operations: Non-blocking file I/O Lazy Loading: Load data on demand Output Streaming: Real-time output delivery This architecture overview provides the foundation for understanding omnish's design principles and implementation patterns."}],keywords:["architecture","overview","omnish","executive","summary","key","architectural","principles","system","data","flow","security","session","management","transport","output","processing","state","error","handling","extension","points","performance","considerations"],relatedCommands:["/grammy sessions","/inbound","/gateway","/outbound","/jobs","/apps","/command","/bg","/job","/pty","/config","/sessions"]},{id:"docs-architecture-platform-layer-spec",path:"docs/architecture/platform-layer-spec.md",title:"Communication layer \u2014 API and operations sketch",summary:"Implementation detail for the unified model in Communication layer model. The attached CLI client is implemented in and . The hosted relay (connectors, dashboard, routing) lives in .",sections:[{title:"Goals",body:"Messenger connectors on the layer: WhatsApp, Telegram, others \u2014 linked once per workspace from a dashboard. Device registry and routing: many CLIs attach with ; inbound chat is routed to the correct device; replies return through the layer. Token issuance and lifecycle from the dashboard (create, rotate, revoke). Minimal env on each device: + (legacy aliases: , , ). Non-goals: Running user shell or PTY in the cloud. Replacing local enforcement of allowlists before command execution on each device. Requiring the hosted layer for open-source standalone use ( on the host without env)."},{title:"Operating modes (messenger termination)",body:"Mode Messenger clients Device (CLI) ------ ------------------- -------------- Standalone On gateway host ( , Baileys / Telegram bot today) Same process as messengers Attached On communication layer (connectors) CLI only: API/WebSocket client + local shell See Communication layer model \u2014 Two modes."},{title:"Threat model (summary)",body:"Asset Risk Mitigation -------- ------ ------------ Device token Theft \u2192 control of one attach point Short TTL, rotation, revoke; scoped to one device slot Workspace / dashboard session Account takeover Strong auth, audit log on connector and device changes Message content on layer Privacy / retention Policy, encryption in transit, minimal logging; no training on content by default (product policy) Signed config blob Forged defaults on device Signing keys, , host-only keys; see Gateway config precedence Chat as secret channel Token pasted in DM Never echo full secrets in replies Trust boundary: The communication layer routes messages and holds connector credentials in attached mode. Each CLI enforces allowlists and runs shell locally. Layer compromise must not run shell on a device without a valid device token and allowlisted sender on that device."},{title:"Token scopes (recommended)",body:"Scope Purpose Typical lifetime ------- --------- ------------------ Long-lived channel for one CLI attach point Rotatable; per device One-time or short exchange when creating a device slot Minutes Fetch non-secret config subset for a device Hours\u2013days Relay bearer (may be issued by layer) Per relay policy Human dashboard (not for CLI) Session Rules: narrow scopes, stable token ids for revocation, no user-facing \u201Cgod\u201D token."},{title:"Implemented API (this repository)",body:"The relay implements a single-account model (not multi-workspace). Authoritative route list, request bodies, and CLI commands: Platform reference Summary: Area Implemented paths ------ ------------------- Auth , Account , , Devices , Telegram WhatsApp , , , Attach , UI"},{title:"Future API sketch (not implemented)",body:"The following were early design notes; do not assume they exist on the relay today: Multi-workspace ( ) Separate device tokens per slot (today: one account bearer token) , scoped bootstrap tokens SSE alternative to WebSocket"},{title:"Compliance and logging",body:"Attached mode: layer may process message content for delivery; document retention and access in privacy policy. Avoid logging message bodies in application logs by default."},{title:"Relation to this repository",body:"Communication layer service: (deploy separately; MongoDB + dashboard). CLI attached mode: , \u2014 WebSocket client + local shell. Standalone: / on the same host (unchanged when platform env is unset). Operator guides: Platform attached mode, Platform reference."}],keywords:["communication","layer","api","and","operations","sketch","implementation","detail","for","the","unified","model","in","attached","cli","client","is","implemented","hosted","relay","connectors","dashboard","routing","lives","goals","operating","modes","messenger","termination","threat","summary","token","scopes","recommended","this","repository","future","not","compliance","logging","relation","to"],relatedCommands:["/platform","/gateway","/attached","/tunnel-relay","/contrib","/guides","/platform-reference","/platform-attached-mode","/websocket client","/auth","/signup","/login"]},{id:"docs-architecture-routing",path:"docs/architecture/routing.md",title:"Message Routing Architecture - omnish",summary:"",sections:[{title:"Introduction",body:"The message routing system is the heart of omnish, responsible for determining how incoming messages are processed and executed. It follows a deterministic precedence model with clear rules for each message type."},{title:"Routing Overview",body:"Message Flow Core Components Allowlist Validator: Ensures user has permission Message Normalizer: Converts transport-specific formats Session Loader: Retrieves per-peer state Router: Applies precedence rules and dispatches Executors: Command-specific handlers"},{title:"Message Precedence Rules",body:"Precedence Hierarchy Messages are evaluated in strict order from highest to lowest precedence: Free Shell Mode Toggle ( / ) Synchronous Commands ( ) System Commands ( ) App Session Shorthand ( ) Attached App Sessions (when focused and running) Free Shell Mode (when enabled) Help Fallback Rule Implementation"},{title:"Command Types and Handling",body:"Free Shell Mode Toggle Purpose: Enable/disable direct shell execution Pattern: or Synchronous Commands Purpose: Execute shell commands immediately Pattern: System Commands Purpose: Control omnish functionality Pattern: App Session Shorthand Purpose: Quick interaction with app sessions Pattern: Free Shell Mode Purpose: Execute any text as shell command Pattern: Plain text (when enabled) Attached App Sessions Purpose: Send input to focused PTY session Pattern: Plain text (when session focused)"},{title:"Special Cases and Edge Handling",body:"Shortcut Expansion Commands starting with prefix are checked for shortcuts: Command Precedence Override Certain commands always take precedence: Error Handling"},{title:"Session Integration",body:"Session Context The router uses session context for state: Session Management"},{title:"Transport Integration",body:"Peer Key Normalization Transport-Specific Handling"},{title:"Performance Considerations",body:"Optimization Strategies Session Caching: Cache frequently accessed sessions Command Caching: Cache command results for identical inputs Output Debouncing: Buffer output to reduce message spam Lazy Loading: Load sessions only when needed Concurrency Handling"},{title:"Extension Points",body:"Adding New Command Types Custom Command Handlers"},{title:"Fleet and config slash commands",body:"The authoritative dispatch order for messages that start with is implemented in (not the pseudocode snippets earlier in this doc). Highlights: runs before fleet aliases so paths like never collide with . Fleet commands accept , , or ( ): only a standalone token or + rest matches \u2014 paths such as do not match the shortcut. (also , ) sets ; reapplies config while is active. See Cluster and chat configuration for behavior and Configuration guide for fields."},{title:"Testing and Validation",body:"Unit Tests Integration Tests This message routing architecture ensures predictable, efficient processing of all incoming messages while maintaining security and session isolation."}],keywords:["message","routing","architecture","omnish","introduction","overview","precedence","rules","command","types","and","handling","special","cases","edge","session","integration","transport","performance","considerations","extension","points","fleet","config","slash","commands","testing","validation"],relatedCommands:["/command","/disable direct","/new","/path","/router","/config","/config set","/computers","/pcs","/cluster","/gateway","/gw"]},{id:"docs-architecture-security",path:"docs/architecture/security.md",title:"Security model",summary:"omnish bridges WhatsApp and/or Telegram direct messages to shell commands on your machine. Security is explicit allowlists plus local filesystem and process boundaries. There is no cloud relay for commands: whoever can send messages as an allowed identity can execute code as the OS user running .",sections:[{title:"Trust boundaries",body:"Allowlisted identities are credentials. If an attacker can spoof or compromise an allowed WhatsApp number or Telegram user id, they get the same power as you gave on that host. Wildcards are rejected. must never contain . Secrets on disk: WhatsApp session material lives under the data directory ( / / legacy ). Telegram bot tokens live in or . Restrict permissions so other Unix users cannot read them. Chat config: lets allowlisted users edit from chat (same trust as running shell commands). Do not treat DMs as \u201Clow privilege.\u201D Multiple Telegram bots: If and several hosts run Telegram, use a different bot token per host; one token cannot be long-polled twice (see finding in ). Interactive terminal ( ): Same trust as running a shell on that machine. It is not gated by WhatsApp/Telegram inbound allowlists\u2014those lists still apply to messages arriving through the gateways. Gateway control ( ): Written only while is active; carries localhost connection info and a token so can request outbound media through the existing gateway. Anyone who can talk to 127.0.0.1 as your user could misuse it\u2014treat host-local access like filesystem access. : Sends files to arbitrary WhatsApp numbers or Telegram chats through your linked session, comparable to sending those files manually from the linked WhatsApp device or bot\u2014powerful and intentional. Tunneling ( , when ): Publishes public URLs to local HTTP/TCP ports. Anyone with the URL can reach the forwarded service; chat tunneling is gated by the same allowlists as shell commands."},{title:"Automated posture checks",body:"The same rules run in three places: Surface How -------- ----- CLI (plain text) or Chat , , Startup refuses to start if any error-severity finding is present; warnings are printed but do not block Automated checks cover configuration and local Unix permissions. They do not replace firewall rules, SSH hardening, malware scanning, or review of who physically accesses the machine. Severity levels error \u2014 Blocks until fixed (or until you change / token so the check no longer applies). warn \u2014 Shown on startup and in reports; you should understand and usually fix. info \u2014 Informational (for example, env var overrides)."},{title:"Finding codes and remediation",body:"Code Severity Meaning What to do ------ ---------- --------- ------------ error contains Remove from in . error Telegram transport enabled but no token Set in config or . error Configured binary does not exist Install the shell or point at a valid absolute path. error is but path empty Set or change mode. error Fixed receive path not absolute Use an absolute . warn WhatsApp enabled, empty warn Telegram enabled, empty warn is true Set to unless you trust every recipe. warn is not an absolute path Use e.g. in config. warn Could not stat Fix permissions/path. warn group/world readable or writable on config file. warn Process UID is root Run as a normal user when possible. warn Data directory group/world accessible on data dir ( or default ). warn Jobs directory group/world accessible (only checked if data dir is already tight) on the jobs directory under the data dir. warn WhatsApp auth dir readable by others on under the data dir. info set; overrides config Unset env if you want config file to win. info Cluster + Telegram: same token must not be used on two running gateways Use one bot per host or run Telegram on fewer machines. Unix permission checks are skipped on Windows."},{title:"JSON output for automation",body:"prints: Exit code if any error-severity finding exists (same as plain text)."},{title:"Related commands",body:"\u2014 Includes a one-line security summary. In chat: for a short hardening checklist. See also the user guide Security section in User Guide."}],keywords:["security","model","omnish","bridges","whatsapp","and/or","telegram","direct","messages","to","shell","commands","on","your","machine","is","explicit","allowlists","plus","local","filesystem","and","process","boundaries","there","no","cloud","relay","for","whoever","can","send","as","an","allowed","identity","execute","code","the","os","user","running","trust","automated","posture","checks","finding","codes","remediation","json","output","automation","related"],relatedCommands:["/security help","/security summary","/or telegram","/config set","/telegram","/sendto","/tunnel","/tcp ports","/security","/security tips","/run","/bin","/bash"]},{id:"docs-features-background-jobs",path:"docs/features/background-jobs.md",title:"Background jobs \u2014 omnish",summary:"Background jobs run a single shell command asynchronously in the current chat\u2019s working directory while you keep using the chat. Output is appended to a log file under your data directory; you can pull the last N lines or only new bytes since your last for that job in this chat.",sections:[{title:"Commands (implemented)",body:"Command Meaning -------- --------- Start a background job; reply includes an 8-char job id and hints for and . \xB7 \xB7 Same as , but stores a name you can use instead of the hex id with , , and . \xB7 Start a job that sends a completion notification to your chat when it exits (includes exit code, duration, and command summary). Combine flags: named job with completion notification. List recent jobs (up to 20), newest first: id (and name if set), status ( \\ \\ ), exit code, duration, command preview. Last lines of the log (default from ; max 500 if you pass a number: or ). Incremental: new log bytes since your last of this job (resolved id) in this chat (per-chat cursor). Send SIGTERM to the running process (or best-effort to the recorded pid if the child already detached). Job ids are 8-character hex (e.g. ). Names are optional labels (letters, digits, , , , up to 64 characters). If several jobs share the same name, / / resolve to the newest (most recently started) matching job. Resolving -shaped tokens: if a job with that id exists on disk, the token is treated as an id; otherwise it is treated as a name (so a name that looks like 8 hex digits still works when no job file uses that id). There is no , , or in chat \u2014 use and / instead. ( exists for gateway shutdown; it is not exposed as a slash command.)"},{title:"Behavior details",body:"Working directory: Same as session cwd ( applies before ). Environment: Inherited from the omnish gateway process (the Node process running ), not from a prior in chat \u2014 each and runs a new shell. Set env vars in the shell that starts the gateway, or put them in the job command line. Timeouts: applies to synchronous commands, not to children. Long-running jobs are not auto-killed by that setting. Logs: and (see )."},{title:"Configuration",body:""},{title:"Job lifecycle (actual statuses)",body:"From : running \u2014 child spawned; log growing. done \u2014 process exited (or spawn error); / recorded in meta. killed \u2014 user ran (SIGTERM); meta updated. Optional field is stored in when you start a job with / ."},{title:"Completion notifications",body:"When you start a job with (or ), the gateway sends a message to your chat as soon as the process exits: The notification includes: Exit code (0 for success, non-zero for failure) or signal name (e.g. SIGTERM) Duration of the run Command that was executed This is useful for long builds, deployments, or test suites where you want to walk away and get notified when it finishes. Combine with for readability: Notifications are sent via the same transport as the chat (WhatsApp or Telegram). If the gateway shuts down before the job finishes, the notification is lost."},{title:"Examples",body:"Same using the printed id:"},{title:"Compared to Cowork",body:"--- -------- ----------- Scheduling One-shot , , , etc. Notifications Opt-in per job ( ) Per task ( + ) Queue / catch-up No Yes (SQLite + pending queue) Typical use Ad hoc long command Recurring or on-demand saved tasks See cowork.md."},{title:"Troubleshooting",body:"No output in chat: Streaming behavior depends on the gateway; use or for the log file. Lost jobs on gateway restart: In-memory handles are cleared; log and meta files on disk may still be present under . Verbose gateway logs: ---"},{title:"Change log \u2014 named background jobs (2026-05-08)",body:"Area Change ------ -------- ; , , , ; . Parse named ; , , accept id or name via ; lists name when set. help and main help bullet updated for names. Unit tests for parsing, validation, and name/id resolution. User-facing docs This file, user-guide.md, quick-start.md, README.md, comprehensive-documentation.md, practical-guide-for-agents.md, CHANGELOG.md."}],keywords:["background","jobs","omnish","run","single","shell","command","asynchronously","in","the","current","chat","working","directory","while","you","keep","using","output","is","appended","to","log","file","under","your","data","can","pull","last","lines","or","only","new","bytes","since","for","that","job","this","commands","implemented","behavior","details","configuration","lifecycle","actual","statuses","completion","notifications","examples","compared","cowork","troubleshooting","change","named","2026-05-08"],relatedCommands:["/bg","/jobs","/tail","/log","/kill","/log abcdef12","/log mybuild","/stats","/history","/kill all","/shell","/src"]},{id:"docs-features-chat-llm-fallback",path:"docs/features/chat-llm-fallback.md",title:"Chat LLM fallback (optional)",summary:"When this feature is on, a plain inbound message that would normally get \u201CNo command matched\u201D is handled silently: omnish runs your in the background (with limits and a restricted environment), then sends the subprocess output back to the same WhatsApp or Telegram chat. In , the reply is printed to the terminal instead.",sections:[{title:"Where to change settings",body:"What Where ------ -------- Config file . If is unset, omnish uses (or the legacy tree if does not exist). Template / all keys Repo root \u2014 copy the entries into your real . Confirm data directory Run on the gateway host, or . Edit the file with any text editor on the gateway host (SSH, local console, or your usual config workflow). Valid JSON is required (trailing commas are not allowed). Reload behavior: The gateway calls when handling each inbound message, so changes to fields in normally apply on the next message without restarting . API keys and environment variables: The subprocess receives a filtered copy of the gateway process environment (see below). Keys such as are only present if they were set when the gateway was started. If you add or change API keys in the shell profile or systemd unit, restart so the parent process picks them up. ---"},{title:"How to enable (step by step)",body:"Open (or ) on the machine that runs . Add or merge these fields (defaults shown; enable only when ready): Set to a single shell command passed as the argument to (same pattern as sync commands). Use an absolute path to a wrapper script if the default in the sandbox is too minimal. Smoke test: With the gateway running, send a plain line from an allowlisted chat (not starting with or your , not text). You should get no immediate \u201CNo command matched\u201D reply; after the subprocess finishes, the combined stdout/stderr (trimmed and capped) is sent back. Sandbox / real isolation: omnish runs the command in a dedicated working directory (temp dir under the data directory unless is set) and a reduced env. For Docker, , or other tools, put that logic inside your wrapper and point at it. ---"},{title:"What the subprocess receives",body:"Shell: Config key (e.g. ). Working directory: if non-empty (created if needed); otherwise a new temp directory under the omnish data dir for that run (removed afterward). Stdin (default): The inbound chat text (truncated to ). If is true, stdin is not piped the same way; use (and a PTY) instead. Environment (high level): \u2014 e.g. or \u2014 same payload as stdin (piped mode) Common vars: , , , , , , , , All vars from the parent process Parent vars whose names end with or Place API keys in the gateway environment (systemd , shell before , etc.) so they are inherited and forwarded by the allowlist above. ---"},{title:"When the fallback runs (and when it does not)",body:"Runs only if all of the following hold: is true and is non-empty after trim. The message is plain text that reached the router\u2019s final \u201Cno match\u201D branch: not a command, not shell, not / , not app line, not consumed by a focused session, and free shell mode is off for that peer. Does not run (examples): unknown , , , media-only messages, messages dropped by cluster binding on non-primary hosts, or any path that already returns a normal reply. ---"},{title:"Related keys (reference)",body:"Key Purpose ----- -------- Master switch ( by default). Full command string for . Wall-clock limit (ms). Max size of text passed in (stdin / ). Max captured output (stdout+stderr in piped mode). Use PTY execution (for CLIs that require a TTY); prefer in the script. Fixed cwd; empty = ephemeral dir per run. Implementation checklist and code pointers: chat-llm-fallback-implementation-plan.md. ---"},{title:"Security note",body:"Allowlisted chats already have shell-level trust on the gateway host. Turning this on means accidental plain messages can invoke your command and any API usage inside it. Keep allowlists small, use timeouts and caps, and wrap external agents in a policy you control."}],keywords:["chat","llm","fallback","optional","when","this","feature","is","on","plain","inbound","message","that","would","normally","get","no","command","matched","handled","silently","omnish","runs","your","in","the","background","with","limits","and","restricted","environment","then","sends","subprocess","output","back","to","same","whatsapp","or","telegram","reply","printed","terminal","instead","where","change","settings","how","enable","step","by","what","receives","it","does","not","related","keys","reference","security","note"],relatedCommands:["/config help","/config","/absolute","/path","/to","/your-wrapper","/stderr","/bin","/bash","/apps","/foo","/help","/chat-llm-fallback-implementation-plan"]},{id:"docs-features-cluster-and-chat-config",path:"docs/features/cluster-and-chat-config.md",title:"Multi-host cluster and chat configuration",summary:"This document describes the chat-driven cluster ( , , ) and for viewing or editing from WhatsApp or Telegram.",sections:[{title:"Cluster (per-sender bindings)",body:`Use one WhatsApp phone number with multiple linked devices \u2014 each machine runs once. Every machine that runs will receive the same DMs through WhatsApp multi-device. There is no shared file. There is no Syncthing/NFS to set up. Coordination flows entirely through the WhatsApp chat itself: every reply omnish sends carries an invisible footer that other linked omnish hosts read to learn who is online and which machine each sender is currently bound to. Telegram cannot carry this coordination (different bot tokens cannot see each other), so the per-sender binding still applies on Telegram, but you only get convergence between linked hosts on WhatsApp. How it routes Each allowlisted sender (one phone number / one Telegram id) picks one machine to talk to. Only that machine processes the sender's normal traffic; other linked hosts stay silent for that sender. Two different controllers can independently bind to two different machines without affecting each other. A's only runs on . B's only runs on . Neither sees the other's output. Fleet commands ( , , ) are processed by every linked host so that bindings, status, and help stay reachable even before a sender has bound. Stable identity Each data directory has a file ( ) with a UUID. Display names in the roster come from , or the OS hostname when empty. When you , "alpha" matches against the (or the 8-character node id). Local state Each host keeps a private file at (schema v3): \u2014 short id, label, role, and last-seen timestamp for every host this machine has observed in chat traffic. \u2014 map of sender key ( or ) to the bound machine's short node id, when it was set, and whether it came from a chat command ( ) or config defaults ( ). This file is per host and never shared. Disagreements between linked hosts are resolved by the next footer-stamped message anyone sends to the chat. Commands (WhatsApp / Telegram) may be shortened to or (the bare token matches; does not). Command Action --------- -------- Overview of the per-sender model Bind YOUR messages to that machine. Resolves either an 8-character node id or a . Other senders are not affected. Bind YOUR messages to the local machine (shorthand for ). Show YOUR current binding (machine, source: chat or config). Clear YOUR chat binding. The config default in (if any) takes over again. Every online host replies with its own one-paragraph status (this is the discovery moment \u2014 each host parses the others' footers and updates its peer list). Locally known roster \u2014 only one host responds (the bound one, or a deterministic fallback). Convergence When you send , every linked host runs locally. Only the resolved target host (the one labelled ) sends the confirmation reply; siblings stay silent. The reply carries the cluster footer with , and every sibling observes it via WhatsApp upserts and converges on the same binding for your sender key. Defaults via config You can pre-seed bindings so a controller never has to type on first contact: Keys are sender keys ( or ). Values are the target machine's or its 8-character node id. Chat-set bindings ( ) always win over config defaults; clearing a chat binding ( ) falls back to the config default. CLI prints a short cluster line when is true. The legacy was removed \u2014 bindings need a sender, so use the chat ( ) or the new CLI form. Configuration keys (cluster) See the Configuration guide. Relevant fields: \u2014 display name for THIS machine in the roster \u2014 sender \u2192 machine defaults \u2014 kept for backwards compat, but no longer used for traffic gating Migration from v2 (single global active host) is automatically migrated on first read. The previous field is dropped \u2014 every sender now binds individually. If you relied on the old global-active model, set the equivalent entry for each allowlisted sender in , or send from each controller's chat. The footer protocol on the wire is unchanged; the field is now interpreted as "the node bound for this chat" rather than "the globally active node." ---`},{title:"Chat configuration (`/config`)",body:"Allowlisted chat users can change many settings without SSH. This is as sensitive as shell access: anyone who can DM as an allowed identity can modify config. Commands Command Action --------- -------- or Help (overview + hint) Snapshot of effective config; telegram bot token masked One whitelisted key Update one key; values may be quoted for paths with spaces List all keys allowed for After changes Save goes to immediately (same normalization as the rest of the app). For or , the gateway runs -style Telegram restart when is up; otherwise send after editing. Changing returns an explicit warning in the reply. Whitelist ( ) The canonical allowlist is in . In chat, prints the same comma-separated list your build supports. Groups include: gateway ( , \u2026); cluster; shell / sync / jobs; apps; files; recipes (including , ); Telegram ( ); service ( ); update checks ( , , , ); tunneling ( , , ); chat LLM fallback ( , , , , , , ). accepts a JSON object value (or / to wipe), e.g. . Not exposed via : , \u2014 use and in chat (or / on the host). Tunnel token is not in : run on the gateway host (or set ). Chat can turn tunneling on with after the token exists. Examples ---"},{title:"Related documentation",body:"Configuration guide \u2014 full file layout and fields Security model \u2014 trust boundaries and Message routing \u2014 slash-command ordering (high level)"}],keywords:["multi-host","cluster","and","chat","configuration","this","document","describes","the","chat-driven","for","viewing","or","editing","from","whatsapp","telegram","per-sender","bindings","/config","related","documentation"],relatedCommands:["/c help","/config help","/computers","/pcs","/config","/nfs","/node-id","/c use","/cluster-local","/cluster","/c here","/c using","/c unuse"]},{id:"docs-features-cowork",path:"docs/features/cowork.md",title:"Cowork \u2014 scheduled and on-demand shell tasks",summary:"Cowork runs saved shell commands on a timer while is active. It uses the same execution model as sync commands ( via your configured ), with and from . There is no AI layer.",sections:[{title:"Prerequisites",body:"A running gateway: (foreground or background). You must be allowlisted (same trust as ). With cluster enabled, only the host bound to your sender processes chat commands; tasks you create are stored on that machine\u2019s data directory."},{title:"Commands",body:"Use or . Send for the short form. Action Example -------- --------- Add task List Show Run now (on-demand or scheduled) Enable / disable / Remove (also , ) Check in (heartbeat only) Update \u2014 notify condition (see below) \u2014 also send the run log file with each notify \u2014 optional artifact paths (see below) Add syntax Scheduled / on-demand tasks (run a command): Heartbeat tasks (dead-man's-switch, no command): Name: letters, digits, , ; max 32 characters (stored lowercased). Schedule: (or ), , , , (weekday names or \u2013 , Sunday = ), or . For scheduled/on-demand tasks, the command must come after a literal separator. Heartbeat tasks have no command. Duration format (heartbeat): , , , . Minimum interval: 1m. Minimum grace: 30s. Grace defaults to 50% of interval if omitted. Schedules and time zones Fire times use the gateway host\u2019s local timezone (Node\u2019s on that machine). Output logs Each run writes one UTF-8 log file under the task\u2019s (default ). Filenames include a timestamp, task id, and whether the run was scheduled, catch-up, or on-demand. Catch-up Successful scheduled runs are recorded in (per task id and slot time). The scheduler uses that database as the source of truth for what is already done; may still contain a legacy field, which is seeded into SQLite on first open if the DB has no rows for that task. If several slots were missed (gateway down), the next tick runs the command once for the newest due slot and records coalesced rows for older missed slots so the backlog clears in a single execution instead of one run every 30 seconds per missed slot. A slot is only recorded after exit code 0, no timeout, and no terminating signal; failed runs do not advance the watermark (same retry behavior as before). Kinds stored for successful scheduled work: (within 2 minutes of the slot), (late), (satisfied without a separate run during backlog coalescing), and (imported from legacy on upgrade). On-demand queue appends a request to . The scheduler dequeues up to eight entries per tick, one read/write of the queue file per batch, so a crash mid-run does not drop the rest of the queue. Notifications After each run, a short summary can be sent (same routing as other replies: WhatsApp vs Telegram formatting). For scheduled and catch-up runs, the message is one line: The bracketed status appears only when the run did not finish cleanly (timeout, non-zero exit, or signal). For on-demand runs ( ), the message is two lines: Optional lines may follow when attachments fail or when too many artifact files match (see below). Mode Recipients ------ ------------ (default) Task owner only All entries in (WhatsApp) All entries in Owner plus both allowlists No messages Trust: , , and can message every allowlisted identity. Anyone who can edit tasks (allowlisted senders) can point shell and notifications at powerful actions\u2014same overall model as remote shell. WhatsApp notification targets use normalized JIDs so delivery matches Baileys . Conditional notifications ( ) Control when a notification is sent with : Mode Behavior ------ ---------- (default) Notify after every run Notify only when the command exits with a non-zero code, times out, or is killed by a signal Notify only when the result flips (e.g. ok to fail, or fail to ok). The last-known state is tracked in SQLite so it survives gateway restarts. is useful for tasks that run frequently (e.g. hourly health checks) where you only want to know when something breaks or recovers, not on every successful run. Heartbeat (dead-man's-switch) A heartbeat task does not run a command. Instead, it expects periodic check-ins and alerts when they stop arriving. This creates a task that expects a check-in at least every 1 hour, with a 10-minute grace period. If no check-in arrives within 1h10m of the last one, a missed-heartbeat alert is sent. Check in from chat or from a script: From a script (e.g. at the end of a cron job or backup pipeline): Recovery: when check-ins resume after a missed heartbeat, a recovery "},{title:"Data files",body:"Under (default unless overridden): \u2014 task definitions (atomic replace on save). \u2014 successful scheduled slot completions (watermark for catch-up); WAL mode. \u2014 queued on-demand runs. Directory mode is restrictive ( for where created by the app; task file )."},{title:"Limitations and caveats",body:"Gateway must be up at fire time for scheduled runs; catch-up handles downtime afterward. WhatsApp-only: the cowork scheduler starts before the first successful WhatsApp connection; very early completion notifications to WhatsApp may no-op until outbound is ready. Telegram outbound is typically ready earlier when the bot is enabled. Shared : if several gateways share the same data directory (unusual), each running process could execute the same schedules\u2014use one data dir per machine for normal setups. Cluster: tasks live on disk for the gateway that received commands; they do not sync across hosts."},{title:"See also",body:"Cowork implementation plan (design and maintainer notes) User guide \u2014 Cowork section Security model"}],keywords:["cowork","scheduled","and","on-demand","shell","tasks","runs","saved","commands","on","timer","while","is","active","it","uses","the","same","execution","model","as","sync","via","your","configured","with","from","there","no","ai","layer","prerequisites","data","files","limitations","caveats","see","also"],relatedCommands:["/cowork help","/cw help","/cowork","/cw","/cowork add","/data","/backup","/cowork list","/cowork show","/cowork run","/cowork enable","/cowork disable","/cowork remove"]},{id:"docs-features-device-update-delivery",path:"docs/features/device-update-delivery.md",title:"Update information on installed Omnish devices",summary:"This document is the reference plan for how operators and maintainers can reach already-installed Omnish gateways with version and notice information. It matches what the CLI and gateway implement today.",sections:[{title:"Reality check: there is no silent \u201Cpush\u201D to every machine",body:"Omnish is self-hosted: each install is a process on a user\u2019s machine, talking to WhatsApp/Telegram. There is no central fleet server and no always-on back channel from the project to those hosts unless the host initiates outbound traffic (or an allowlisted user sends a chat command). So 100% delivery in the literal sense (every device, every time, with no user action) is not possible: machines can be offline, firewalled, on air-gapped networks, or running an old binary forever. What is achievable is a reliable, predictable path that works whenever the network and registry are reachable\u2014same practical bar as other CLIs ( , , etc.)."},{title:"Options that were considered",body:"Approach Pros Cons ---------- ------ ------ npm registry Canonical published version; no custom infra; HTTPS; already used for installs Needs outbound HTTPS; scoped/unpublished forks differ GitHub Releases / API Rich metadata Rate limits; not identical to \u201Cwhat gets\u201D Static JSON on a URL you control ( ) Arbitrary maintainer text (security, migration) You must host and secure expectations (HTTPS only in omnish) In-chat broadcast from \u201Cthe project\u201D Uses existing DM surface There is no project-owned chat to all installs; only allowlisted users can command their host Auto-upgrade in place Hands-off for users High risk (native deps, , Baileys); out of scope for this design doc Chosen combination: npm registry for semver discovery + optional HTTPS JSON for human notices, exposed in the product as , (cached one-liner), , optional background checks when is true, and for the same keys."},{title:"Implemented behavior (source of truth)",body:"(allowlisted chat, gateway running): performs a live GET to (default package name ). If is set to an https URL, fetches JSON (link must also be ). : shows the last in-memory snapshot from the last live or scheduled check (no network). : gateway runs a timer (checks at most every 1 minute internally, but only performs a registry+info fetch when has elapsed, clamped 1h\u20137d). Results are logged with and stored for / . : CLI one-shot check (same fetches as ). : completion text may append \u201CUpdates (last check): \u2026\u201D if a snapshot exists. Configuration keys (also in and ): (boolean, default false) \u2014 privacy-first default. (default 86400000) \u2014 clamped between 1 hour and 7 days. (default ) \u2014 for forks or scoped packages. (default empty) \u2014 optional maintainer notice JSON over HTTPS."},{title:"Maintainer playbook",body:"Publish a new version to npm as today ( bump, publish). Devices that run or have background checks enabled will see the new latest when the registry updates. Optional notice (security advisory, breaking change): host a static JSON file at an HTTPS URL you control; set in docs or tell users to set it via . Do not rely on chat alone to \u201Creach\u201D every install; treat registry + optional URL as the scalable channel."},{title:"Code map",body:"Piece Role ------- ------ npm + optional info URL fetch, snapshot, scheduler Numeric compare for \u201Cnewer on npm\u201D Reads running from package root (dev + bundled ) , Schedules checks; ; reload footer status lines, formatted reply Schema defaults and merge"},{title:"Future extensions (not implemented)",body:"Signed notices (e.g. minisign) over . Deprecation warnings via (visible on , not parsed here)."}],keywords:["update","information","on","installed","omnish","devices","this","document","is","the","reference","plan","for","how","operators","and","maintainers","can","reach","already-installed","gateways","with","version","notice","it","matches","what","cli","gateway","implement","today","reality","check","there","no","silent","push","to","every","machine","options","that","were","considered","implemented","behavior","source","of","truth","maintainer","playbook","code","map","future","extensions","not"],relatedCommands:["/updates","/updates cached","/telegram","/latest","/unpublished forks","/gateway","/config set","/registry","/reload","/config keys","/omnish-notice","/check"]},{id:"docs-features-docs-search-from-chat",path:"docs/features/docs-search-from-chat.md",title:"Documentation search from chat",summary:"Find omnish guides by topic from WhatsApp, Telegram, or \u2014without hunting the repo or omnish.dev first. Search is offline (bundled index at build time; no AI layer).",sections:[{title:"Commands",body:"Command Purpose --------- --------- Subcommand list Ranked results (keywords + headings) Repeat the last search list in this chat or Excerpt, GitHub link, and Try: related slash commands Run the primary related help (e.g. ) Alias for Host terminal (same index): ---"},{title:"Example flow",body:"You are not sure which command exposes HTTP: Pick a result: You get a short excerpt, the doc path, a GitHub link, and lines like . Jump to live help: Same as sending in that chat. ---"},{title:"When nothing matches a slash command",body:"Plain text that looks like a question (contains a space or ) gets a hint on No command matched: Use with your own keywords if the auto-filled phrase is too long. ---"},{title:"Index scope",body:"The build includes guides, features, architecture, and advanced troubleshooting under (not , , or maintainer runbooks). Rebuild the index with: ( and run this automatically.) Overrides for related commands: . ---"},{title:"Related",body:"User guide Message routing Online catalog \u2014 community recipes/apps (separate from docs search)"}],keywords:["documentation","search","from","chat","find","omnish","guides","by","topic","whatsapp","telegram","or","without","hunting","the","repo","dev","first","is","offline","bundled","index","at","build","time","no","ai","layer","commands","example","flow","when","nothing","matches","slash","command","scope","related"],relatedCommands:["/docs help","/docs search","/docs list","/docs","/docs show","/docs follow","/tunnel help","/help search","/features","/tunneling","/doc-chat-overrides","/scripts"]},{id:"docs-features-implementation-101",path:"docs/features/implementation-101.md",title:"Tunneling implementation 101",summary:"This document explains how omnish tunneling is built: relay edge, client, CLI, chat integration, configuration, security, and how to exercise it locally.",sections:[{title:"Big picture",body:"Tunneling is a relay + client design. The relay is the public edge; the omnish client on the user machine keeps an outbound WebSocket and forwards traffic to a local port. HTTP uses JSON control messages on the WebSocket. TCP uses JSON for stream open/close and length-prefixed binary frames for payload. Default production relay: . The relay service itself lives under and is deployed separately from the npm CLI package."},{title:"Repository map",body:"Path Role ------ ------ Deployable relay: HTTP edge, WSS control, TCP listeners Relay package ( dependency) Shared control message types and binary frame codec Tunnel kinds, records, default relay URL One tunnel: WSS session, local forwarding Active tunnels, limits, stop/stopAll Token and relay URL resolution CLI and chat argument parsing subcommands handlers for the gateway CLI dispatch; gateway shutdown stops tunnels Chat routing for and , , under the data dir Posture findings for tunneling lines when tunneling is enabled Tests: , , , ."},{title:"Relay (`contrib/tunnel-relay/`)",body:"is the tunnel process. Production ships it in one Docker image with Caddy (TLS, wildcard DNS-01) via and . proxies public / to and . Listeners Listener Default Role ---------- --------- ------ HTTP edge Public HTTP for tunneled apps Control WSS Authenticated client connections Environment variables: \u2014 base URL shown to users (default ) \u2014 HTTP edge bind port \u2014 control WebSocket bind port / \u2014 TCP tunnel port range \u2014 comma-separated bearer tokens allowed to connect \u2014 per-token tunnel quota Authentication Clients connect with on the WebSocket upgrade. Invalid or missing tokens close the socket. Registration After , the client sends with ( ), , , and optional . The relay assigns a slug (from or random) and replies with including . HTTP routing Local dev: Production-style: when matches that host pattern Fallback: Each slug is bound to the WebSocket session that registered it (not \u201Cany session with the same token\u201D), so multiple tunnels sharing one token each get correct routing. Incoming requests are turned into control messages to the owning client; the client returns . TCP For , the relay binds a port in the configured range and returns . New public TCP connections emit on the control socket; bytes flow over binary frames. State In-memory maps ( , , ). Tunnels disappear when the client disconnects. No cross-replica sticky routing in v1."},{title:"Shared protocol (`src/tunnel/protocol.ts`)",body:"Control messages (JSON on the WebSocket): Session: , , Lifecycle: , , , HTTP: , (optional ) TCP: , Keepalive: , Binary frames (TCP payload): 1 byte frame type ( , , , ) 4 byte stream id (big-endian) 4 byte payload length payload bytes and implement the codec on client and relay."},{title:"Client (`src/tunnel/client.ts`)",body:"represents one active tunnel. Derives from the relay URL ( \u2192 , default path ). Connects with the bearer token. Sends with a random 8-hex . Waits for and stores and . HTTP path: On , issues to , then sends with status, headers, and optional base64 body. TCP path: On , connects locally and pumps bytes via binary frames; or relay close tears down the stream. Lifecycle: Periodic ; on stop, sends and closes the socket. Default target host is unless overrides it."},{title:"Manager (`src/tunnel/manager.ts`)",body:"holds live instances keyed by tunnel id and slug. Resolves relay URL via and token via Enforces from config / for CLI, chat, and gateway shutdown The gateway and CLI share one manager instance from ( )."},{title:"Configuration and secrets",body:"In (non-secret): \u2014 gate chat commands (default ) \u2014 default relay origin (default ) \u2014 max concurrent tunnels on this host (default ) Secrets (not in ): or ( ) via Optional relay override: or in the auth file See and ."},{title:"CLI (`omnish tunnel`)",body:"Implemented in ; wired from . Subcommand Behavior ------------ ---------- Save token (and optional relay) to Remove saved token Register HTTP tunnel; foreground unless Register TCP tunnel List active tunnels on this machine Stop one tunnel Relay reachability and auth presence Flags: , , , (see )."},{title:"Chat integration",body:"When is true, handles: / (optional , ) delegates to the shared . Tunnels run inside ; standalone still works without the gateway. Trust model matches : allowlisted chat users can open tunnels as the gateway OS user. Public URL possession is the visitor credential."},{title:"Security",body:"findings: \u2014 chat tunneling on \u2014 chat tunneling on without a token \u2014 non-default documents tunnel URLs as capabilities and the gateway shutdown path that stops active tunnels."},{title:"Local exercise",body:"Install relay deps: Start relay, for example: - - Run a local HTTP server on a port (for example ) Open the printed (path-based on loopback: ) Automated coverage: spins the relay and asserts HTTP end-to-end."},{title:"Out of scope (v1)",body:"Mandatory omnish.dev account or billing Tunnel persistence across client disconnect or HA relay fleet UDP, mesh VPN, or bundled ngrok/cloudflared as the primary path Setup UI for tunnel login (CLI is the v1 configuration surface)"},{title:"Success criteria (from product plan)",body:"+ yields a public URL that serves the local app (with a running relay) exposes a public TCP endpoint to the local port With and the gateway running, allowlisted users get the same URLs from chat and docs describe capability risk clearly"}],keywords:["tunneling","implementation","101","this","document","explains","how","omnish","is","built","relay","edge","client","cli","chat","integration","configuration","security","and","to","exercise","it","locally","big","picture","repository","map","contrib/tunnel-relay/","shared","protocol","src/tunnel/protocol","ts","src/tunnel/client","manager","src/tunnel/manager","secrets","tunnel","local","out","of","scope","v1","success","criteria","from","product","plan"],relatedCommands:["/tunneling","/architecture","/security","/close and","/tunnel","/tunnel-relay","/server","/contrib","/package","/protocol","/src","/types"]},{id:"docs-features-media-pull",path:"docs/features/media-pull.md",title:"Media pull (`/pull`)",summary:"Download audio, video, subtitles, or a Whisper transcript from a URL in chat \u2014 using yt-dlp, ffmpeg, and optionally openai-whisper on the gateway host.",sections:[{title:"Enable",body:"In (or from an allowlisted chat):"},{title:"Install tools (host)",body:"Binaries are stored under . Whisper uses . From chat (same trust as shell \u2014 off by default):"},{title:"Chat commands",body:"Command Action --------- -------- Usage Tool status OS-specific manual install steps Best video (needs ffmpeg) Audio extract (m4a) Subtitles (en + auto) Whisper speech-to-text video + audio + subs + transcript (background job) Flags: / \u2014 run as job / \u2014 notify in chat when the background job finishes and always run in the background. / / run synchronously unless is set. Auto-detect URLs When is true, a message that is only an or URL runs with (default ). Send results back to chat When is true, files under (and the 8 MiB attached-mode cap) are sent with automatically. Otherwise the reply lists paths \u2014 use manually."},{title:"Config keys",body:"Key Default Purpose ----- --------- --------- Master switch Allow Lone URL \u2192 pull Default mode Output root (empty \u2192 ) yt-dlp cap (0 = none) Push files to chat Whisper model / / Binary overrides"},{title:"Legal and safety",body:"Same allowlist trust as shell commands. Private/local URLs are blocked ( , RFC1918, etc.). Respect copyright and platform terms; omnish only runs tools you install locally."},{title:"See also",body:"Files send/receive \u2014 after a pull Background jobs \u2014 , ,"}],keywords:["media","pull","/pull","download","audio","video","subtitles","or","whisper","transcript","from","url","in","chat","using","yt-dlp","ffmpeg","and","optionally","openai-whisper","on","the","gateway","host","enable","install","tools","commands","config","keys","legal","safety","see","also"],relatedCommands:["/pull","/config","/config set","/bin","/venvs","/whisper","/pull install","/pull help","/pull doctor","/pull setup","/pull video","/pull audio"]},{id:"docs-features-monetization",path:"docs/features/monetization.md",title:"Monetization \u2014 device-first omnish",summary:"omnish keeps WhatsApp and Telegram as the free control plane: allowlisted messages run on the user\u2019s machine. Paid tiers charge for reachability, identity, team boundaries, and safety nets\u2014not for sending a command.",sections:[{title:"Who pays at ~$10/month",body:"Someone who already runs on a home server, laptop, or Mac mini and wants the inbox to stay useful when they are away from the desk: show a dev server, start a long job, drive a TUI agent, tail logs, or hand a link to someone else. Free omnish sells my phone is a remote control for my computer. Paid omnish sells that control still works when I need a stable public URL, more than one tunnel, or light team structure\u2014without being my own ngrok admin."},{title:"Strongest first paid wedge",body:"Hosted tunneling on , integrated with the same chat and CLI, is the most natural first paid feature. Execution stays on their box; omnish runs the edge they would otherwise assemble (TLS, wildcard subdomains, relay uptime, abuse limits). For agent builds on the user\u2019s machine, steered from chat: the agent runs locally; Plus provides a shareable HTTPS preview via or , while iteration stays in WhatsApp or Telegram. That is not a cloud IDE\u2014it is a link that works while code and processes stay on their hardware. Plus ($10) \u2014 draft shape Several concurrent HTTP tunnels (free: self-hosted relay only, or one hosted tunnel with ephemeral names). Reserved slugs so does not change every session. Caps on bandwidth, tunnel lifetime, and concurrent tunnels so hosted relay cost stays bounded. TCP tunnels optional on Plus or stricter on free (TCP is riskier to operate). Comparable spend to ngrok-class tools; the hook is already inside omnish, not a separate dashboard."},{title:"Second layer: team and accountability",body:"Solo users may stay on free plus self-hosted relay. Small teams pay when my phone controls our box needs structure: More than one allowlisted identity with clearer roles (run vs read-only vs tunnel-only). Audit trail: who ran what, which tunnels opened, when\u2014exportable for a client or cofounder. Named machines in cluster mode without ambiguous shared binding. Still device-first: gateway on their hardware; paid layer is policy and visibility."},{title:"Third layer: reliability",body:"Weaker as the only $10 hook unless they depend on the gateway daily; pairs well with hosted tunnels: Offline alerts when stops heartbeating. Guided boot / service setup with a simple health view (gateway up, last command, disk, tunnel count). Optional backup / restore of omnish data (shortcuts, cowork defs, session cwd maps)\u2014not the whole disk."},{title:"What not to lead with at $10",body:"Hosted agent sandbox \u2014 fights device-first positioning; competes with Cursor, Replit, etc. Generic AI \u2014 conflicts with no-AI, your-shell positioning. Per-message chat fees \u2014 trains workarounds. Security theater \u2014 erodes trust on remote shell access."},{title:"Packaging sketch",body:"Tier Free Plus $10 Pro (later) ------ ------ ----------- ------------- Chat \u2192 your shell Yes Yes Yes Self-hosted relay Yes Yes Yes Omnish-hosted HTTPS tunnels No / very limited Yes, with caps Higher caps Stable / custom names No Reserved slug Custom domain Team / audit Basic allowlist Small team + logs More seats, exports Gateway monitoring DIY Optional alerts SLA-style support One-liner: Keep controlling your machine from WhatsApp and Telegram for free; pay for public preview links, stable names, and team guardrails when that is how you work."},{title:"When someone will pay",body:"They pay when hosted tunnels remove a recurring pain: client demos, \u201Copen this while I change it from chat,\u201D or not maintaining Caddy, Cloudflare, and a relay on a VPS. Remote shell alone may stay free forever\u2014that supports adoption, not revenue."},{title:"Sequencing",body:"Plus = hosted tunnel + limits + account/token; chat stays unlimited. Team audit when paying users need shared access. Custom domains when reserved slugs feel tight. The preview-from-chat loop on their machine is credible paid value only if the URL is stable, HTTPS, and boring\u2014that is worth money while the shell stays free and local."}],keywords:["monetization","device-first","omnish","keeps","whatsapp","and","telegram","as","the","free","control","plane","allowlisted","messages","run","on","user","machine","paid","tiers","charge","for","reachability","identity","team","boundaries","safety","nets","not","sending","command","who","pays","at","10/month","strongest","first","wedge","second","layer","accountability","third","reliability","what","to","lead","with","10","packaging","sketch","when","someone","will","pay","sequencing"],relatedCommands:["/month someone","/tunnel","/theirname","/token"]},{id:"docs-features-online-catalog",path:"docs/features/online-catalog.md",title:"Online catalog",summary:"Share and install recipes, app templates, cowork tasks, and shortcuts across the omnish community. Browse and download from chat; publish when logged into the platform.",sections:[{title:"Discover and install (chat)",body:"Browse from the command family you care about \u2014 each prefix filters the catalog to the matching kind in MongoDB: Prefix Kind filter Example -------- ------------- --------- all kinds (optional on trending/search) only only only Shared subcommands (replace with , , , or ): Command Purpose --------- --------- Catalog subcommands for that family Most downloaded (kind-scoped when not ) Text search Full payload (review before install) Install to gateway-shared storage Install item #n from the last list in that family Repeat the last trending/search list for that family Examples: Use or on download to install for this chat only instead of gateway-shared: Numbered lists are per family \u2014 uses the last list, not a prior list. Platform URL: set in config (default hosted relay). Browse does not require a token; the CLI uses your configured relay origin. ---"},{title:"Publish (requires platform account)",body:"Sign up on the relay dashboard or run , then publish from chat: Command What is published Catalog --------- ------------------- ---------------- User or gateway-shared recipe (not built-ins) Running PTY session command (session must be alive) Cowork task for this chat Shortcut (chat or shared) On success you get a (e.g. ). Others install with the matching prefix, e.g. or . Flags: , , , ---"},{title:"What gets installed",body:"Kind Local storage ------ ---------------- gateway-shared bucket Same as recipe (category ; use with stored command) gateway-shared bucket (owned by the importing chat) ---"},{title:"Security",body:"Recipes and app commands can run shell on your machine. Always before download. The platform rejects dangerous recipe flags and invalid command shapes at publish time. Download does not auto-run \u2014 it only adds the template locally. ---"},{title:"API",body:"Hosted relay routes are documented in Platform reference \u2014 Catalog. Related: Recipes / \xB7 Cowork \xB7 Shortcuts"}],keywords:["online","catalog","share","and","install","recipes","app","templates","cowork","tasks","shortcuts","across","the","omnish","community","browse","download","from","chat","publish","when","logged","into","platform","discover","requires","account","what","gets","installed","security","api"],relatedCommands:["/run online help","/apps online help","/guides","/platform-reference","/run online","/search","/apps online","/cowork online","/shortcut online","/run","/apps","/cowork","/shortcut","/search list"]},{id:"docs-features-run-queue",path:"docs/features/run-queue.md",title:"`/run` queue (`-q` / `--queue`)",summary:"The run queue runs recipe launches one at a time per chat, in FIFO order. It is meant for back-to-back agent jobs (e.g. several tasks) without starting multiple PTYs at once.",sections:[{title:"Syntax",body:"and are equivalent (case-insensitive). Short form works the same way. Combine with attach flags after the recipe name (same as non-queued ): attaches the queue head on start; without / , the head starts detached (default; see in Configuration). Not the same as the slash subcommand (status) or ."},{title:"Loading many jobs from JSON (`/run queue load`)",body:"You can enqueue multiple recipe tasks in one step from a JSON payload. Each row is validated like a separate (same recipe resolution, task length limits, and behavior). The queue still runs them one at a time in order. Ways to provide the JSON Method What to send -------- ---------------- Host file path \u2014 path is resolved from this chat\u2019s session cwd (same idea as ): relative segments, globs, and quoted paths work like file selection elsewhere. Exactly one file must match. Inline JSON \u2014 everything after the word (with separating whitespace) is parsed as a single JSON value. Keep the payload on one message line if your client splits on newlines. Inbound file + caption Attach a document (e.g. ) and set the message text to (no path). Omnish uses the saved upload path from that same inbound turn (see Files \u2014 send & receive). You still get the usual \u201CSaved: \u2026\u201D line before the queue result. JSON format The file or inline text must be valid JSON in one of these shapes: Array of jobs (most common): Object with a single property (optional wrapper): Rules: Each job object must contain only two keys: and , both strings. Extra keys are rejected (catches typos like vs ). Top-level object form must be exactly \u2014 no other top-level keys. There must be at least one job. Empty arrays are rejected. Maximum 64 jobs per load. Not supported: raw , , or arbitrary shell in JSON. That would bypass recipe validation; only + are accepted, and omnish builds each run the same way as . File size limit for When reading a JSON file from disk (path argument or saved attachment path), the read is capped: If in is greater than zero, that value is used. If it is zero (no inbound cap), queue-load still uses a 1 MiB ceiling so a huge file cannot be pulled into memory unintentionally. Batch behavior and replies Jobs are enqueued in array order (first element becomes the next head when the queue is idle, or waits behind the current head). Omnish returns one reply that concatenates the status line from each enqueue step (started head, \u201Cwait slot \u2026\u201D, paused, etc.), same semantics as sending several messages in sequence. , , pauses, and clean-exit rules are unchanged \u2014 see Status and control and Pauses and failures. See also (files) Files \u2014 send & receive \u2014 where uploads land, , per-chat ."},{title:"What actually happens",body:"In memory only \u2014 Each waiting item is a small record (command + env + recipe label). Nothing runs until it becomes the head of the queue. Head starts immediately \u2014 When you enqueue and nothing else is running as the queue head, omnish shifts the first item off the FIFO and starts one app session (PTY), same as a normal . Others wait \u2014 Additional / calls append to the waiting list. No extra processes are spawned for those rows; the next job starts only when the previous head session exits with exit code 0 and signal 0 (clean exit). Per chat \u2014 Queue state is keyed by the chat ( ), not global across all users."},{title:"Why `/run queue` can show `Pending: 0` right after you queued something",body:"counts only jobs that have not started yet. The first item you add with / is removed from the pending list as soon as it starts; it then appears under Active with the session name and recipe label. Example: you send three queued runs in a row while the gateway is up: After message 1: Active = new session, Pending = 0 (nothing left waiting). After message 2: Active still the first session, Pending = 1 (second job waiting). After message 3: Pending = 2 (second and third waiting). So after a single enqueue is normal \u2014 it means the one job is already running, not that the queue \u201Clost\u201D your task. If you used before omnish accepted , the old parser treated as part of the task text and ran non-queued instead; the queue stayed empty. Use or after the recipe name (current omnish supports both)."},{title:"Status and control",body:"\u2014 Shows Active (session + recipe), Pending (with a short numbered list of waiting recipe labels), Paused, and a reminder that the next item auto-starts only after a clean exit. \u2014 Clears the paused flag and tries to start the next waiting item (e.g. after you fixed the host or session limits). If a head session is still running, resume tells you to wait until it finishes cleanly. \u2014 Enqueue many jobs from JSON (file path, inline , or attachment + caption); see Loading many jobs from JSON."},{title:"Pauses and failures",body:"Non-clean exit (non-zero exit code, or non-zero signal, e.g. SIGKILL) on the queue head \u2192 the queue pauses; waiting items stay in the FIFO until you (or fix limits and resume). Failed spawn (e.g. per-chat app limit reached) when trying to start the head \u2192 queue pauses and the item is put back; fix the error, then ."},{title:"Resource model",body:"Waiting rows: RAM only (no polling timers for the queue itself). Running head: one PTY + child process, same cost as a normal . Not persisted \u2014 If the gateway process ( ) restarts, the in-memory queue is cleared. Long-term scheduling belongs in Cowork or your own job runner."},{title:"See also",body:"System agents and User guide \u2014 shortcuts vs Interactive sessions ( )"}],keywords:["/run","queue","-q","--queue","the","run","runs","recipe","launches","one","at","time","per","chat","in","fifo","order","it","is","meant","for","back-to-back","agent","jobs","several","tasks","without","starting","multiple","ptys","once","syntax","loading","many","from","json","load","what","actually","happens","why","can","show","pending","right","after","you","queued","something","status","and","control","pauses","failures","resource","model","see","also"],relatedCommands:["/run help","/run queue","/run","/run remosh","/guides","/configuration","/to","/file","/send","/files-send-receive","/receive here","/run name","/system-agents-and-run"]},{id:"docs-features-service-from-chat",path:"docs/features/service-from-chat.md",title:"Service commands from chat (`/service`)",summary:"After WhatsApp or Telegram is connected and your identity is allowlisted, you can manage background gateway setup from the same DM thread \u2014 without SSH.",sections:[{title:"Commands",body:"Command Purpose --------- --------- Overview OS, data directory, , resolved Node + entry script paths Copy-paste steps for this host (paths filled by the running gateway) Last n lines of the default gateway log (default 80, max 120) Writes a user-level unit (Linux systemd or macOS LaunchAgent). Requires . Removes that unit (same gate). Windows: instructions only."},{title:"Trust model",body:", , and are safe to use like any other slash command (same allowlist as ). and modify login/boot integration files under your home directory. That is equivalent to shell access: anyone who can DM as an allowed identity can trigger them once is true in . Default: is . Enable only when you trust every entry on / as much as SSH. When enabled, reports a warning for ."},{title:"Relation to `CHANGE_ME` templates",body:"The contrib plist/service/XML files use placeholders for manual edits. bypasses that by injecting live paths from the running process. writes generated units with those paths automatically."},{title:"See also",body:"Background gateway and start on boot Cluster and chat configuration ( )"}],keywords:["service","commands","from","chat","/service","after","whatsapp","or","telegram","is","connected","and","your","identity","allowlisted","you","can","manage","background","gateway","setup","the","same","dm","thread","without","ssh","trust","model","relation","to","change","me","templates","see","also"],relatedCommands:["/service help","/service","/service status","/service instructions","/service logs","/service install","/config set","/service uninstall","/help","/boot integration","/contrib","/xml files"]},{id:"docs-features-sessions",path:"docs/features/sessions.md",title:"Interactive Sessions - omnish",summary:"Interactive sessions provide full terminal access within your messaging chats, enabling you to run TUI applications, REPLs, and interactive tools directly from WhatsApp or Telegram.",sections:[{title:"Overview",body:"Interactive sessions use to create pseudo-terminal sessions that mimic real terminal behavior. Each chat can maintain multiple named sessions with independent state. Key Features PTY-based: Full terminal emulation Named sessions: Multiple sessions per chat Focus management: One session attached at a time Output streaming: Real-time output with debouncing ANSI support: Color output and formatting Session persistence: State maintained across chats"},{title:"Session Management",body:"Starting Sessions Session Limits Per chat: Default 5 sessions (configurable) Global: Default 20 sessions (configurable) Named: Each session has a unique name per chat Plain messages and free shell For a single incoming line with no , , or , the router sends text to the attached session first when it is running; free shell mode ( ) applies only if nothing is attached (or the focused session is not running). Use for a one-off sync shell line while attached. Session States Created: Session initialized but not started Running: Session active and accepting input Attached: Session is focused for input Detached: Session running but not focused Stopped: Graceful shutdown requested Killed: Force termination Exited: Process terminated normally"},{title:"Session Commands",body:"Basic Operations Input Control Output Management Session Information"},{title:"Configuration",body:"Session Limits Terminal Settings Behavior Settings When , , or similar shows a password prompt, omnish detects it in recent terminal output and does not send the readline clear keys ( ) for your next reply \u2014 those keys break no-echo password readers and often appear as literal in chat. A one-time hint is sent unless is false. Passwords are still written to the session log on disk."},{title:"Use Cases",body:"Development Workflows ```text"}],keywords:["interactive","sessions","omnish","provide","full","terminal","access","within","your","messaging","chats","enabling","you","to","run","tui","applications","repls","and","tools","directly","from","whatsapp","or","telegram","overview","session","management","commands","configuration","use","cases"],relatedCommands:["/apps help","/apps start","/apps list","/apps attach","/apps detach","/apps stop","/apps kill","/apps send","/apps key","/apps tail","/apps since","/apps mute","/apps raw"]},{id:"docs-features-tunneling",path:"docs/features/tunneling.md",title:"Tunneling \u2014 omnish",summary:"omnish tunneling publishes a public URL that forwards to a local HTTP or TCP port on the machine running the tunnel client. The default relay is .",sections:[{title:"CLI (primary)",body:"Secrets are stored in (mode ) or . Override the relay with , in , or on expose commands."},{title:"Chat (optional)",body:"Login, logout, and status from chat work whenever the gateway runs (even if is false): , , . The bot reply does not echo your token; the inbound chat message still contains it (WhatsApp/Telegram history) \u2014 prefer on the host for highly sensitive tokens. When is in , allowlisted users can also run: Chat tunnels run inside the gateway process ( ) and share the same relay token as the CLI."},{title:"Security",body:"A tunnel URL is a capability: anyone who can open the URL can reach the forwarded service. Dev servers are often unauthenticated; treat tunneling like exposing a port on the public internet. Chat tunneling uses the same trust model as : allowlisted identities can open tunnels as the gateway OS user. in chat stores the bearer token on the gateway host but leaves a copy in the messaging transcript; use host CLI login if that risk matters for your threat model. See Security model."},{title:"Self-hosted relay",body:"For development or private deployments, run the relay in and point omnish at it with or . Operators: testing and operations (health checks, smoke, production VPS layout)."}],keywords:["tunneling","omnish","publishes","public","url","that","forwards","to","local","http","or","tcp","port","on","the","machine","running","tunnel","client","default","relay","is","cli","primary","chat","optional","security","self-hosted"],relatedCommands:["/tunnel help","/tunnels","/config help","/tunnel","/guides","/tunnel-setup-from-zero","/tunnel-auth","/tunnel login","/tunnel logout","/tunnel status","/telegram history","/tunnel http","/tunnel tcp","/tunnel stop"]},{id:"docs-features-watch",path:"docs/features/watch.md",title:"Watch \u2014 OS event eye",summary:"Lightweight OS event subscriptions that notify you on WhatsApp or Telegram when something changes on the machine running .",sections:[{title:"Quick start",body:"Enable watching: Or: or Run . Add rules: You should receive debounced messages like:"},{title:"Device-wide rules",body:"Watch rules are shared on the host, not private to one chat: One namespace per machine in (max 20 rules per device). Any allowlisted peer can , edit, pause, or remove any rule. Rule names must be unique on the device (two peers cannot each have a rule named ). (the default on new rules) alerts the peer who created the rule. Use , , or to reach more recipients. shows the creator peer key. If you had duplicate names from an older per-chat layout, omnish renames extras to on load."},{title:"Runtime model",body:"Watch is not a separate daemon or timed session. It runs inside the gateway process ( , foreground or ). Topic Behavior -------- ---------- How long Indefinite while the gateway runs, is true, and the rule is enabled and not paused. There is no session timeout. Debounce (default 2s, range 500ms\u201360s) coalesces bursts before chat notify. Rate cap per rule (default 30). Service polls Each rule runs / / about every 30 seconds. Background Same Node process as the gateway; keeps Watch alive like foreground. Not a separate OS service unless you installed the gateway as one."},{title:"Cowork, `/bg`, and Watch",body:"Feature What it does Relation to Watch --------- ---------------- ------------------- Watch OS events (FS, package logs, services) \u2192 chat alerts \u2014 Cowork Scheduled or on-demand shell commands while the gateway runs Parallel \u2014 same gateway, shared notify routing only. Does not trigger or consume watch rules. Background shell jobs from chat Unrelated \u2014 no integration with watch adapters. See Cowork for task schedules and heartbeats."},{title:"Persistence and restart",body:'Watch rules are not memory-only. They are saved on disk and survive gateway and service restarts. Data Path ------ ------ Rules (paths, excludes, paused, notify, \u2026) Recent events and state-change keys Global on/off and tuning ( , , debounce, rate cap) On gateway boot: If and (default), omnish reloads and starts adapters for rules that are enabled and not paused. If , rules remain on disk but adapters do not run until . If , rules persist but you must (or change a rule) to start adapters without restarting the whole gateway. Use to see file paths, saved rule counts, and adapter health. Troubleshooting persistence Symptom Check --------- -------- "Rules gone" after restart \u2014 they should still be there No alerts after restart in config; paused/disabled rules; Rules on disk but nothing running or ; if'},{title:"Rule lifecycle",body:"State What it means Command ------- ---------------- --------- Active Adapter running, alerts on or Paused Rule kept, adapter stopped, no alerts or Disabled Rule kept in list, Removed Deleted from Global off All adapters stopped After pause, stop, disable, or rm, pending debounced messages are cancelled so you should not get a late alert. pause / stop \u2014 same effect: stop watching, keep the rule for later. resume \u2014 start again if the rule is enabled. enable / disable \u2014 per-rule on/off (distinct from global and ). Check state: , , ."},{title:"Filesystem watches",body:"Add Root path \u2014 first path after the name ( , ). Events \u2014 optional comma list (default: ). Excludes (optional, both supported): Syntax Example -------- --------- or You can combine them: between exclude clauses is also accepted as a separator. Manage excludes later Excludes are applied twice for efficiency: native watcher ignore list + post-filter before notify."},{title:"Package and service watches",body:"Kind Command ------ --------- Packages \u2014 install/remove from OS logs Services \u2014 state changes on named units Use on noisy service checks. Discover services Finding the right unit name is easier with discovery commands (read-only; no rules are created): Command What you get --------- ---------------- Bulleted services with state, then a second message with copy-paste lines Template lines only (no list) Package log path for this OS + existing FS directories you can watch caps at 40 matches; narrow with a filter (e.g. ). Running units are listed first. Example second message after :"},{title:"Notifications",body:""},{title:"Efficiency and noise control",body:"Watch narrow directories when possible; use excludes for , caches, build output. Built-in ignores: , , , swap files, etc. Debounce \u2014 (default 2s) coalesces bursts. Rate cap \u2014 per rule (default 30). Sensitive paths ( , , keys) are blocked. Resource use: Adapter Cost --------- ------ fs Kernel-native watcher (inotify / FSEvents) \u2014 low for small trees; high if you watch all of without excludes. pkg Tails OS install logs (file watch or 2s poll fallback). svc One subprocess poll every 30s per rule \u2014 many service rules add steady CPU. Timers use so pending debounce/poll timers alone will not keep Node alive. Avoid watching all of without excludes."},{title:"Permissions",body:"Platform Packages Services ---------- ---------- ---------- Linux (often group) for named units macOS labels Windows Application log for named services"},{title:"Troubleshooting",body:"Problem What to do --------- ------------ No alerts true? Gateway running? Alerts after pause Should be fixed \u2014 if you paused mid-debounce, wait one debounce window; report if alerts continue unknown Use (name required) Path blocked Sensitive path denylist \u2014 pick another root Adapter error on status Fix log permissions or service names"},{title:"Data files",body:"Rules: Recent events:"},{title:"Config keys (chat-editable)",body:", , , \u2014 see ."},{title:"Related",body:"Cowork \u2014 scheduled tasks and heartbeat Webhook receiver \u2014 CI/CD to chat"}],keywords:["watch","os","event","eye","lightweight","subscriptions","that","notify","you","on","whatsapp","or","telegram","when","something","changes","the","machine","running","quick","start","device-wide","rules","runtime","model","cowork","/bg","and","persistence","restart","rule","lifecycle","filesystem","watches","package","service","notifications","efficiency","noise","control","permissions","troubleshooting","data","files","config","keys","chat-editable","related"],relatedCommands:["/watch help","/config","/watch on","/config set","/watch add","/deploy create","/projects","/tmp","/watch list","/home","/you","/deploy"]},{id:"docs-features-webhook-receiver",path:"docs/features/webhook-receiver.md",title:"Webhook receiver \u2014 CI/CD notifications via chat",summary:"The webhook receiver is a lightweight HTTP server built into the omnish gateway. It accepts requests with JSON payloads, formats them into concise messages, and delivers them to your WhatsApp or Telegram chat via the existing pipeline.",sections:[{title:"Prerequisites",body:"A running gateway: . set to in ."},{title:"Configuration",body:"Key Type Default Description ----- ------ --------- ------------- boolean Enable the webhook HTTP server number (random) Port to listen on. picks a random available port. string Bind address. Use to accept external connections (see security note). string Bearer token for authentication. If empty when the receiver starts, a random 32-byte hex token is generated and saved to . Example :"},{title:"Endpoint",body:"The token can also be passed as a query parameter: . Request body Any valid JSON object. The receiver formats the payload into a chat message using built-in formatters for known CI systems, or a generic format for everything else. Optional fields in the JSON body: Field Type Description ------- ------ ------------- string Target chat identity (e.g. or ). If omitted, the message goes to the first allowlisted peer. string Label for the source system (shown in the formatted message). Can also be set via query parameter or header. string Simple text message (used as-is when present). string Fallback message text. string Title line for generic payloads. string Status line for generic payloads. Response Status Body Meaning -------- ------ --------- Message delivered Invalid JSON or no target peer Missing or invalid token Not a POST request Body exceeds 256 KB failed"},{title:"Built-in CI formatters",body:"GitHub Actions When the payload contains and a object, the receiver formats: GitLab CI When the payload contains and , the receiver formats: Generic payloads For any other JSON, the receiver uses , , , and fields if present, or falls back to a truncated JSON preview."},{title:"Examples",body:"GitHub Actions workflow Add a step at the end of your workflow to notify on completion: GitLab CI Simple notification from a script"},{title:"Security",body:"The webhook server binds to 127.0.0.1 by default \u2014 only local processes can reach it. If you set to , the server is accessible from the network. Use a firewall or reverse proxy with TLS in production. The bearer token is compared using to prevent timing attacks. Maximum payload size is 256 KB."},{title:"Default peer resolution",body:"When the incoming payload does not include a , the receiver picks the first available peer from the gateway's allowlist: WhatsApp peers are checked first, then Telegram. If no peer is available, the request returns a error."},{title:"See also",body:"Configuration guide \u2014 webhook config keys Cowork heartbeat \u2014 combine with heartbeat tasks for dead-man's-switch monitoring Background jobs \u2014 per-job completion notifications"}],keywords:["webhook","receiver","ci/cd","notifications","via","chat","the","is","lightweight","http","server","built","into","omnish","gateway","it","accepts","requests","with","json","payloads","formats","them","concise","messages","and","delivers","to","your","whatsapp","or","telegram","existing","pipeline","prerequisites","configuration","endpoint","built-in","ci","formatters","examples","security","default","peer","resolution","see","also"],relatedCommands:["/help","/webhook authorization","/json","/repo","/github","/owner","/actions","/runs","/project","/checkout","/your-server","/webhook","/localhost"]},{id:"docs-guides-background-and-boot",path:"docs/guides/background-and-boot.md",title:"Background gateway and start on boot",summary:"Portable CLI (all platforms): ( ) starts the gateway detached; logs default to ; pass (alias: ) to append elsewhere; reads and stops the process. Use these for ad-hoc background runs without installing a system integration.",sections:[{title:"Linux (systemd --user)",body:"Copy contrib/omnish.service to . Set to your Node path and absolute path to , and if the data directory is not . Run: For a user service to run without an active login session, enable lingering:"},{title:"macOS (launchd)",body:"The LaunchAgent label is (reverse-DNS for omnish.dev). Older templates used ; if you already installed that, boot it out before installing the new plist (see below). Edit contrib/dev.omnish.gateway.plist: set Node path, path, and (and log paths if you use them). Copy to . Load (replace with output): To unload: Migrating from : remove the old job first, then install the new file: Alternative: add a small script that runs to System Settings \u2192 General \u2192 Login Items (simpler, no auto-restart on crash)."},{title:"Windows (Task Scheduler)",body:"Open Task Scheduler \u2192 Create Task\u2026 (not a basic task). General: run only when user is logged on (typical for WhatsApp session). Triggers: At log on for your user. Actions: Start a program Program: path to (e.g. from in ). Add arguments: (adjust the path; quotes if it contains spaces). Start in (optional): install folder. Set OMNISHHOME under user environment variables if you do not use the default data directory, or use a wrapper that then runs . Optional: import contrib/omnish-windows-task.xml after editing paths (import may need tweaks per account; the GUI is more reliable if XML import fails). Advanced: NSSM or WinSW can install Node as a Windows Service for machine-wide or headless scenarios \u2014 not maintained by this repo."},{title:"Stopping the gateway",body:"(any OS): uses the pidfile from a session. On Windows, if signaling the process fails, omnish may fall back to (best-effort shutdown). systemd / launchd / Task Scheduler: use the manager\u2019s stop / disable as usual; do not rely on from a different start method."},{title:"Environment",body:": data directory (same as elsewhere in omnish). For services, set it in the unit / plist / task environment, not only in the shell profile."}],keywords:["background","gateway","and","start","on","boot","portable","cli","all","platforms","starts","the","detached","logs","default","to","pass","alias","append","elsewhere","reads","stops","process","use","these","for","ad-hoc","runs","without","installing","system","integration","linux","systemd","--user","macos","launchd","windows","task","scheduler","stopping","environment"],relatedCommands:["/service help","/service instructions","/logs","/gateway","/service","/service logs","/service install","/features","/service-from-chat","/dist","/index","/omnish","/contrib"]},{id:"docs-guides-configuration",path:"docs/guides/configuration.md",title:"Configuration Guide - omnish",summary:"Complete configuration reference and customization guide.",sections:[{title:"Configuration Overview",body:"omnish uses a JSON configuration file with sensible defaults. You can customize every aspect of the system's behavior. Configuration File Location Default: Legacy: (if upgrading) Override: Set Check your configuration location: ```bash omnish status"}],keywords:["configuration","guide","omnish","complete","reference","and","customization","overview"],relatedCommands:["/config help","/config keys","/config","/path","/to","/dir","/bin","/bash","/features","/chat-llm-fallback","/tunnel","/login","/v1","/me"]},{id:"docs-guides-docker-gateway-golden-path",path:"docs/guides/docker-gateway-golden-path.md",title:"Docker gateway \u2014 golden path (reference)",summary:"Run omnish inside a container so a deployed app gains chat-driven shell access on that box. Reference files: .",sections:[{title:"Two ways to use omnish in Docker",body:"Path When Setup in container ------ ------ ---------------------- Attached (recommended) Platform account; messengers linked on dashboard , , , \u2014 no QR in container Standalone No hosted layer; expert / air-gapped Persistent volume + + inside container (or on host) The compose file in implements standalone by default. Use attached env vars for the platform path (see Platform attached mode, Platform reference)."},{title:"Attached mode (implemented)",body:"Add chat-driven ops to an existing app container without Baileys inside the image: Link WhatsApp/Telegram on the platform dashboard (once per account). Set allowlist on the dashboard. Deploy with the env above; outbound HTTPS to the platform only. Legacy env aliases: , . Messengers do not run inside the container; the platform routes chat to this CLI."},{title:"Standalone reference (implemented today)",body:"What you get Pinned npm version ( ). Non-root user (uid 1000), as PID 1. HEALTHCHECK ( ) \u2014 process up only; not messenger connectivity. Build and run First-time pairing (empty volume): Then as above."},{title:"Networking",body:"Concern Guidance --------- ---------- Attached Outbound HTTPS to communication layer; no Baileys in container Standalone Outbound to WhatsApp/Telegram APIs from container Tunnels Tunnel client runs in gateway namespace; see Tunnel setup from zero"},{title:"Environment",body:"Standalone today: \u2014 volume mount (compose: ) , \u2014 optional overrides Attached: , (aliases: , ) Optional /"},{title:"Security",body:"Allowlisted inbox \u2192 real shell as the container user. Protect volumes and tokens. Do not expose without TLS and auth \u2014 Webhook receiver."},{title:"See also",body:"Communication layer model Background gateway and start on boot"}],keywords:["docker","gateway","golden","path","reference","run","omnish","inside","container","so","deployed","app","gains","chat-driven","shell","access","on","that","box","files","two","ways","to","use","in","attached","mode","implemented","standalone","today","networking","environment","security","see","also"],relatedCommands:["/gateway-docker","/contrib","/architecture","/communication-layer-model","/tunnel","/telegram on","/docker-compose","/telegram apis","/tunnel-setup-from-zero","/home","/node","/features"]},{id:"docs-guides-interactive-cli",path:"docs/guides/interactive-cli.md",title:"Interactive terminal (`omnish i`)",summary:"Run the same commands you use in chat, but from your local terminal.",sections:[{title:"Commands",body:"Run from any directory. Your CLI session\u2019s working directory starts at (change it with or using your configured command prefix). Flags Flag Meaning ------ --------- Sender key for chat-driven cluster commands ( or ). If omitted, a synthetic sender tied to the CLI session is used ( ). / Run a single line and exit (non-interactive). Useful for scripts."},{title:"Trust model",body:"is not gated by the inbox allowlist. Anyone who can run commands on your machine can use it\u2014it has the same trust as your login shell. Chat interfaces remain allowlisted as before."},{title:"`/sendto`: push files or plain text to WhatsApp or Telegram",body:"When is active on this machine, you can use from to choose exactly who gets files, or to send a plain chat message (not an attachment). Syntax Files: selectors are checked from the current folder. Add an optional caption after . Plain text: same destinations; message is everything after the flag (or the value). To send a file whose name looks like a flag, use an explicit path (e.g. ). Destination forms: \u2014 all WhatsApp recipients from \u2014 all Telegram recipients from \u2014 both channels (all allowlisted WA + TG recipients) \u2014 one explicit WhatsApp recipient \u2014 explicit WhatsApp recipient list \u2014 one explicit Telegram recipient (compatibility form) Selector forms: \u2014 explicit list \u2014 cwd glob \u2014 recursive cwd glob Compatibility aliases also accepted: , , , , . Examples: Common patterns ```text"}],keywords:["interactive","terminal","omnish","run","the","same","commands","you","use","in","chat","but","from","your","local","trust","model","/sendto","push","files","or","plain","text","to","whatsapp","telegram"],relatedCommands:["/help","/sendto","/sendto wa","/photo","/sendto tg","/report","/promo","/downloads","/telegram outbound","/send","/files-send-receive","/bg","/reload"]},{id:"docs-guides-platform-attached-mode",path:"docs/guides/platform-attached-mode.md",title:"Platform attached mode",summary:"Run on a laptop, server, or container while WhatsApp and Telegram stay on the hosted omnish platform (tunnel relay). You link messengers once on the dashboard; each machine attaches with an account token and executes shell commands locally.",sections:[{title:"Standalone vs attached",body:"Standalone (default) Attached (platform) -- -------------------------- ------------------------- When No platform URL + token + (or env) set Where messengers connect Same host as Hosted relay (dashboard) Link WhatsApp on the device Dashboard QR or Link Telegram on the device Dashboard bot token Allowlist \u2192 local Dashboard allowFrom / telegramAllowFrom (authoritative) Shell execution Local Local (unchanged) If platform credentials are set, does not start local Baileys/Telegram clients \u2014 it opens a WebSocket to the platform and runs commands on this host only."},{title:"Prerequisites",body:"A platform account (signup/login on your relay \u2014 see tunnel relay README). Messengers linked on the platform (not via on the device, unless you use import \u2014 see below). Your phone or Telegram id on the platform allowlist (dashboard). Outbound HTTPS from the device to the platform URL (and correct reverse-proxy routing if you self-host)."},{title:"Step 1 \u2014 Account and token",body:"On the hosted relay (e.g. ): Sign up or log in via / , or use . Copy the account token from the response or dashboard. The same token works for: Attached gateway: + Persisted config:"},{title:"Step 2 \u2014 Link messengers on the platform",body:"Do this on the platform, not on the machine that will run shell commands (unless noted). WhatsApp Option A \u2014 Dashboard (recommended, 1 minute) Open the platform dashboard ( on your relay origin). Click Link WhatsApp \u2014 a QR appears automatically. Scan with WhatsApp \u2192 Linked devices. The phone may show \u201CLogging in\u201D for a few seconds while the platform reconnects after scan (515 restart \u2014 dashboard shows Finishing link\u2026; no second QR). When connected, use Add my number to allowlist (one click) if shown. Run on your machine. Option A2 \u2014 CLI QR in terminal Scan the ASCII QR, then: To disconnect: dashboard Unlink WhatsApp or . Option B \u2014 Import from a machine that already linked locally On a machine where you ran successfully, stop . Set platform URL + token, then: ```bash omnish platform import-whatsapp"}],keywords:["platform","attached","mode","run","on","laptop","server","or","container","while","whatsapp","and","telegram","stay","the","hosted","omnish","tunnel","relay","you","link","messengers","once","dashboard","each","machine","attaches","with","an","account","token","executes","shell","commands","locally","standalone","vs","prerequisites","step"],relatedCommands:["/help","/architecture","/communication-layer-model","/gateway-config-precedence","/contrib","/tunnel-relay","/readme","/telegram clients","/login on","/tunnel","/auth","/signup","/login"]},{id:"docs-guides-platform-reference",path:"docs/guides/platform-reference.md",title:"Platform reference (complete)",summary:"Consolidated documentation for the omnish hosted platform (tunnel relay + dashboard + attached ). For a guided walkthrough, start with Platform attached mode. For relay deployment, see contrib/tunnel-relay/README.md.",sections:[{title:"One-minute checklist",body:"Step Action ------ -------- 1 Sign up / log in at \u2014 copy account token 2 Link WhatsApp: dashboard Link WhatsApp \u2192 scan QR \u2192 Add my number to allowlist (if offered) 3 Link Telegram: paste bot token \u2192 Link / restart Telegram \u2192 DM bot \u2192 add id under Allowlists \u2192 Save allowlists 4 On your machine: (or ) 5 then 6 From allowlisted chat: or ---"},{title:"How it works",body:"Messengers terminate on the relay (Baileys + grammY). Shell runs only on machines where is attached. Policy (allowlists, ) is stored on the platform and merged into the attached CLI via (refreshed every 5 minutes and on connect). ---"},{title:"What is persisted",body:"Requires on the relay (account data). Without MongoDB, only static relay tokens work \u2014 no dashboard accounts or persistence. Data Storage Survives relay restart ------ --------- ------------------------- Account token, email MongoDB Yes , MongoDB Yes MongoDB Yes Telegram bot token MongoDB ( ) Yes (not returned by API) WhatsApp Baileys auth files Disk Yes Connector status, linked WA phone MongoDB Yes Peer \u2192 device bindings MongoDB Yes Device slots MongoDB Yes Community catalog entries MongoDB Yes On MongoDB connect, linked connectors are restored automatically ( ). Volumes (self-hosted): \u2014 WhatsApp sessions MongoDB \u2014 accounts, allowlists, connector sessions ---"},{title:"Dashboard (`/dashboard/`)",body:"After login, the dashboard loads and hydrates all forms. Account status Shows , WhatsApp/Telegram connector state, allowlist counts, default device, online device count. Devices Create device slots, set default device, see online/offline status. The first attached may auto-create a device. Routing Peer bindings map a (e.g. , ) to a specific . Routing order when a message arrives: Peer binding (if set) Single online device (if exactly one) Default device (if online) Any online device Allowlists Field Format Notes ------- -------- ------- WhatsApp E.164, comma-separated e.g. Telegram Numeric user ids Use bot in DM to discover your id Save allowlists \u2192 . Changes apply immediately (connectors reload allowlists per message). Warning: empty allowlist = any sender can run commands. Telegram connector Paste bot token from @BotFather. Link / restart Telegram \u2014 token required on first link; if already linked, empty token field reuses saved token. Users DM the bot before allowlisting to learn their numeric id. WhatsApp connector State UI ------- ----- Not linked Link WhatsApp \u2014 shows QR, auto-polls until connected; Refresh / Restart pairing while QR is active After scan, WhatsApp closes with 515 \u2014 QR hidden, \u201CFinishing link\u2026\u201D (10s); normal Runtime reconnect after a drop \u2014 QR hidden Linked Green \u201Cconnected\u201D, optional Add my number to allowlist, Unlink WhatsApp Logged out / error Reconnect (unlink + new QR) Advanced: import from local via (see CLI below). Attached CLI snippet Shows , , optional , and . ---"},{title:"CLI reference (`omnish platform \u2026`)",body:"All commands require + (config or env). Run for the latest help text. Setup and diagnostics Command Purpose --------- --------- Save credentials to Same (config CLI aliases) Account, connectors, allowlist counts, online devices URL, token source, effective platform block Test WebSocket paths before Attach device (attached mode when platform creds resolve) Allowlists (platform policy) Command Purpose --------- --------- Show WhatsApp and Telegram allowlists Merge entries into allowlists Replace one or both lists Examples: Wildcard is rejected on the platform (same security model as standalone). WhatsApp on platform Command Purpose --------- --------- Start pairing, print ASCII QR, poll until linked (includes after scan) Remove session and auth files on platform Upload local Baileys auth from on this host Stop local before import to avoid WhatsApp session conflicts. Environment variables Canonical Also accepted Purpose ----------- --------------- --------- , Relay base URL , Account bearer token in config Pin device slot on attach Precedence: environment \u2192 \u2192 (if used). Config keys: ( ), ( ), ( ). ---"},{title:"HTTP API reference",body:"Base URL: relay origin (e.g. ). Auth: for most routes. Catalog browse/download routes are public (no bearer required); publish requires a bearer token. Chat commands for the catalog: Online catalog. Auth (no bearer on signup/login body) Method Path Body Response -------- ------ ------ ---------- POST (201) POST Account Method Path Body Response -------- ------ ------ ---------- GET \u2014 , , , , , , , , , \u2026 PUT PUT GET \u2014 shape (example): means a bot token is stored; the token is never returned. Devices and routing Method Path Body Response -------- ------ ------ ---------- GET \u2014 POST (201) PUT DELETE Telegram connector Method Path Body Response -------- ------ ------ ---------- PUT required on first link; omit on later calls to reuse stored token. optional; can also use . WhatsApp connector Method Path Body Response -------- ------ ------ ---------- POST (optional legacy ) GET \u2014 POST GET \u2014 Legacy; prefer POST Status values: , , , , , , , . After a successful QR scan, Baileys typically closes with (515) and reconnects using saved creds (no second QR). The connector reports (no ) until the new socket opens as . Catalog (community templates) Public read (no bearer). Publish requires account bearer token. Method Path Auth Query / body Response -------- ------ ------ -------------- ---------- GET optional GET optional GET optional \u2014 Full entry including POST required POST optional \u2014 Full entry; increments values: , , , . Upsert on publish is per . Max payload 32 KiB. Recipe payloads must include quoted (or custom ) in the command. WebSocket (attached CLI) Path Purpose ------ --------- Primary attach path Fallback when not proxied to control port Register frame: Inbound: Outbound: Reverse proxy (required paths \u2192 control port 8788) , , , , If these hit the HTTP edge (8787) instead, attach fails with WebSocket 400. Run . ---"},{title:"Telegram `/id` command",body:"Works on platform-linked and standalone bots, before the user is on the allowlist. Mode Reply hints ------ ------------- Platform Numeric id + + dashboard / Standalone Numeric id + See Telegram integration notes. ---"},{title:"Policy in attached mode",body:"When attaches successfully: loads , , . These override local for inbound messenger policy. Host-only settings (shell, jobs, tunnels, webhooks) still come from local config. Policy refreshes every 5 minutes while attached. If fails at startup, the CLI may fall back to local allowlists until the next successful sync. You do not need on the device for platform-routed chat when platform policy loads successfully. ---"},{title:"Troubleshooting index",body:"Symptom See --------- ----- WebSocket 400 on attach Platform attached mode \u2014 Troubleshooting, relay README Device online, no command output Platform allowlist; relay logs; Troubleshooting \u2014 Attached Dashboard fields empty after login Requires MongoDB; hard-refresh; re-save allowlists Telegram token \u201Cgone\u201D after reload By design \u2014 token not shown; use empty field + Link to reuse Phone stuck on \u201CLogging in\u201D after scan Wait 15s \u2014 platform should show then connected; see Troubleshooting \u2014 Platform WhatsApp WhatsApp stuck / logged out Dashboard Reconnect or then link-whatsapp with correct allowlist WhatsApp LID resolution \u2014 update relay + CLI Empty allowlist surprises Empty = allow all senders ---"},{title:"See also",body:"Platform attached mode \u2014 guided setup Quick start \u2014 Path C Configuration \u2014 Platform / attached mode Docker gateway golden path Tunnel setup from zero \u2014 same account token for tunnels Communication layer \u2014 API sketch (future notes; implemented API is in this doc)"}],keywords:["platform","reference","complete","consolidated","documentation","for","the","omnish","hosted","tunnel","relay","dashboard","attached","guided","walkthrough","start","with","mode","deployment","see","contrib/tunnel-relay/readme","md","one-minute","checklist","how","it","works","what","is","persisted","/dashboard/","cli","http","api","telegram","/id","command","policy","in","troubleshooting","index","also"],relatedCommands:["/tunnel-relay","/readme","/contrib","/architecture","/communication-layer-model","/gateway-config-precedence","/telegram-integration-notes","/dashboard","/id","/v1","/me","/whatsapp"]},{id:"docs-guides-quick-start",path:"docs/guides/quick-start.md",title:"Quick Start Guide - omnish",summary:"",sections:[{title:"What you\u2019ll get",body:"By the end of this page you will have: A working gateway \u2014 listening for your DMs. A first win \u2014 a successful sync command (e.g. or ) with output back in chat. A path to deep control \u2014 optional: for a PTY smoke test (see the main README)."},{title:"What is omnish?",body:"A secure CLI tool that bridges messaging platforms (WhatsApp, Telegram) to your system shell. It allows allowlisted users to: Execute shell commands directly from chat Run background jobs with streaming output Start interactive terminal sessions Transfer files between your system and chats Key differentiator: No AI layer - direct, deterministic shell access with explicit security controls."},{title:"One phone with WhatsApp",body:"You do not need a second phone. Host \u2014 omnish runs on a laptop, desktop, home server, or VPS (Linux, macOS, or Windows). It is a CLI gateway, not a phone app. Phone \u2014 you use the same WhatsApp account: scan the QR once under Linked devices, then send command DMs from that app (for example Message yourself). Linking works like WhatsApp Web: your account is the phone plus linked devices. Session files live under (see Comprehensive documentation). The phone stays the primary device; omnish on the host is an extra linked device. Keep the phone online as WhatsApp expects for multi-device; Meta\u2019s caps on linked devices still apply. Follow Path A: WhatsApp below for install, , , and . Private chats only \u2014 groups are ignored. No computer handy? You still need a machine that stays on to run ; a small VPS is typical. If you prefer not to use WhatsApp linked devices, you can use Telegram instead ( ); see Telegram integration notes. Security Treat allowlisted numbers (and Telegram ids) like remote shell passwords \u2014 only add identities you fully trust. See the main README and Security model."},{title:"Installation",body:"From npm (recommended) From source ```bash git clone https://github.com/eligapris/omnish.git cd omnish pnpm install"}],keywords:["quick","start","guide","omnish","what","you","ll","get","is","one","phone","with","whatsapp","installation"],relatedCommands:["/help","/wa help","/tg help","/omnish","/media","/logo-horizontal","/apps start","/readme","/auth","/comprehensive-documentation","/telegram-integration-notes","/architecture","/security","/github","/eligapris"]},{id:"docs-guides-system-agents-and-run",path:"docs/guides/system-agents-and-run.md",title:"System agents, multi-agent, and `/run`",summary:"omnish is built for agents on your machine \u2014 not another hosted model or subscription. You run on hardware you control; from chat you drive system agents: CLIs and TUIs such as , , your own scripts, or multi-step orchestrators. Omnish forwards deterministic shell and PTY to those tools; it does not replace them.",sections:[{title:"Default macro command (`recipesMacroDefaultCommand`)",body:"In , is the shell command used when a macro-style recipe stores a long body as a . The default targets the Claude CLI agent: Point it at any agent or orchestrator that reads the task from the environment, for example a wrapper that fans out to multiple local agents: Restart the gateway after editing (or use when supported)."},{title:"Per-chat recipes with `/run add`",body:"The stored command must reference (or your custom ). Example: Cursor / other CLI agents Example: Claude (default-class agent CLI) Patterns are the same for any binary on the gateway host: omnish only spawns the process you configure."},{title:"Optional: local model CLIs",body:"If your agent stack sometimes calls a local inference CLI (e.g. Ollama), you can still register it in a recipe \u2014 same pattern. That is optional plumbing, not the core story: Prefer scripts on disk for awkward HTTP/JSON so you are not pasting payloads into chat."},{title:"Limits and safety",body:"\u2014 see configuration. \u2014 gated built-ins; leave off unless you understand the risk. Secrets: use env vars on the gateway host or your secret store; do not paste keys into chat."},{title:"Discoverability",body:", Cowork: \u2014 scheduled and on-demand tasks, notifications, logs (name unchanged)."},{title:"See also",body:"User guide \u2014 shortcuts vs Configuration Interactive sessions Background jobs vs Cowork"}],keywords:["system","agents","multi-agent","and","/run","omnish","is","built","for","on","your","machine","not","another","hosted","model","or","subscription","you","run","hardware","control","from","chat","drive","clis","tuis","such","as","own","scripts","multi-step","orchestrators","forwards","deterministic","shell","pty","to","those","tools","it","does","replace","them","default","macro","command","recipesmacrodefaultcommand","per-chat","recipes","with","add","optional","local","limits","safety","discoverability","see","also"],relatedCommands:["/run help","/apps help","/run","/apps start","/apps attach","/jobs","/cowork","/cw","/features","/sessions","/apps","/run queue","/run-queue"]},{id:"docs-guides-tunnel-setup-from-zero",path:"docs/guides/tunnel-setup-from-zero.md",title:"Tunnel setup from zero (gateway host)",summary:"This guide is for someone who wants public URLs for apps running on the machine where executes, using WhatsApp or Telegram (optional commands) or the CLI.",sections:[{title:"What runs where",body:"Piece Where Role ------- -------- ------ Relay VPS (e.g. ) or your own Docker host Accepts HTTPS/WSS, forwards to connected clients omnish gateway Your PC / server \u2014 runs shell, chat commands, and tunnel client Your app Same host as the gateway (usually) , etc. The default public relay is . You can self-host from and point clients at your origin."},{title:"Checklist",body:"Install omnish on the gateway host Native modules may require a normal install (not ). See the project README. Link WhatsApp and/or Telegram, allow yourself ```bash omnish link omnish allow +YOURE164"}],keywords:["tunnel","setup","from","zero","gateway","host","this","guide","is","for","someone","who","wants","public","urls","apps","running","on","the","machine","where","executes","using","whatsapp","or","telegram","optional","commands","cli","what","runs","checklist"],relatedCommands:["/tunnel help","/tunnel login","/tunnel","/features","/tunneling","/contrib","/tunnel-relay","/docs","/testing-and-operations","/docker-gateway-golden-path","/wss","/or telegram","/auth","/signup"]},{id:"docs-guides-ui",path:"docs/guides/ui.md",title:"Browser setup UI (`omnish ui`)",summary:"Local-first configuration panel that edits the same as the CLI and chat commands. Use it when you want to finish basics from a phone on your LAN before touching WhatsApp/Telegram.",sections:[{title:"Quick start",body:"Default bind is (reachable on your LAN). The CLI prints: A setup token (saved under your data dir as ; legacy installs may have had , which is migrated automatically) URLs on localhost and discovered IPv4 LAN addresses A quick link that includes so your phone can authenticate in one tap Then open the UI in a browser, unlock with the token, edit core settings, and Save."},{title:"Run the gateway from the UI",body:"After configuration (and WhatsApp pairing if you use it), you can start the chat gateway without going back to the terminal: Under Host snapshot, use Start gateway. This is equivalent to : a detached process runs with the same data directory, and stdout/stderr append to the default log file ( \u2014 the panel shows the resolved path). Stop gateway sends SIGTERM to the background gateway tracked by , same idea as (including stale pidfile cleanup when the process is already gone). Starting the gateway does not stop the setup UI. You can Stop setup server and leave the gateway running in the background. Authenticated session cookies are required (same as the rest of the panel): and . Port reclaim and stopping the UI The host writes in your data dir with the last process PID and bind port. When you start again on the same (default 3789), the new process terminates the previous one if it is still running, so the port is usually free without manual . A different does not kill another UI instance on a different port (the state file is overwritten when the new server starts). In the browser, Stop setup server (Core settings) ends the HTTP process after your session authenticates \u2014 the page will disconnect; run on the machine again to continue. Same trust as the rest of the panel: only someone with the setup token can unlock a session that can call ."},{title:"WhatsApp pairing (QR in the browser)",body:"You can link this host to WhatsApp without a terminal QR: Stop the gateway on this machine if it is running: use Stop gateway in the UI, , or stop your service. The UI checks for a live process and refuses browser pairing while it is present \u2014 two Baileys clients must not use the same directory at once. Unlock the UI with your setup token. Under Host snapshot, open WhatsApp pairing (QR) and choose Start QR pairing. On your phone: WhatsApp \u2192 Settings \u2192 Linked devices \u2192 Link a device, then scan the QR shown in the browser. If this host is already linked and you need a fresh login, enable Replace session (clears saved WhatsApp auth on disk, same idea as ) and confirm. You can still use from a shell on the host if you prefer the terminal QR. Pairing events are delivered over an authenticated same-origin event stream ( ) after you call . Traffic remains plain HTTP on the LAN \u2014 same trust model as the rest of the UI."},{title:"Security model (read this)",body:"Same trust as editing config on disk. Anyone who can change settings here could affect gateway behavior after reload/restart. LAN exposure: binding to all interfaces means anyone on your Wi\u2011Fi/Ethernet who can reach the port must not guess the token. Treat the token like a password. Not HTTPS: traffic is plain HTTP on your network segment v1. Run behind a reverse proxy with TLS only if you extend exposure beyond the LAN. Reduce exposure \u2014 loopback only (no phone from LAN). Firewall \u2014 block TCP 3789 (or your ) from the WAN side of your router. Guest Wi\u2011Fi \u2014 avoid running on networks shared with untrusted devices."},{title:"Flags",body:"Flag Meaning ------ --------- / Bind address (default ). / TCP port (default ). / Set or rotate the setup token (stored in )."},{title:"Environment",body:"Variable Meaning ---------- --------- Absolute path to a directory containing (advanced override for custom builds). Legacy alias for ."},{title:"Build note for contributors",body:"The UI is built into during / . If static files are missing, run the repo build from the project root so Vite runs before the esbuild CLI bundle."}],keywords:["browser","setup","ui","omnish","local-first","configuration","panel","that","edits","the","same","as","cli","and","chat","commands","use","it","when","you","want","to","finish","basics","from","phone","on","your","lan","before","touching","whatsapp/telegram","quick","start","run","gateway","whatsapp","pairing","qr","in","security","model","read","this","flags","environment","build","note","for","contributors"],relatedCommands:["/help","/configuration","/config set","/telegram","/stderr append","/logs","/gateway","/api","/start","/stop","/shutdown","/wa","/link"]},{id:"docs-guides-user-guide",path:"docs/guides/user-guide.md",title:"User Guide - omnish",summary:"Complete manual for using omnish's features.",sections:[{title:"Table of Contents",body:"Introduction Basic Shell Commands Background Jobs Interactive Sessions Terminal interactive ( ) File Transfer System Commands User Shortcuts User shortcuts vs recipes Cowork (scheduled tasks) Working with Multiple Platforms Configuration Management Chat LLM fallback (optional) Best Practices"},{title:"Introduction",body:"omnish bridges your messaging chats to your system shell with these main execution surfaces: Sync Shell: Immediate command execution (prefix: ) Background Jobs: Asynchronous commands with streaming output ( ) Cowork: Saved commands on a schedule or on demand ( ) while the gateway runs \u2014 see Cowork (scheduled tasks) Interactive Sessions: Terminal sessions via PTY ( ) Terminal REPL ( ): The same slash/ interface in your local shell\u2014see Interactive terminal (not gated by the inbox allowlist; same trust as your login session). Each chat maintains its own working directory and state, making it perfect for team workflows and personal use. The CLI REPL uses a dedicated session key and starts in your current directory when you launch ."},{title:"Basic Shell Commands",body:"Synchronous Execution Commands prefixed with execute immediately in the shell: Working Directories Each chat maintains its own working directory: Use to change directories Changes persist across commands View current directory with Command Prefix The prefix can be customized in : Without prefix, use free shell mode (see below)."},{title:"Background Jobs",body:"Start long-running commands without blocking your chat. Full detail: Background jobs. Starting Jobs Job Management Job IDs are 8-character hex strings; you can also assign a name with (or ). / / accept either form. Use (or ) to get a chat message when the job exits. Job Output and limits Logs persist under with matching . Jobs use the chat session cwd; they inherit the gateway process environment (not a prior from another shell). applies to sync commands only, not to children."},{title:"Interactive Sessions",body:'Start full terminal sessions in your chat: Starting Sessions Session Management Session Features Maximum 5 sessions per chat (configurable) One session "attached" at a time Plain text goes to attached session Output debounced and chunked ANSI codes can be preserved Session Commands Session Lifecycle'},{title:"Terminal interactive (`omnish i`)",body:"From a terminal on the same machine, run (or ) to type the same commands you would send in WhatsApp or Telegram: , , , , , etc. Trust: The inbox allowlist does not apply\u2014only local OS users who can run processes as you can start . Outbound files: Use or while is active; see Interactive terminal and Files send/receive. Media pull: downloads from URLs (yt-dlp + ffmpeg + optional Whisper) \u2014 see media pull. Jobs: in this REPL belongs only to this process, not to the gateway\u2019s job list. Full reference: Interactive terminal."},{title:"File Transfer",body:'Sending Files Receiving Files When someone sends media files: Files are automatically downloaded Saved to organized directory structure Reply with "Saved: /path/to/file" File Location Control'},{title:"System Commands",body:"Gateway Control Notes: Aliases: and work like . is accepted as an alias for . prints command help. Service management from chat and are gated by (same trust as shell). Allowlist Management Cluster (optional, chat-driven) When is true on multiple machines linked to the same WhatsApp number, only the active host replies. Coordination flows through the chat itself \u2014 no shared file, no Syncthing. Shorthand: \u2026 and the long form \u2026 (only the exact token matches; does not). See Cluster and chat configuration. Config from chat View or change many keys without SSH (same trust as shell): Allowlists stay / , not . Full list: Cluster and chat configuration. Other Commands"},{title:"User Shortcuts",body:"Create chat-specific command aliases: Creating Shortcuts Using Shortcuts Managing Shortcuts Scope model: / : private to this chat. / : shared on this gateway across chats. Resolution order for or : chat shortcut first, then shared."},{title:"User shortcuts vs `/run` recipes",body:"Both features are per-chat (stored on the gateway host), but they solve different problems: Shortcuts ( , , ) recipes ( , ) --- ----------------------------------------------- ----------------------------------- Purpose One-line alias: expand to a saved message once (e.g. , ). Parameterized runner: inject your task text into a CLI via (and optional ), then start a detached app session (PTY) by default \u2014 use or when you want plain DMs to go to the agent. still attaches on start. Typical use Jump to a directory, repeat a favorite sync command, short macros. Drive system agents (e.g. , , your orchestrators) from chat with a task each time. Invocation Bare token only: or (no extra words on the line). \u2014 task is required for named recipes. Shortcuts do not add a task environment variable. Recipes require the stored command to reference (or a custom ). For system agents, multi-agent PTY, and , see System agents and . Queued recipe runs (same chat, one PTY at a time): or , then for status. You can also batch-enqueue from JSON with (file path, inline JSON, or an uploaded file with that caption); see Run queue for the exact JSON shape and limits. Multi-step runbooks: create a recipe with sequential steps using : Steps are separated by or newlines. Each step runs in order; if any step fails (non-zero exit), remaining steps are skipped. Prefix a step with to continue on failure: When you invoke a runbook ( ), it runs in the background and sends a formatted per-step summary when complete. Use to see the step listing. Useful management forms:"},{title:"Cowork (scheduled tasks)",body:"Cowork saves shell commands and runs them on a schedule or on demand while is active. Commands use the same shell, timeout, and byte limits as sync commands. Alias: . Default log directory: (override with ). Missed scheduled times catch up when the gateway returns (one oldest slot per tick). Disabled tasks reject until re-enabled. Conditional notifications: only sends a notification when the task fails. tracks ok/fail transitions and only alerts on flips (useful for frequent health checks). Full reference: Cowork feature doc (notifications, data files, cluster and WhatsApp caveats)."},{title:"Working with Multiple Platforms",body:"Multi-Platform Setup Configure for both platforms: Platform Differences Feature WhatsApp Telegram --------- ---------- ---------- Message Limit 3500 chars 4096 chars Format Plain text HTML support File Types All media types Photo, document, video, audio Authentication QR code Bot token Best Practices Use different users for different platforms Keep allowlists minimal Monitor usage across platforms Use platform-specific features appropriately"},{title:"Chat LLM fallback (optional)",body:"If is true in on the gateway host, plain messages that would otherwise show \u201CNo command matched\u201D are answered asynchronously by a subprocess you configure ( ). Replies go back to the same WhatsApp/Telegram chat (or to the terminal in ). Where to configure: the same file as the rest of omnish \u2014 default , or if set. You edit JSON on the machine where runs; API keys must be available in that process\u2019s environment if your wrapper needs them (restart the gateway after changing systemd/shell env). Full step-by-step instructions, stdin vs PTY, and environment forwarding: Chat LLM fallback."},{title:"Configuration Management",body:"Configuration File Location Default: Override with environment variable Legacy: Uses if doesn't exist Editing from chat Most tunables support (whitelist). See Cluster and chat configuration and Configuration guide. Key Configuration Options Environment Variables : Override data directory : Enable debug logging (legacy: ) : Override bot token"},{title:"Best Practices",body:`Security Minimal Allowlists: Only add trusted users No Wildcards: Explicit phone numbers only Monitor Usage: Regularly check allowlists Session Isolation: Each chat is isolated Performance Output Chunking: Large outputs automatically split Session Limits: Don't exceed max sessions Timeout Settings: Adjust for your use case Log Rotation: Monitor log file sizes Usage Patterns Use Sessions for Interactive Apps: Vim, Python REPL, etc. Use Jobs for Long Operations: Builds, data processing Use Shortcuts for Common Tasks: Reduce typing Organize with Working Directories: Keep related work together Troubleshooting "Not in allowlist": Check E.164 format (+15551234567) "Maximum sessions reached": Stop unused sessions Messages cut off: Check Connection issues: Use for debug logs`},{title:"Next Steps",body:"Configuration: Customize for your workflow Security: Learn about the security model Features: Explore advanced features Integration: Set up for your team"}],keywords:["user","guide","omnish","complete","manual","for","using","features","table","of","contents","introduction","basic","shell","commands","background","jobs","interactive","sessions","terminal","file","transfer","system","shortcuts","vs","/run","recipes","cowork","scheduled","tasks","working","with","multiple","platforms","chat","llm","fallback","optional","configuration","management","best","practices","next","steps"],relatedCommands:["/help","/docs help","/run","/bg","/cowork","/apps","/tmp","/path","/var","/log","/system","/features","/background-jobs","/bg npm"]}]};var sn=Hm;function Vs(e,t){return`${e.replace(/\/$/,"")}/${t}`}function $k(e){return e.toLowerCase().replace(/[^a-z0-9\s/-]/g," ").split(/\s+/).filter(t=>t.length>1)}function Mk(e,t,n){if(t.length===0)return null;let o=e.title.toLowerCase(),r=e.summary.toLowerCase(),s=new Set(e.keywords),i=0,a,l=n.toLowerCase().trim();l.length>=3&&o.includes(l)&&(i+=40);for(let d of t){o.includes(d)&&(i+=12),r.includes(d)&&(i+=4),s.has(d)&&(i+=6),e.path.toLowerCase().includes(d)&&(i+=3);for(let u of e.sections){let c=u.title.toLowerCase(),m=u.body.toLowerCase();c.includes(d)&&(i+=5,a??=u.title),m.includes(d)&&(i+=2,a??=u.title)}}return i<=0?null:{entry:e,score:i,matchedSection:a}}function Xs(e,t=12){let n=e.trim();if(!n)return[];let o=$k(n),r=[];for(let s of sn.entries){let i=Mk(s,o,n);i&&r.push(i)}return r.sort((s,i)=>i.score-s.score||s.entry.title.localeCompare(i.entry.title)),r.slice(0,t)}function Zs(e){let{entry:t,matchedSection:n}=e,o=n?`${t.summary.slice(0,120)} (${n})`:t.summary.slice(0,160);return{id:t.id,path:t.path,title:t.title,summary:o,relatedCommands:t.relatedCommands}}function Xo(e){return sn.entries.find(t=>t.id===e)}function ei(e){let t=e.replace(/\\/g,"/").replace(/^\.\//,"");return sn.entries.find(n=>n.path===t||n.id===t)}function ti(e,t=1800){let n=[];e.summary&&n.push(e.summary);for(let r of e.sections){if(n.join(`
363
+ id: ${r.id}`)}return $h()}var Mh={version:1,generatedAt:"2026-05-23T09:24:47.448Z",repoUrl:"https://github.com/eligapris/omnish/blob/main",entries:[{id:"docs-advanced-implementation",path:"docs/advanced/implementation.md",title:"Implementation Guide - omnish",summary:"For contributors and developers who want to understand and extend omnish.",sections:[{title:"Development Setup",body:"Prerequisites Node.js >= 20 pnpm package manager Native build tools (make, g++, etc.) Getting Started ```bash git clone https://github.com/labKnowledge/whatsLive.git cd whatsLive pnpm install"}],keywords:["implementation","guide","omnish","for","contributors","and","developers","who","want","to","understand","extend","development","setup"],relatedCommands:["/omnish","/github","/labknowledge","/whatslive","/wa","/inbound","/tg","/gateway","/signal","/outbound","/index","/config"]},{id:"docs-advanced-troubleshooting",path:"docs/advanced/troubleshooting.md",title:"Troubleshooting Guide - omnish",summary:"Common issues and solutions for omnish.",sections:[{title:"Quick Reference",body:`Symptom Common Cause Solution --------- ------------- ---------- Connection failed Network issues Check connectivity "Not in allowlist" Wrong format Use E.164 format Sessions won't start Resource limits Check session limits Messages cut off Size limits Adjust config Telegram not working Bot token Verify token Attached: probe fails / WS 400 Proxy routes to wrong port Route , to control port 8788; Attached: online but no commands Platform allowlist / routing Dashboard allowFrom; relay logs Message yourself: complete silence Older builds dropped all traffic Update omnish + relay; send after upgrade`},{title:"Attached / platform mode",body:"Symptom: fails or errors with WebSocket Solutions: Reverse proxy must forward , , , , to relay control port (8788), not the HTTP edge (8787). See tunnel relay README. Run before . Confirm URL and token: . Symptom: Device shows online on dashboard but WhatsApp commands do nothing Solutions: Set allowFrom on the platform dashboard (attached mode uses platform policy, not local ). Check relay logs for with your device id. Only one should hold the device WebSocket per token. WhatsApp LID chats: ensure relay and CLI are up to date; use on the device and look for vs . with correct in logs but lists that number: the platform stores allowlist phones as digits only (CLI shows for display). Older CLI builds compared to raw digits and always denied \u2014 update omnish so normalizes platform entries. Symptom: Message yourself (or self-chat) \u2014 commands get no reply at all (not even \u201CNot allowlisted\u201D) Cause: WhatsApp marks messages you send from your phone as on linked devices. Older omnish builds ignored all upserts, so the documented Message yourself flow never reached the gateway. Solutions: Upgrade both the CLI ( ) and the platform relay ( ) to a version that tracks outbound message ids and routes allowlisted non-echo traffic. Confirm your number is on the platform allowlist (dashboard \u2192 Save allowlists). Run , then ; send or in Message yourself. Relay logs should show after you send a command. If you see nothing, WhatsApp may still be disconnected on the platform \u2014 check dashboard WhatsApp status. Standalone mode: same behavior applies; use and on the linked host. Symptom: warning at attach Solutions: Fix token, URL, or platform database (MongoDB on self-hosted relay). Until works, attached mode falls back to local allowlists. Full setup: Platform attached mode. Complete API/CLI/dashboard: Platform reference. WhatsApp link / unlink on platform Symptom Fix --------- ----- QR never appears Dashboard Link WhatsApp or ; check relay logs Stuck after logout Reconnect on dashboard or then link again Import fails Stop local ; use Linked but dashboard shows idle with autostart; ensure volume persists Allowlists and persistence Symptom Fix --------- ----- Dashboard does not remember allowlists Relay needs ; click Save allowlists CLI changes not reflected on device Wait up to 5 min or restart (policy refresh) Telegram token missing in UI Expected \u2014 token is not returned; leave field empty and Link to reuse saved token"},{title:"Installation Issues",body:"Native Module Build Failures Symptom: Build errors during Solutions: ```bash"}],keywords:["troubleshooting","guide","omnish","common","issues","and","solutions","for","quick","reference","attached","platform","mode","installation"],relatedCommands:["/help","/wa help","/tg help","/platform","/v1","/control","/dashboard","/auth","/contrib","/tunnel-relay","/readme","/me failed","/me","/guides","/platform-attached-mode"]},{id:"docs-architecture-communication-layer-model",path:"docs/architecture/communication-layer-model.md",title:"Communication layer and omnish CLI \u2014 unified model",summary:"Single source of truth for how omnish works today and how the optional hosted communication layer fits. Implementation sketches: Communication layer \u2014 API & ops, Gateway config precedence.",sections:[{title:"Summary",body:"CLI installs anywhere ( ) and works without the hosted layer (standalone mode). Optional hosted communication layer exposes WhatsApp, Telegram, and future surfaces through an API. Users link messengers once on the platform. Many devices (Docker, VM, VPS, bare metal) run the CLI with a device token and talk to that layer over a long-lived channel. One messenger connection \u2192 many devices: the platform routes chat to the right attached CLI; replies return through the same layer. That single connection is what makes the platform useful for fleets and containers. Shell execution stays on each device. The communication layer is not a remote shell; the CLI is the agent (shell, PTY, files, tunnels) on the box."},{title:"Two modes",body:"Mode When Where messengers connect CLI role ------ ------ -------------------------- ---------- Standalone No platform URL + token (default) On the same host as Full gateway: , , Attached + (or config / aliases) On the hosted communication layer Local executor + WebSocket client: register device, receive routed messages, send replies Both modes: allowlist and shell authority on the device. The platform may store policy in a dashboard, but the CLI enforces who may trigger commands before running shell on that host. Standalone (today) Documented in the README and Quick start. Baileys / Telegram bot clients run inside the gateway process; credentials live under . Attached (implemented) User links WhatsApp/Telegram on the platform dashboard (or ) \u2014 once per account. User sets platform URL + account token on each machine ( or env). On the target: , then . CLI opens WebSocket (fallback ), handles inbound messages like the standalone router, executes locally, posts replies. Setup: Platform attached mode. Full API/CLI/dashboard: Platform reference. No per-container WhatsApp QR is required for the default path."},{title:"Docker example (attached mode)",body:"A team runs an app in Docker and wants chat-driven ops inside that container without running Baileys in the image: Messengers stay on the communication layer; the container only needs outbound HTTPS to the layer API. See Docker gateway golden path."},{title:"Environment contract",body:"Purpose Canonical Also accepted --------- ----------- --------------- Platform base URL , Account token (tunnel + attached) , Optional device id in config Config file keys: ( ), ( ), ( ). Precedence: env \u2192 \u2192 tunnel auth file. Implementation: ."},{title:"Security boundaries",body:"Concern Standalone Attached --------- ------------ ---------- Who runs shell Local gateway Local CLI on device Messenger secrets on gateway host Communication layer (connectors) Stolen device token N/A Risk to that attach point; revoke in dashboard Platform sees message content N/A (direct to gateway) Yes, for routing \u2014 document retention and policy Remote shell from platform alone No No \u2014 requires valid device token + allowlisted sender on device Never echo full tokens in chat replies (same as tunnel tokens today)."},{title:"Relation to this repository",body:"Component Status ----------- -------- Standalone gateway ( , ) Implemented in Attached mode client ( , platform WebSocket) Implemented in , Hosted communication layer (relay + dashboard) Implemented in (deploy separately)"},{title:"See also",body:"Platform attached mode \u2014 connect, link, configure, run Platform reference \u2014 complete API, CLI, dashboard, persistence Vision \u2014 hosted communication layer \u2014 short product summary Platform layer spec \u2014 API sketch, tokens, threat model Gateway config precedence \u2014 config merge in attached mode Relay operator README \u2014 deploy, paths, dashboard docs/ideas/ \u2014 historical voice-note inputs"}],keywords:["communication","layer","and","omnish","cli","unified","model","single","source","of","truth","for","how","works","today","the","optional","hosted","fits","implementation","sketches","api","ops","gateway","config","precedence","summary","two","modes","docker","example","attached","mode","environment","contract","security","boundaries","relation","to","this","repository","see","also"],relatedCommands:["/readme","/guides","/quick-start","/telegram on","/platform","/device","/control","/platform-attached-mode","/cli","/dashboard","/platform-reference","/tunnel"]},{id:"docs-architecture-gateway-config-precedence",path:"docs/architecture/gateway-config-precedence.md",title:"Gateway configuration precedence",summary:"How effective configuration is computed. See Communication layer model for standalone vs attached modes.",sections:[{title:"Sources (ordered low \u2192 high priority)",body:"Standalone ( without platform token) Compiled defaults \u2014 values baked into the binary when a key is unset. Local \u2014 under . Environment variables \u2014 e.g. , . Chat \u2014 allowlisted senders; same trust as shell. Higher layers win per key. Attached ( with platform URL + token) Compiled defaults Local \u2014 host paths, shell, tunnel, jobs, etc. Platform account ( ) \u2014 wins for policy keys: , , (derived from linked connectors on the platform). Not written back to disk by default. Environment variables \u2014 still override local file for host keys; do not replace platform allowlists unless you also change them on the dashboard. Chat \u2014 updates local file only in attached mode; platform allowlists remain authoritative for inbound from messengers on the layer. If fails at connect, the CLI logs a warning and uses local for allowlists (or partial data from the WebSocket ack: + connectors only)."},{title:"Keys that stay host-only (never taken from platform)",body:", job limits, sync paths, webhook ports/tokens on the device Baileys auth paths, local tunnel client settings WhatsApp session blobs and Telegram bot tokens in attached mode (connectors run on the platform)"},{title:"Keys owned by the platform in attached mode",body:", \u2014 set on the dashboard; device enforces the platform copy on inbound WebSocket messages. \u2014 derived from which connectors are linked on the platform ( , , or )."},{title:"Merge algorithm (attached inbound)",body:"```text effective = loadConfig() # local host + defaults if platform snapshot loaded: effective.allowFrom = platform.allowFrom effective.telegramAllowFrom = platform.telegramAllowFrom effective.gatewayMode = platform.gatewayMode"}],keywords:["gateway","configuration","precedence","how","effective","is","computed","see","communication","layer","model","for","standalone","vs","attached","modes","sources","ordered","low","high","priority","keys","that","stay","host-only","never","taken","from","platform","owned","by","the","in","mode","merge","algorithm","inbound"],relatedCommands:["/v1","/me","/config","/guides","/platform-reference","/config set","/tokens on","/platform","/account-sync","/default-device"]},{id:"docs-architecture-overview",path:"docs/architecture/overview.md",title:"Architecture Overview - omnish",summary:"",sections:[{title:"Executive Summary",body:"omnish is a secure, deterministic messaging-to-shell gateway that bridges messaging platforms (WhatsApp, Telegram) to system shell access. The architecture follows a thin transport adapter pattern with a unified core, explicit security controls, and comprehensive session management. Local control plane: When the gateway process ( ) is up, it may expose a localhost-only control endpoint (metadata in ) so a separate process can request outbound file sends that reuse the same Baileys/grammY sessions\u2014avoiding a second login. This is optional machinery for CLI ergonomics; inbound chat traffic still flows through the transports above."},{title:"Key Architectural Principles",body:"Thin Transport Adapter Pattern Transport Layer: Platform-specific implementations (WhatsApp, Telegram) Adapter Layer: Normalizes all inputs to format Core Layer: Unified business logic and message routing Persistence Layer: Configuration and state management Deterministic Behavior No AI or agent layer Rule-based message routing Predictable command precedence Explicit state management Security-First Design Explicit allowlists required No anonymous access Per-peer session isolation Minimal attack surface"},{title:"System Architecture",body:"High-Level Diagram Component Responsibilities Layer Component Responsibility ------- ----------- ---------------- Transport WhatsApp WebSocket lifecycle, QR auth, media handling Telegram Long polling, bot API, formatting Adapter Inbound Normalize messages to Outbound Platform-specific message sending Core Gateway Transport multiplexing, lifecycle management Router Command dispatch with precedence rules Session Manager Per-peer working directories and state Apps Manager Interactive PTY session lifecycle Job Manager Background process execution Persistence Config Configuration loading and validation Sessions JSON-based state persistence Files Log files, session output, job output"},{title:"Data Flow Architecture",body:"Message Processing Pipeline Command Precedence Flow Messages are processed in strict precedence order: Free Shell Toggle: / Sync Commands: (e.g., ) System Commands: (e.g., , ) App Shorthand: (e.g., ) Free Shell Mode: Plain text execution Attached App: Forward to focused PTY session Help Fallback: Display help message"},{title:"Security Architecture",body:"Allowlist System Peer Key Model WhatsApp: or normalized phone number Telegram: Hashed for storage: SHA-1 prefix for log directories Session isolation: Each peer has separate state Security Boundaries Transport Boundary: Platform-specific authentication Authorization Boundary: Allowlist validation Execution Boundary: Command and process isolation Data Boundary: Per-peer state separation"},{title:"Session Management Architecture",body:"Session Context Persistence Session Lifecycle Load: From JSON storage or create default Update: Persist changes immediately Isolate: Per-peer separation with migration support Cleanup: Remove on session destruction"},{title:"Transport Architecture",body:"Transport Interface All transports implement the same core interface: Transport-Specific Features Transport Authentication Message Limit Features ----------- --------------- -------------- ---------- WhatsApp QR code 3500 chars Media download, auto-reconnect Telegram Bot token 4096 chars HTML formatting, webhook support"},{title:"Output Processing Architecture",body:"Output Flow Output Processing Pipeline Buffer: Collect output chunks Debounce: Wait for output completion ( ) Chunk: Split into transport-sized pieces Format: Strip ANSI, add prefixes, etc. Send: Deliver via appropriate transport"},{title:"State Management Architecture",body:"State Types Configuration: Global settings Session Context: Per-peer state Job State: Background job metadata App State: Interactive session state Persistence Strategy State Type Format Location Access Pattern ------------ -------- ---------- --------------- Config JSON Load at startup, update on change Sessions JSON Load on demand, append updates Jobs Files Streaming write, read on demand Apps Files Streaming write, read on demand"},{title:"Error Handling Architecture",body:"Error Categories Transport Errors: Connection issues, timeouts Validation Errors: Invalid input, malformed commands Execution Errors: Command failures, process timeouts Resource Errors: Memory limits, session limits Error Handling Flow Capture: Error detected at source Log: Structured logging with context Notify: User-friendly error message Recover: Graceful degradation where possible"},{title:"Extension Points",body:"Transport Extension Command Extension Configuration Extension"},{title:"Performance Considerations",body:"Memory Management Session Limits: Prevent unbounded growth Output Buffering: Size-limited buffers Cleanup: Proper resource disposal Network Optimization Message Chunking: Minimize API calls Connection Reuse: Persistent connections Backoff Strategy: Exponential backoff for failures I/O Optimization Async Operations: Non-blocking file I/O Lazy Loading: Load data on demand Output Streaming: Real-time output delivery This architecture overview provides the foundation for understanding omnish's design principles and implementation patterns."}],keywords:["architecture","overview","omnish","executive","summary","key","architectural","principles","system","data","flow","security","session","management","transport","output","processing","state","error","handling","extension","points","performance","considerations"],relatedCommands:["/grammy sessions","/inbound","/gateway","/outbound","/jobs","/apps","/command","/bg","/job","/pty","/config","/sessions"]},{id:"docs-architecture-platform-layer-spec",path:"docs/architecture/platform-layer-spec.md",title:"Communication layer \u2014 API and operations sketch",summary:"Implementation detail for the unified model in Communication layer model. The attached CLI client is implemented in and . The hosted relay (connectors, dashboard, routing) lives in .",sections:[{title:"Goals",body:"Messenger connectors on the layer: WhatsApp, Telegram, others \u2014 linked once per workspace from a dashboard. Device registry and routing: many CLIs attach with ; inbound chat is routed to the correct device; replies return through the layer. Token issuance and lifecycle from the dashboard (create, rotate, revoke). Minimal env on each device: + (legacy aliases: , , ). Non-goals: Running user shell or PTY in the cloud. Replacing local enforcement of allowlists before command execution on each device. Requiring the hosted layer for open-source standalone use ( on the host without env)."},{title:"Operating modes (messenger termination)",body:"Mode Messenger clients Device (CLI) ------ ------------------- -------------- Standalone On gateway host ( , Baileys / Telegram bot today) Same process as messengers Attached On communication layer (connectors) CLI only: API/WebSocket client + local shell See Communication layer model \u2014 Two modes."},{title:"Threat model (summary)",body:"Asset Risk Mitigation -------- ------ ------------ Device token Theft \u2192 control of one attach point Short TTL, rotation, revoke; scoped to one device slot Workspace / dashboard session Account takeover Strong auth, audit log on connector and device changes Message content on layer Privacy / retention Policy, encryption in transit, minimal logging; no training on content by default (product policy) Signed config blob Forged defaults on device Signing keys, , host-only keys; see Gateway config precedence Chat as secret channel Token pasted in DM Never echo full secrets in replies Trust boundary: The communication layer routes messages and holds connector credentials in attached mode. Each CLI enforces allowlists and runs shell locally. Layer compromise must not run shell on a device without a valid device token and allowlisted sender on that device."},{title:"Token scopes (recommended)",body:"Scope Purpose Typical lifetime ------- --------- ------------------ Long-lived channel for one CLI attach point Rotatable; per device One-time or short exchange when creating a device slot Minutes Fetch non-secret config subset for a device Hours\u2013days Relay bearer (may be issued by layer) Per relay policy Human dashboard (not for CLI) Session Rules: narrow scopes, stable token ids for revocation, no user-facing \u201Cgod\u201D token."},{title:"Implemented API (this repository)",body:"The relay implements a single-account model (not multi-workspace). Authoritative route list, request bodies, and CLI commands: Platform reference Summary: Area Implemented paths ------ ------------------- Auth , Account , , Devices , Telegram WhatsApp , , , Attach , UI"},{title:"Future API sketch (not implemented)",body:"The following were early design notes; do not assume they exist on the relay today: Multi-workspace ( ) Separate device tokens per slot (today: one account bearer token) , scoped bootstrap tokens SSE alternative to WebSocket"},{title:"Compliance and logging",body:"Attached mode: layer may process message content for delivery; document retention and access in privacy policy. Avoid logging message bodies in application logs by default."},{title:"Relation to this repository",body:"Communication layer service: (deploy separately; MongoDB + dashboard). CLI attached mode: , \u2014 WebSocket client + local shell. Standalone: / on the same host (unchanged when platform env is unset). Operator guides: Platform attached mode, Platform reference."}],keywords:["communication","layer","api","and","operations","sketch","implementation","detail","for","the","unified","model","in","attached","cli","client","is","implemented","hosted","relay","connectors","dashboard","routing","lives","goals","operating","modes","messenger","termination","threat","summary","token","scopes","recommended","this","repository","future","not","compliance","logging","relation","to"],relatedCommands:["/platform","/gateway","/attached","/tunnel-relay","/contrib","/guides","/platform-reference","/platform-attached-mode","/websocket client","/auth","/signup","/login"]},{id:"docs-architecture-routing",path:"docs/architecture/routing.md",title:"Message Routing Architecture - omnish",summary:"",sections:[{title:"Introduction",body:"The message routing system is the heart of omnish, responsible for determining how incoming messages are processed and executed. It follows a deterministic precedence model with clear rules for each message type."},{title:"Routing Overview",body:"Message Flow Core Components Allowlist Validator: Ensures user has permission Message Normalizer: Converts transport-specific formats Session Loader: Retrieves per-peer state Router: Applies precedence rules and dispatches Executors: Command-specific handlers"},{title:"Message Precedence Rules",body:"Precedence Hierarchy Messages are evaluated in strict order from highest to lowest precedence: Free Shell Mode Toggle ( / ) Synchronous Commands ( ) System Commands ( ) App Session Shorthand ( ) Attached App Sessions (when focused and running) Free Shell Mode (when enabled) Help Fallback Rule Implementation"},{title:"Command Types and Handling",body:"Free Shell Mode Toggle Purpose: Enable/disable direct shell execution Pattern: or Synchronous Commands Purpose: Execute shell commands immediately Pattern: System Commands Purpose: Control omnish functionality Pattern: App Session Shorthand Purpose: Quick interaction with app sessions Pattern: Free Shell Mode Purpose: Execute any text as shell command Pattern: Plain text (when enabled) Attached App Sessions Purpose: Send input to focused PTY session Pattern: Plain text (when session focused)"},{title:"Special Cases and Edge Handling",body:"Shortcut Expansion Commands starting with prefix are checked for shortcuts: Command Precedence Override Certain commands always take precedence: Error Handling"},{title:"Session Integration",body:"Session Context The router uses session context for state: Session Management"},{title:"Transport Integration",body:"Peer Key Normalization Transport-Specific Handling"},{title:"Performance Considerations",body:"Optimization Strategies Session Caching: Cache frequently accessed sessions Command Caching: Cache command results for identical inputs Output Debouncing: Buffer output to reduce message spam Lazy Loading: Load sessions only when needed Concurrency Handling"},{title:"Extension Points",body:"Adding New Command Types Custom Command Handlers"},{title:"Fleet and config slash commands",body:"The authoritative dispatch order for messages that start with is implemented in (not the pseudocode snippets earlier in this doc). Highlights: runs before fleet aliases so paths like never collide with . Fleet commands accept , , or ( ): only a standalone token or + rest matches \u2014 paths such as do not match the shortcut. (also , ) sets ; reapplies config while is active. See Cluster and chat configuration for behavior and Configuration guide for fields."},{title:"Testing and Validation",body:"Unit Tests Integration Tests This message routing architecture ensures predictable, efficient processing of all incoming messages while maintaining security and session isolation."}],keywords:["message","routing","architecture","omnish","introduction","overview","precedence","rules","command","types","and","handling","special","cases","edge","session","integration","transport","performance","considerations","extension","points","fleet","config","slash","commands","testing","validation"],relatedCommands:["/command","/disable direct","/new","/path","/router","/config","/config set","/computers","/pcs","/cluster","/gateway","/gw"]},{id:"docs-architecture-security",path:"docs/architecture/security.md",title:"Security model",summary:"omnish bridges WhatsApp and/or Telegram direct messages to shell commands on your machine. Security is explicit allowlists plus local filesystem and process boundaries. There is no cloud relay for commands: whoever can send messages as an allowed identity can execute code as the OS user running .",sections:[{title:"Trust boundaries",body:"Allowlisted identities are credentials. If an attacker can spoof or compromise an allowed WhatsApp number or Telegram user id, they get the same power as you gave on that host. Wildcards are rejected. must never contain . Secrets on disk: WhatsApp session material lives under the data directory ( / / legacy ). Telegram bot tokens live in or . Restrict permissions so other Unix users cannot read them. Chat config: lets allowlisted users edit from chat (same trust as running shell commands). Do not treat DMs as \u201Clow privilege.\u201D Multiple Telegram bots: If and several hosts run Telegram, use a different bot token per host; one token cannot be long-polled twice (see finding in ). Interactive terminal ( ): Same trust as running a shell on that machine. It is not gated by WhatsApp/Telegram inbound allowlists\u2014those lists still apply to messages arriving through the gateways. Gateway control ( ): Written only while is active; carries localhost connection info and a token so can request outbound media through the existing gateway. Anyone who can talk to 127.0.0.1 as your user could misuse it\u2014treat host-local access like filesystem access. : Sends files to arbitrary WhatsApp numbers or Telegram chats through your linked session, comparable to sending those files manually from the linked WhatsApp device or bot\u2014powerful and intentional. Tunneling ( , when ): Publishes public URLs to local HTTP/TCP ports. Anyone with the URL can reach the forwarded service; chat tunneling is gated by the same allowlists as shell commands."},{title:"Automated posture checks",body:"The same rules run in three places: Surface How -------- ----- CLI (plain text) or Chat , , Startup refuses to start if any error-severity finding is present; warnings are printed but do not block Automated checks cover configuration and local Unix permissions. They do not replace firewall rules, SSH hardening, malware scanning, or review of who physically accesses the machine. Severity levels error \u2014 Blocks until fixed (or until you change / token so the check no longer applies). warn \u2014 Shown on startup and in reports; you should understand and usually fix. info \u2014 Informational (for example, env var overrides)."},{title:"Finding codes and remediation",body:"Code Severity Meaning What to do ------ ---------- --------- ------------ error contains Remove from in . error Telegram transport enabled but no token Set in config or . error Configured binary does not exist Install the shell or point at a valid absolute path. error is but path empty Set or change mode. error Fixed receive path not absolute Use an absolute . warn WhatsApp enabled, empty warn Telegram enabled, empty warn is true Set to unless you trust every recipe. warn is not an absolute path Use e.g. in config. warn Could not stat Fix permissions/path. warn group/world readable or writable on config file. warn Process UID is root Run as a normal user when possible. warn Data directory group/world accessible on data dir ( or default ). warn Jobs directory group/world accessible (only checked if data dir is already tight) on the jobs directory under the data dir. warn WhatsApp auth dir readable by others on under the data dir. info set; overrides config Unset env if you want config file to win. info Cluster + Telegram: same token must not be used on two running gateways Use one bot per host or run Telegram on fewer machines. Unix permission checks are skipped on Windows."},{title:"JSON output for automation",body:"prints: Exit code if any error-severity finding exists (same as plain text)."},{title:"Related commands",body:"\u2014 Includes a one-line security summary. In chat: for a short hardening checklist. See also the user guide Security section in User Guide."}],keywords:["security","model","omnish","bridges","whatsapp","and/or","telegram","direct","messages","to","shell","commands","on","your","machine","is","explicit","allowlists","plus","local","filesystem","and","process","boundaries","there","no","cloud","relay","for","whoever","can","send","as","an","allowed","identity","execute","code","the","os","user","running","trust","automated","posture","checks","finding","codes","remediation","json","output","automation","related"],relatedCommands:["/security help","/security summary","/or telegram","/config set","/telegram","/sendto","/tunnel","/tcp ports","/security","/security tips","/run","/bin","/bash"]},{id:"docs-features-background-jobs",path:"docs/features/background-jobs.md",title:"Background jobs \u2014 omnish",summary:"Background jobs run a single shell command asynchronously in the current chat\u2019s working directory while you keep using the chat. Output is appended to a log file under your data directory; you can pull the last N lines or only new bytes since your last for that job in this chat.",sections:[{title:"Commands (implemented)",body:"Command Meaning -------- --------- Start a background job; reply includes an 8-char job id and hints for and . \xB7 \xB7 Same as , but stores a name you can use instead of the hex id with , , and . \xB7 Start a job that sends a completion notification to your chat when it exits (includes exit code, duration, and command summary). Combine flags: named job with completion notification. List recent jobs (up to 20), newest first: id (and name if set), status ( \\ \\ ), exit code, duration, command preview. Last lines of the log (default from ; max 500 if you pass a number: or ). Incremental: new log bytes since your last of this job (resolved id) in this chat (per-chat cursor). Send SIGTERM to the running process (or best-effort to the recorded pid if the child already detached). Job ids are 8-character hex (e.g. ). Names are optional labels (letters, digits, , , , up to 64 characters). If several jobs share the same name, / / resolve to the newest (most recently started) matching job. Resolving -shaped tokens: if a job with that id exists on disk, the token is treated as an id; otherwise it is treated as a name (so a name that looks like 8 hex digits still works when no job file uses that id). There is no , , or in chat \u2014 use and / instead. ( exists for gateway shutdown; it is not exposed as a slash command.)"},{title:"Behavior details",body:"Working directory: Same as session cwd ( applies before ). Environment: Inherited from the omnish gateway process (the Node process running ), not from a prior in chat \u2014 each and runs a new shell. Set env vars in the shell that starts the gateway, or put them in the job command line. Timeouts: applies to synchronous commands, not to children. Long-running jobs are not auto-killed by that setting. Logs: and (see )."},{title:"Configuration",body:""},{title:"Job lifecycle (actual statuses)",body:"From : running \u2014 child spawned; log growing. done \u2014 process exited (or spawn error); / recorded in meta. killed \u2014 user ran (SIGTERM); meta updated. Optional field is stored in when you start a job with / ."},{title:"Completion notifications",body:"When you start a job with (or ), the gateway sends a message to your chat as soon as the process exits: The notification includes: Exit code (0 for success, non-zero for failure) or signal name (e.g. SIGTERM) Duration of the run Command that was executed This is useful for long builds, deployments, or test suites where you want to walk away and get notified when it finishes. Combine with for readability: Notifications are sent via the same transport as the chat (WhatsApp or Telegram). If the gateway shuts down before the job finishes, the notification is lost."},{title:"Examples",body:"Same using the printed id:"},{title:"Compared to Cowork",body:"--- -------- ----------- Scheduling One-shot , , , etc. Notifications Opt-in per job ( ) Per task ( + ) Queue / catch-up No Yes (SQLite + pending queue) Typical use Ad hoc long command Recurring or on-demand saved tasks See cowork.md."},{title:"Troubleshooting",body:"No output in chat: Streaming behavior depends on the gateway; use or for the log file. Lost jobs on gateway restart: In-memory handles are cleared; log and meta files on disk may still be present under . Verbose gateway logs: ---"},{title:"Change log \u2014 named background jobs (2026-05-08)",body:"Area Change ------ -------- ; , , , ; . Parse named ; , , accept id or name via ; lists name when set. help and main help bullet updated for names. Unit tests for parsing, validation, and name/id resolution. User-facing docs This file, user-guide.md, quick-start.md, README.md, comprehensive-documentation.md, practical-guide-for-agents.md, CHANGELOG.md."}],keywords:["background","jobs","omnish","run","single","shell","command","asynchronously","in","the","current","chat","working","directory","while","you","keep","using","output","is","appended","to","log","file","under","your","data","can","pull","last","lines","or","only","new","bytes","since","for","that","job","this","commands","implemented","behavior","details","configuration","lifecycle","actual","statuses","completion","notifications","examples","compared","cowork","troubleshooting","change","named","2026-05-08"],relatedCommands:["/bg","/jobs","/tail","/log","/kill","/log abcdef12","/log mybuild","/stats","/history","/kill all","/shell","/src"]},{id:"docs-features-chat-llm-fallback",path:"docs/features/chat-llm-fallback.md",title:"Chat LLM fallback (optional)",summary:"When this feature is on, a plain inbound message that would normally get \u201CNo command matched\u201D is handled silently: omnish runs your in the background (with limits and a restricted environment), then sends the subprocess output back to the same WhatsApp or Telegram chat. In , the reply is printed to the terminal instead.",sections:[{title:"Where to change settings",body:"What Where ------ -------- Config file . If is unset, omnish uses (or the legacy tree if does not exist). Template / all keys Repo root \u2014 copy the entries into your real . Confirm data directory Run on the gateway host, or . Edit the file with any text editor on the gateway host (SSH, local console, or your usual config workflow). Valid JSON is required (trailing commas are not allowed). Reload behavior: The gateway calls when handling each inbound message, so changes to fields in normally apply on the next message without restarting . API keys and environment variables: The subprocess receives a filtered copy of the gateway process environment (see below). Keys such as are only present if they were set when the gateway was started. If you add or change API keys in the shell profile or systemd unit, restart so the parent process picks them up. ---"},{title:"How to enable (step by step)",body:"Open (or ) on the machine that runs . Add or merge these fields (defaults shown; enable only when ready): Set to a single shell command passed as the argument to (same pattern as sync commands). Use an absolute path to a wrapper script if the default in the sandbox is too minimal. Smoke test: With the gateway running, send a plain line from an allowlisted chat (not starting with or your , not text). You should get no immediate \u201CNo command matched\u201D reply; after the subprocess finishes, the combined stdout/stderr (trimmed and capped) is sent back. Sandbox / real isolation: omnish runs the command in a dedicated working directory (temp dir under the data directory unless is set) and a reduced env. For Docker, , or other tools, put that logic inside your wrapper and point at it. ---"},{title:"What the subprocess receives",body:"Shell: Config key (e.g. ). Working directory: if non-empty (created if needed); otherwise a new temp directory under the omnish data dir for that run (removed afterward). Stdin (default): The inbound chat text (truncated to ). If is true, stdin is not piped the same way; use (and a PTY) instead. Environment (high level): \u2014 e.g. or \u2014 same payload as stdin (piped mode) Common vars: , , , , , , , , All vars from the parent process Parent vars whose names end with or Place API keys in the gateway environment (systemd , shell before , etc.) so they are inherited and forwarded by the allowlist above. ---"},{title:"When the fallback runs (and when it does not)",body:"Runs only if all of the following hold: is true and is non-empty after trim. The message is plain text that reached the router\u2019s final \u201Cno match\u201D branch: not a command, not shell, not / , not app line, not consumed by a focused session, and free shell mode is off for that peer. Does not run (examples): unknown , , , media-only messages, messages dropped by cluster binding on non-primary hosts, or any path that already returns a normal reply. ---"},{title:"Related keys (reference)",body:"Key Purpose ----- -------- Master switch ( by default). Full command string for . Wall-clock limit (ms). Max size of text passed in (stdin / ). Max captured output (stdout+stderr in piped mode). Use PTY execution (for CLIs that require a TTY); prefer in the script. Fixed cwd; empty = ephemeral dir per run. Implementation checklist and code pointers: chat-llm-fallback-implementation-plan.md. ---"},{title:"Security note",body:"Allowlisted chats already have shell-level trust on the gateway host. Turning this on means accidental plain messages can invoke your command and any API usage inside it. Keep allowlists small, use timeouts and caps, and wrap external agents in a policy you control."}],keywords:["chat","llm","fallback","optional","when","this","feature","is","on","plain","inbound","message","that","would","normally","get","no","command","matched","handled","silently","omnish","runs","your","in","the","background","with","limits","and","restricted","environment","then","sends","subprocess","output","back","to","same","whatsapp","or","telegram","reply","printed","terminal","instead","where","change","settings","how","enable","step","by","what","receives","it","does","not","related","keys","reference","security","note"],relatedCommands:["/config help","/config","/absolute","/path","/to","/your-wrapper","/stderr","/bin","/bash","/apps","/foo","/help","/chat-llm-fallback-implementation-plan"]},{id:"docs-features-cluster-and-chat-config",path:"docs/features/cluster-and-chat-config.md",title:"Multi-host cluster and chat configuration",summary:"This document describes the chat-driven cluster ( , , ) and for viewing or editing from WhatsApp or Telegram.",sections:[{title:"Cluster (per-sender bindings)",body:`Use one WhatsApp phone number with multiple linked devices \u2014 each machine runs once. Every machine that runs will receive the same DMs through WhatsApp multi-device. There is no shared file. There is no Syncthing/NFS to set up. Coordination flows entirely through the WhatsApp chat itself: every reply omnish sends carries an invisible footer that other linked omnish hosts read to learn who is online and which machine each sender is currently bound to. Telegram cannot carry this coordination (different bot tokens cannot see each other), so the per-sender binding still applies on Telegram, but you only get convergence between linked hosts on WhatsApp. How it routes Each allowlisted sender (one phone number / one Telegram id) picks one machine to talk to. Only that machine processes the sender's normal traffic; other linked hosts stay silent for that sender. Two different controllers can independently bind to two different machines without affecting each other. A's only runs on . B's only runs on . Neither sees the other's output. Fleet commands ( , , ) are processed by every linked host so that bindings, status, and help stay reachable even before a sender has bound. Stable identity Each data directory has a file ( ) with a UUID. Display names in the roster come from , or the OS hostname when empty. When you , "alpha" matches against the (or the 8-character node id). Local state Each host keeps a private file at (schema v3): \u2014 short id, label, role, and last-seen timestamp for every host this machine has observed in chat traffic. \u2014 map of sender key ( or ) to the bound machine's short node id, when it was set, and whether it came from a chat command ( ) or config defaults ( ). This file is per host and never shared. Disagreements between linked hosts are resolved by the next footer-stamped message anyone sends to the chat. Commands (WhatsApp / Telegram) may be shortened to or (the bare token matches; does not). Command Action --------- -------- Overview of the per-sender model Bind YOUR messages to that machine. Resolves either an 8-character node id or a . Other senders are not affected. Bind YOUR messages to the local machine (shorthand for ). Show YOUR current binding (machine, source: chat or config). Clear YOUR chat binding. The config default in (if any) takes over again. Every online host replies with its own one-paragraph status (this is the discovery moment \u2014 each host parses the others' footers and updates its peer list). Locally known roster \u2014 only one host responds (the bound one, or a deterministic fallback). Convergence When you send , every linked host runs locally. Only the resolved target host (the one labelled ) sends the confirmation reply; siblings stay silent. The reply carries the cluster footer with , and every sibling observes it via WhatsApp upserts and converges on the same binding for your sender key. Defaults via config You can pre-seed bindings so a controller never has to type on first contact: Keys are sender keys ( or ). Values are the target machine's or its 8-character node id. Chat-set bindings ( ) always win over config defaults; clearing a chat binding ( ) falls back to the config default. CLI prints a short cluster line when is true. The legacy was removed \u2014 bindings need a sender, so use the chat ( ) or the new CLI form. Configuration keys (cluster) See the Configuration guide. Relevant fields: \u2014 display name for THIS machine in the roster \u2014 sender \u2192 machine defaults \u2014 kept for backwards compat, but no longer used for traffic gating Migration from v2 (single global active host) is automatically migrated on first read. The previous field is dropped \u2014 every sender now binds individually. If you relied on the old global-active model, set the equivalent entry for each allowlisted sender in , or send from each controller's chat. The footer protocol on the wire is unchanged; the field is now interpreted as "the node bound for this chat" rather than "the globally active node." ---`},{title:"Chat configuration (`/config`)",body:"Allowlisted chat users can change many settings without SSH. This is as sensitive as shell access: anyone who can DM as an allowed identity can modify config. Commands Command Action --------- -------- or Help (overview + hint) Snapshot of effective config; telegram bot token masked One whitelisted key Update one key; values may be quoted for paths with spaces List all keys allowed for After changes Save goes to immediately (same normalization as the rest of the app). For or , the gateway runs -style Telegram restart when is up; otherwise send after editing. Changing returns an explicit warning in the reply. Whitelist ( ) The canonical allowlist is in . In chat, prints the same comma-separated list your build supports. Groups include: gateway ( , \u2026); cluster; shell / sync / jobs; apps; files; recipes (including , ); Telegram ( ); service ( ); update checks ( , , , ); tunneling ( , , ); chat LLM fallback ( , , , , , , ). accepts a JSON object value (or / to wipe), e.g. . Not exposed via : , \u2014 use and in chat (or / on the host). Tunnel token is not in : run on the gateway host (or set ). Chat can turn tunneling on with after the token exists. Examples ---"},{title:"Related documentation",body:"Configuration guide \u2014 full file layout and fields Security model \u2014 trust boundaries and Message routing \u2014 slash-command ordering (high level)"}],keywords:["multi-host","cluster","and","chat","configuration","this","document","describes","the","chat-driven","for","viewing","or","editing","from","whatsapp","telegram","per-sender","bindings","/config","related","documentation"],relatedCommands:["/c help","/config help","/computers","/pcs","/config","/nfs","/node-id","/c use","/cluster-local","/cluster","/c here","/c using","/c unuse"]},{id:"docs-features-cowork",path:"docs/features/cowork.md",title:"Cowork \u2014 scheduled and on-demand shell tasks",summary:"Cowork runs saved shell commands on a timer while is active. It uses the same execution model as sync commands ( via your configured ), with and from . There is no AI layer.",sections:[{title:"Prerequisites",body:"A running gateway: (foreground or background). You must be allowlisted (same trust as ). With cluster enabled, only the host bound to your sender processes chat commands; tasks you create are stored on that machine\u2019s data directory."},{title:"Commands",body:"Use or . Send for the short form. Action Example -------- --------- Add task List Show Run now (on-demand or scheduled) Enable / disable / Remove (also , ) Check in (heartbeat only) Update \u2014 notify condition (see below) \u2014 also send the run log file with each notify \u2014 optional artifact paths (see below) Add syntax Scheduled / on-demand tasks (run a command): Heartbeat tasks (dead-man's-switch, no command): Name: letters, digits, , ; max 32 characters (stored lowercased). Schedule: (or ), , , , (weekday names or \u2013 , Sunday = ), or . For scheduled/on-demand tasks, the command must come after a literal separator. Heartbeat tasks have no command. Duration format (heartbeat): , , , . Minimum interval: 1m. Minimum grace: 30s. Grace defaults to 50% of interval if omitted. Schedules and time zones Fire times use the gateway host\u2019s local timezone (Node\u2019s on that machine). Output logs Each run writes one UTF-8 log file under the task\u2019s (default ). Filenames include a timestamp, task id, and whether the run was scheduled, catch-up, or on-demand. Catch-up Successful scheduled runs are recorded in (per task id and slot time). The scheduler uses that database as the source of truth for what is already done; may still contain a legacy field, which is seeded into SQLite on first open if the DB has no rows for that task. If several slots were missed (gateway down), the next tick runs the command once for the newest due slot and records coalesced rows for older missed slots so the backlog clears in a single execution instead of one run every 30 seconds per missed slot. A slot is only recorded after exit code 0, no timeout, and no terminating signal; failed runs do not advance the watermark (same retry behavior as before). Kinds stored for successful scheduled work: (within 2 minutes of the slot), (late), (satisfied without a separate run during backlog coalescing), and (imported from legacy on upgrade). On-demand queue appends a request to . The scheduler dequeues up to eight entries per tick, one read/write of the queue file per batch, so a crash mid-run does not drop the rest of the queue. Notifications After each run, a short summary can be sent (same routing as other replies: WhatsApp vs Telegram formatting). For scheduled and catch-up runs, the message is one line: The bracketed status appears only when the run did not finish cleanly (timeout, non-zero exit, or signal). For on-demand runs ( ), the message is two lines: Optional lines may follow when attachments fail or when too many artifact files match (see below). Mode Recipients ------ ------------ (default) Task owner only All entries in (WhatsApp) All entries in Owner plus both allowlists No messages Trust: , , and can message every allowlisted identity. Anyone who can edit tasks (allowlisted senders) can point shell and notifications at powerful actions\u2014same overall model as remote shell. WhatsApp notification targets use normalized JIDs so delivery matches Baileys . Conditional notifications ( ) Control when a notification is sent with : Mode Behavior ------ ---------- (default) Notify after every run Notify only when the command exits with a non-zero code, times out, or is killed by a signal Notify only when the result flips (e.g. ok to fail, or fail to ok). The last-known state is tracked in SQLite so it survives gateway restarts. is useful for tasks that run frequently (e.g. hourly health checks) where you only want to know when something breaks or recovers, not on every successful run. Heartbeat (dead-man's-switch) A heartbeat task does not run a command. Instead, it expects periodic check-ins and alerts when they stop arriving. This creates a task that expects a check-in at least every 1 hour, with a 10-minute grace period. If no check-in arrives within 1h10m of the last one, a missed-heartbeat alert is sent. Check in from chat or from a script: From a script (e.g. at the end of a cron job or backup pipeline): Recovery: when check-ins resume after a missed heartbeat, a recovery "},{title:"Data files",body:"Under (default unless overridden): \u2014 task definitions (atomic replace on save). \u2014 successful scheduled slot completions (watermark for catch-up); WAL mode. \u2014 queued on-demand runs. Directory mode is restrictive ( for where created by the app; task file )."},{title:"Limitations and caveats",body:"Gateway must be up at fire time for scheduled runs; catch-up handles downtime afterward. WhatsApp-only: the cowork scheduler starts before the first successful WhatsApp connection; very early completion notifications to WhatsApp may no-op until outbound is ready. Telegram outbound is typically ready earlier when the bot is enabled. Shared : if several gateways share the same data directory (unusual), each running process could execute the same schedules\u2014use one data dir per machine for normal setups. Cluster: tasks live on disk for the gateway that received commands; they do not sync across hosts."},{title:"See also",body:"Cowork implementation plan (design and maintainer notes) User guide \u2014 Cowork section Security model"}],keywords:["cowork","scheduled","and","on-demand","shell","tasks","runs","saved","commands","on","timer","while","is","active","it","uses","the","same","execution","model","as","sync","via","your","configured","with","from","there","no","ai","layer","prerequisites","data","files","limitations","caveats","see","also"],relatedCommands:["/cowork help","/cw help","/cowork","/cw","/cowork add","/data","/backup","/cowork list","/cowork show","/cowork run","/cowork enable","/cowork disable","/cowork remove"]},{id:"docs-features-device-update-delivery",path:"docs/features/device-update-delivery.md",title:"Update information on installed Omnish devices",summary:"This document is the reference plan for how operators and maintainers can reach already-installed Omnish gateways with version and notice information. It matches what the CLI and gateway implement today.",sections:[{title:"Reality check: there is no silent \u201Cpush\u201D to every machine",body:"Omnish is self-hosted: each install is a process on a user\u2019s machine, talking to WhatsApp/Telegram. There is no central fleet server and no always-on back channel from the project to those hosts unless the host initiates outbound traffic (or an allowlisted user sends a chat command). So 100% delivery in the literal sense (every device, every time, with no user action) is not possible: machines can be offline, firewalled, on air-gapped networks, or running an old binary forever. What is achievable is a reliable, predictable path that works whenever the network and registry are reachable\u2014same practical bar as other CLIs ( , , etc.)."},{title:"Options that were considered",body:"Approach Pros Cons ---------- ------ ------ npm registry Canonical published version; no custom infra; HTTPS; already used for installs Needs outbound HTTPS; scoped/unpublished forks differ GitHub Releases / API Rich metadata Rate limits; not identical to \u201Cwhat gets\u201D Static JSON on a URL you control ( ) Arbitrary maintainer text (security, migration) You must host and secure expectations (HTTPS only in omnish) In-chat broadcast from \u201Cthe project\u201D Uses existing DM surface There is no project-owned chat to all installs; only allowlisted users can command their host Auto-upgrade in place Hands-off for users High risk (native deps, , Baileys); out of scope for this design doc Chosen combination: npm registry for semver discovery + optional HTTPS JSON for human notices, exposed in the product as , (cached one-liner), , optional background checks when is true, and for the same keys."},{title:"Implemented behavior (source of truth)",body:"(allowlisted chat, gateway running): performs a live GET to (default package name ). If is set to an https URL, fetches JSON (link must also be ). : shows the last in-memory snapshot from the last live or scheduled check (no network). : gateway runs a timer (checks at most every 1 minute internally, but only performs a registry+info fetch when has elapsed, clamped 1h\u20137d). Results are logged with and stored for / . : CLI one-shot check (same fetches as ). : completion text may append \u201CUpdates (last check): \u2026\u201D if a snapshot exists. Configuration keys (also in and ): (boolean, default false) \u2014 privacy-first default. (default 86400000) \u2014 clamped between 1 hour and 7 days. (default ) \u2014 for forks or scoped packages. (default empty) \u2014 optional maintainer notice JSON over HTTPS."},{title:"Maintainer playbook",body:"Publish a new version to npm as today ( bump, publish). Devices that run or have background checks enabled will see the new latest when the registry updates. Optional notice (security advisory, breaking change): host a static JSON file at an HTTPS URL you control; set in docs or tell users to set it via . Do not rely on chat alone to \u201Creach\u201D every install; treat registry + optional URL as the scalable channel."},{title:"Code map",body:"Piece Role ------- ------ npm + optional info URL fetch, snapshot, scheduler Numeric compare for \u201Cnewer on npm\u201D Reads running from package root (dev + bundled ) , Schedules checks; ; reload footer status lines, formatted reply Schema defaults and merge"},{title:"Future extensions (not implemented)",body:"Signed notices (e.g. minisign) over . Deprecation warnings via (visible on , not parsed here)."}],keywords:["update","information","on","installed","omnish","devices","this","document","is","the","reference","plan","for","how","operators","and","maintainers","can","reach","already-installed","gateways","with","version","notice","it","matches","what","cli","gateway","implement","today","reality","check","there","no","silent","push","to","every","machine","options","that","were","considered","implemented","behavior","source","of","truth","maintainer","playbook","code","map","future","extensions","not"],relatedCommands:["/updates","/updates cached","/telegram","/latest","/unpublished forks","/gateway","/config set","/registry","/reload","/config keys","/omnish-notice","/check"]},{id:"docs-features-docs-search-from-chat",path:"docs/features/docs-search-from-chat.md",title:"Documentation search from chat",summary:"Find omnish guides by topic from WhatsApp, Telegram, or \u2014without hunting the repo or omnish.dev first. Search is offline (bundled index at build time; no AI layer).",sections:[{title:"Commands",body:"Command Purpose --------- --------- Subcommand list Ranked results (keywords + headings) Repeat the last search list in this chat or Excerpt, GitHub link, and Try: related slash commands Run the primary related help (e.g. ) Alias for Host terminal (same index): ---"},{title:"Example flow",body:"You are not sure which command exposes HTTP: Pick a result: You get a short excerpt, the doc path, a GitHub link, and lines like . Jump to live help: Same as sending in that chat. ---"},{title:"When nothing matches a slash command",body:"Plain text that looks like a question (contains a space or ) gets a hint on No command matched: Use with your own keywords if the auto-filled phrase is too long. ---"},{title:"Index scope",body:"The build includes guides, features, architecture, and advanced troubleshooting under (not , , or maintainer runbooks). Rebuild the index with: ( and run this automatically.) Overrides for related commands: . ---"},{title:"Related",body:"User guide Message routing Online catalog \u2014 community recipes/apps (separate from docs search)"}],keywords:["documentation","search","from","chat","find","omnish","guides","by","topic","whatsapp","telegram","or","without","hunting","the","repo","dev","first","is","offline","bundled","index","at","build","time","no","ai","layer","commands","example","flow","when","nothing","matches","slash","command","scope","related"],relatedCommands:["/docs help","/docs search","/docs list","/docs","/docs show","/docs follow","/tunnel help","/help search","/features","/tunneling","/doc-chat-overrides","/scripts"]},{id:"docs-features-implementation-101",path:"docs/features/implementation-101.md",title:"Tunneling implementation 101",summary:"This document explains how omnish tunneling is built: relay edge, client, CLI, chat integration, configuration, security, and how to exercise it locally.",sections:[{title:"Big picture",body:"Tunneling is a relay + client design. The relay is the public edge; the omnish client on the user machine keeps an outbound WebSocket and forwards traffic to a local port. HTTP uses JSON control messages on the WebSocket. TCP uses JSON for stream open/close and length-prefixed binary frames for payload. Default production relay: . The relay service itself lives under and is deployed separately from the npm CLI package."},{title:"Repository map",body:"Path Role ------ ------ Deployable relay: HTTP edge, WSS control, TCP listeners Relay package ( dependency) Shared control message types and binary frame codec Tunnel kinds, records, default relay URL One tunnel: WSS session, local forwarding Active tunnels, limits, stop/stopAll Token and relay URL resolution CLI and chat argument parsing subcommands handlers for the gateway CLI dispatch; gateway shutdown stops tunnels Chat routing for and , , under the data dir Posture findings for tunneling lines when tunneling is enabled Tests: , , , ."},{title:"Relay (`contrib/tunnel-relay/`)",body:"is the tunnel process. Production ships it in one Docker image with Caddy (TLS, wildcard DNS-01) via and . proxies public / to and . Listeners Listener Default Role ---------- --------- ------ HTTP edge Public HTTP for tunneled apps Control WSS Authenticated client connections Environment variables: \u2014 base URL shown to users (default ) \u2014 HTTP edge bind port \u2014 control WebSocket bind port / \u2014 TCP tunnel port range \u2014 comma-separated bearer tokens allowed to connect \u2014 per-token tunnel quota Authentication Clients connect with on the WebSocket upgrade. Invalid or missing tokens close the socket. Registration After , the client sends with ( ), , , and optional . The relay assigns a slug (from or random) and replies with including . HTTP routing Local dev: Production-style: when matches that host pattern Fallback: Each slug is bound to the WebSocket session that registered it (not \u201Cany session with the same token\u201D), so multiple tunnels sharing one token each get correct routing. Incoming requests are turned into control messages to the owning client; the client returns . TCP For , the relay binds a port in the configured range and returns . New public TCP connections emit on the control socket; bytes flow over binary frames. State In-memory maps ( , , ). Tunnels disappear when the client disconnects. No cross-replica sticky routing in v1."},{title:"Shared protocol (`src/tunnel/protocol.ts`)",body:"Control messages (JSON on the WebSocket): Session: , , Lifecycle: , , , HTTP: , (optional ) TCP: , Keepalive: , Binary frames (TCP payload): 1 byte frame type ( , , , ) 4 byte stream id (big-endian) 4 byte payload length payload bytes and implement the codec on client and relay."},{title:"Client (`src/tunnel/client.ts`)",body:"represents one active tunnel. Derives from the relay URL ( \u2192 , default path ). Connects with the bearer token. Sends with a random 8-hex . Waits for and stores and . HTTP path: On , issues to , then sends with status, headers, and optional base64 body. TCP path: On , connects locally and pumps bytes via binary frames; or relay close tears down the stream. Lifecycle: Periodic ; on stop, sends and closes the socket. Default target host is unless overrides it."},{title:"Manager (`src/tunnel/manager.ts`)",body:"holds live instances keyed by tunnel id and slug. Resolves relay URL via and token via Enforces from config / for CLI, chat, and gateway shutdown The gateway and CLI share one manager instance from ( )."},{title:"Configuration and secrets",body:"In (non-secret): \u2014 gate chat commands (default ) \u2014 default relay origin (default ) \u2014 max concurrent tunnels on this host (default ) Secrets (not in ): or ( ) via Optional relay override: or in the auth file See and ."},{title:"CLI (`omnish tunnel`)",body:"Implemented in ; wired from . Subcommand Behavior ------------ ---------- Save token (and optional relay) to Remove saved token Register HTTP tunnel; foreground unless Register TCP tunnel List active tunnels on this machine Stop one tunnel Relay reachability and auth presence Flags: , , , (see )."},{title:"Chat integration",body:"When is true, handles: / (optional , ) delegates to the shared . Tunnels run inside ; standalone still works without the gateway. Trust model matches : allowlisted chat users can open tunnels as the gateway OS user. Public URL possession is the visitor credential."},{title:"Security",body:"findings: \u2014 chat tunneling on \u2014 chat tunneling on without a token \u2014 non-default documents tunnel URLs as capabilities and the gateway shutdown path that stops active tunnels."},{title:"Local exercise",body:"Install relay deps: Start relay, for example: - - Run a local HTTP server on a port (for example ) Open the printed (path-based on loopback: ) Automated coverage: spins the relay and asserts HTTP end-to-end."},{title:"Out of scope (v1)",body:"Mandatory omnish.dev account or billing Tunnel persistence across client disconnect or HA relay fleet UDP, mesh VPN, or bundled ngrok/cloudflared as the primary path Setup UI for tunnel login (CLI is the v1 configuration surface)"},{title:"Success criteria (from product plan)",body:"+ yields a public URL that serves the local app (with a running relay) exposes a public TCP endpoint to the local port With and the gateway running, allowlisted users get the same URLs from chat and docs describe capability risk clearly"}],keywords:["tunneling","implementation","101","this","document","explains","how","omnish","is","built","relay","edge","client","cli","chat","integration","configuration","security","and","to","exercise","it","locally","big","picture","repository","map","contrib/tunnel-relay/","shared","protocol","src/tunnel/protocol","ts","src/tunnel/client","manager","src/tunnel/manager","secrets","tunnel","local","out","of","scope","v1","success","criteria","from","product","plan"],relatedCommands:["/tunneling","/architecture","/security","/close and","/tunnel","/tunnel-relay","/server","/contrib","/package","/protocol","/src","/types"]},{id:"docs-features-media-commands",path:"docs/features/media-commands.md",title:"Media commands (`/dl`, `/tr`, `/edit`)",summary:"Download, transcribe, and edit media from chat \u2014 using yt-dlp, ffmpeg, and optionally openai-whisper on the gateway host.",sections:[{title:"Defaults",body:"Files are sent to chat by default ( ). To only list paths, set:"},{title:"Install tools (host)",body:"Binaries are stored under . Whisper uses . From chat (same trust as shell \u2014 off by default):"},{title:"Chat commands",body:"Command Action --------- -------- Download file or video (background job) Tool status OS-specific manual install steps Install tools into Whisper transcript + (+ video if URL); background job Trim or convert with ffmpeg; background job flags: / , / , / , / , . Direct file URLs vs streaming pages picks the method from the URL path: Direct file link (pathname ends with a known extension such as , , , , ) \u2014 HTTP download with (no yt-dlp/ffmpeg required) No file extension (e.g. YouTube watch pages) \u2014 yt-dlp best video (+ ffmpeg for merge) Examples: \u2192 direct file; \u2192 yt-dlp. Background jobs , , and always run as background jobs ( via ). You get a job id immediately; use and to follow output. Optional flags (stripped before the payload): / \u2014 ping in chat when the shell job finishes (exit status) / \u2014 accepted for compatibility (no-op; already background) Step progress When is true (default), multi-step work sends chat messages such as while it runs (via the gateway control channel). Disable with . sends a completion summary when the shell job ends; step progress is separate and on by default. Auto-detect URLs When is true, a message that is only an or URL runs as a background job (direct HTTP for -style links; yt-dlp for extensionless streaming URLs). Legacy still works as aliases: / \u2192 , / \u2192 . Other modes show a deprecation hint."},{title:"Config keys",body:"Key Default Purpose ----- --------- --------- Send files to chat (else paths only) Allow from chat Lone URL \u2192 Output root (empty \u2192 ) yt-dlp cap (0 = none) Whisper model Step-by-step chat messages during multi-step work / / Binary overrides Older keys in are migrated on load (e.g. \u2192 )."},{title:"Legal and safety",body:"Same allowlist trust as shell commands. Private/local URLs are blocked ( , RFC1918, etc.) when the job runs. Respect copyright and platform terms; omnish only runs tools you install locally."},{title:"See also",body:"Files send/receive \u2014 for manual delivery Background jobs \u2014 , ,"}],keywords:["media","commands","/dl","/tr","/edit","download","transcribe","and","edit","from","chat","using","yt-dlp","ffmpeg","optionally","openai-whisper","on","the","gateway","host","defaults","install","tools","config","keys","legal","safety","see","also"],relatedCommands:["/dl","/tr","/edit","/bin","/venvs","/whisper","/config set","/dl install","/dl doctor","/dl setup","/ffmpeg required","/cdn"]},{id:"docs-features-media-pull",path:"docs/features/media-pull.md",title:"Media pull (`/pull`) \u2014 deprecated",summary:"> Superseded by media commands ( , , ). This page is kept for older releases.",sections:[{title:"Enable",body:"In (or from an allowlisted chat):"},{title:"Install tools (host)",body:"Binaries are stored under . Whisper uses . From chat (same trust as shell \u2014 off by default):"},{title:"Chat commands",body:"Command Action --------- -------- Usage Tool status OS-specific manual install steps Best video (needs ffmpeg) Audio extract (m4a) Subtitles (en + auto) Whisper speech-to-text video + audio + subs + transcript (background job) Flags: / \u2014 run as job / \u2014 notify in chat when the background job finishes and always run in the background. / / run synchronously unless is set. Auto-detect URLs When is true, a message that is only an or URL runs with (default ). Send results back to chat When is true, files under (and the 8 MiB attached-mode cap) are sent with automatically. Otherwise the reply lists paths \u2014 use manually."},{title:"Config keys",body:"Key Default Purpose ----- --------- --------- Master switch Allow Lone URL \u2192 pull Default mode Output root (empty \u2192 ) yt-dlp cap (0 = none) Push files to chat Whisper model / / Binary overrides"},{title:"Legal and safety",body:"Same allowlist trust as shell commands. Private/local URLs are blocked ( , RFC1918, etc.). Respect copyright and platform terms; omnish only runs tools you install locally."},{title:"See also",body:"Files send/receive \u2014 after a pull Background jobs \u2014 , ,"}],keywords:["media","pull","/pull","deprecated","superseded","by","commands","this","page","is","kept","for","older","releases","enable","install","tools","host","chat","config","keys","legal","and","safety","see","also"],relatedCommands:["/pull","/dl","/tr","/edit","/config","/config set","/bin","/venvs","/whisper","/pull install","/pull help","/pull doctor"]},{id:"docs-features-monetization",path:"docs/features/monetization.md",title:"Monetization \u2014 device-first omnish",summary:"omnish keeps WhatsApp and Telegram as the free control plane: allowlisted messages run on the user\u2019s machine. Paid tiers charge for reachability, identity, team boundaries, and safety nets\u2014not for sending a command.",sections:[{title:"Who pays at ~$10/month",body:"Someone who already runs on a home server, laptop, or Mac mini and wants the inbox to stay useful when they are away from the desk: show a dev server, start a long job, drive a TUI agent, tail logs, or hand a link to someone else. Free omnish sells my phone is a remote control for my computer. Paid omnish sells that control still works when I need a stable public URL, more than one tunnel, or light team structure\u2014without being my own ngrok admin."},{title:"Strongest first paid wedge",body:"Hosted tunneling on , integrated with the same chat and CLI, is the most natural first paid feature. Execution stays on their box; omnish runs the edge they would otherwise assemble (TLS, wildcard subdomains, relay uptime, abuse limits). For agent builds on the user\u2019s machine, steered from chat: the agent runs locally; Plus provides a shareable HTTPS preview via or , while iteration stays in WhatsApp or Telegram. That is not a cloud IDE\u2014it is a link that works while code and processes stay on their hardware. Plus ($10) \u2014 draft shape Several concurrent HTTP tunnels (free: self-hosted relay only, or one hosted tunnel with ephemeral names). Reserved slugs so does not change every session. Caps on bandwidth, tunnel lifetime, and concurrent tunnels so hosted relay cost stays bounded. TCP tunnels optional on Plus or stricter on free (TCP is riskier to operate). Comparable spend to ngrok-class tools; the hook is already inside omnish, not a separate dashboard."},{title:"Second layer: team and accountability",body:"Solo users may stay on free plus self-hosted relay. Small teams pay when my phone controls our box needs structure: More than one allowlisted identity with clearer roles (run vs read-only vs tunnel-only). Audit trail: who ran what, which tunnels opened, when\u2014exportable for a client or cofounder. Named machines in cluster mode without ambiguous shared binding. Still device-first: gateway on their hardware; paid layer is policy and visibility."},{title:"Third layer: reliability",body:"Weaker as the only $10 hook unless they depend on the gateway daily; pairs well with hosted tunnels: Offline alerts when stops heartbeating. Guided boot / service setup with a simple health view (gateway up, last command, disk, tunnel count). Optional backup / restore of omnish data (shortcuts, cowork defs, session cwd maps)\u2014not the whole disk."},{title:"What not to lead with at $10",body:"Hosted agent sandbox \u2014 fights device-first positioning; competes with Cursor, Replit, etc. Generic AI \u2014 conflicts with no-AI, your-shell positioning. Per-message chat fees \u2014 trains workarounds. Security theater \u2014 erodes trust on remote shell access."},{title:"Packaging sketch",body:"Tier Free Plus $10 Pro (later) ------ ------ ----------- ------------- Chat \u2192 your shell Yes Yes Yes Self-hosted relay Yes Yes Yes Omnish-hosted HTTPS tunnels No / very limited Yes, with caps Higher caps Stable / custom names No Reserved slug Custom domain Team / audit Basic allowlist Small team + logs More seats, exports Gateway monitoring DIY Optional alerts SLA-style support One-liner: Keep controlling your machine from WhatsApp and Telegram for free; pay for public preview links, stable names, and team guardrails when that is how you work."},{title:"When someone will pay",body:"They pay when hosted tunnels remove a recurring pain: client demos, \u201Copen this while I change it from chat,\u201D or not maintaining Caddy, Cloudflare, and a relay on a VPS. Remote shell alone may stay free forever\u2014that supports adoption, not revenue."},{title:"Sequencing",body:"Plus = hosted tunnel + limits + account/token; chat stays unlimited. Team audit when paying users need shared access. Custom domains when reserved slugs feel tight. The preview-from-chat loop on their machine is credible paid value only if the URL is stable, HTTPS, and boring\u2014that is worth money while the shell stays free and local."}],keywords:["monetization","device-first","omnish","keeps","whatsapp","and","telegram","as","the","free","control","plane","allowlisted","messages","run","on","user","machine","paid","tiers","charge","for","reachability","identity","team","boundaries","safety","nets","not","sending","command","who","pays","at","10/month","strongest","first","wedge","second","layer","accountability","third","reliability","what","to","lead","with","10","packaging","sketch","when","someone","will","pay","sequencing"],relatedCommands:["/month someone","/tunnel","/theirname","/token"]},{id:"docs-features-online-catalog",path:"docs/features/online-catalog.md",title:"Online catalog",summary:"Share and install recipes, app templates, cowork tasks, and shortcuts across the omnish community. Browse and download from chat; publish when logged into the platform.",sections:[{title:"Discover and install (chat)",body:"Browse from the command family you care about \u2014 each prefix filters the catalog to the matching kind in MongoDB: Prefix Kind filter Example -------- ------------- --------- all kinds (optional on trending/search) only only only Shared subcommands (replace with , , , or ): Command Purpose --------- --------- Catalog subcommands for that family Most downloaded (kind-scoped when not ) Text search Full payload (review before install) Install to gateway-shared storage Install item #n from the last list in that family Repeat the last trending/search list for that family Examples: Use or on download to install for this chat only instead of gateway-shared: Numbered lists are per family \u2014 uses the last list, not a prior list. Platform URL: set in config (default hosted relay). Browse does not require a token; the CLI uses your configured relay origin. ---"},{title:"Publish (requires platform account)",body:"Sign up on the relay dashboard or run , then publish from chat: Command What is published Catalog --------- ------------------- ---------------- User or gateway-shared recipe (not built-ins) Running PTY session command (session must be alive) Cowork task for this chat Shortcut (chat or shared) On success you get a (e.g. ). Others install with the matching prefix, e.g. or . Flags: , , , ---"},{title:"What gets installed",body:"Kind Local storage ------ ---------------- gateway-shared bucket Same as recipe (category ; use with stored command) gateway-shared bucket (owned by the importing chat) ---"},{title:"Security",body:"Recipes and app commands can run shell on your machine. Always before download. The platform rejects dangerous recipe flags and invalid command shapes at publish time. Download does not auto-run \u2014 it only adds the template locally. ---"},{title:"API",body:"Hosted relay routes are documented in Platform reference \u2014 Catalog. Related: Recipes / \xB7 Cowork \xB7 Shortcuts"}],keywords:["online","catalog","share","and","install","recipes","app","templates","cowork","tasks","shortcuts","across","the","omnish","community","browse","download","from","chat","publish","when","logged","into","platform","discover","requires","account","what","gets","installed","security","api"],relatedCommands:["/run online help","/apps online help","/guides","/platform-reference","/run online","/search","/apps online","/cowork online","/shortcut online","/run","/apps","/cowork","/shortcut","/search list"]},{id:"docs-features-run-queue",path:"docs/features/run-queue.md",title:"`/run` queue (`-q` / `--queue`)",summary:"The run queue runs recipe launches one at a time per chat, in FIFO order. It is meant for back-to-back agent jobs (e.g. several tasks) without starting multiple PTYs at once.",sections:[{title:"Syntax",body:"and are equivalent (case-insensitive). Short form works the same way. Combine with attach flags after the recipe name (same as non-queued ): attaches the queue head on start; without / , the head starts detached (default; see in Configuration). Not the same as the slash subcommand (status) or ."},{title:"Loading many jobs from JSON (`/run queue load`)",body:"You can enqueue multiple recipe tasks in one step from a JSON payload. Each row is validated like a separate (same recipe resolution, task length limits, and behavior). The queue still runs them one at a time in order. Ways to provide the JSON Method What to send -------- ---------------- Host file path \u2014 path is resolved from this chat\u2019s session cwd (same idea as ): relative segments, globs, and quoted paths work like file selection elsewhere. Exactly one file must match. Inline JSON \u2014 everything after the word (with separating whitespace) is parsed as a single JSON value. Keep the payload on one message line if your client splits on newlines. Inbound file + caption Attach a document (e.g. ) and set the message text to (no path). Omnish uses the saved upload path from that same inbound turn (see Files \u2014 send & receive). You still get the usual \u201CSaved: \u2026\u201D line before the queue result. JSON format The file or inline text must be valid JSON in one of these shapes: Array of jobs (most common): Object with a single property (optional wrapper): Rules: Each job object must contain only two keys: and , both strings. Extra keys are rejected (catches typos like vs ). Top-level object form must be exactly \u2014 no other top-level keys. There must be at least one job. Empty arrays are rejected. Maximum 64 jobs per load. Not supported: raw , , or arbitrary shell in JSON. That would bypass recipe validation; only + are accepted, and omnish builds each run the same way as . File size limit for When reading a JSON file from disk (path argument or saved attachment path), the read is capped: If in is greater than zero, that value is used. If it is zero (no inbound cap), queue-load still uses a 1 MiB ceiling so a huge file cannot be pulled into memory unintentionally. Batch behavior and replies Jobs are enqueued in array order (first element becomes the next head when the queue is idle, or waits behind the current head). Omnish returns one reply that concatenates the status line from each enqueue step (started head, \u201Cwait slot \u2026\u201D, paused, etc.), same semantics as sending several messages in sequence. , , pauses, and clean-exit rules are unchanged \u2014 see Status and control and Pauses and failures. See also (files) Files \u2014 send & receive \u2014 where uploads land, , per-chat ."},{title:"What actually happens",body:"In memory only \u2014 Each waiting item is a small record (command + env + recipe label). Nothing runs until it becomes the head of the queue. Head starts immediately \u2014 When you enqueue and nothing else is running as the queue head, omnish shifts the first item off the FIFO and starts one app session (PTY), same as a normal . Others wait \u2014 Additional / calls append to the waiting list. No extra processes are spawned for those rows; the next job starts only when the previous head session exits with exit code 0 and signal 0 (clean exit). Per chat \u2014 Queue state is keyed by the chat ( ), not global across all users."},{title:"Why `/run queue` can show `Pending: 0` right after you queued something",body:"counts only jobs that have not started yet. The first item you add with / is removed from the pending list as soon as it starts; it then appears under Active with the session name and recipe label. Example: you send three queued runs in a row while the gateway is up: After message 1: Active = new session, Pending = 0 (nothing left waiting). After message 2: Active still the first session, Pending = 1 (second job waiting). After message 3: Pending = 2 (second and third waiting). So after a single enqueue is normal \u2014 it means the one job is already running, not that the queue \u201Clost\u201D your task. If you used before omnish accepted , the old parser treated as part of the task text and ran non-queued instead; the queue stayed empty. Use or after the recipe name (current omnish supports both)."},{title:"Status and control",body:"\u2014 Shows Active (session + recipe), Pending (with a short numbered list of waiting recipe labels), Paused, and a reminder that the next item auto-starts only after a clean exit. \u2014 Clears the paused flag and tries to start the next waiting item (e.g. after you fixed the host or session limits). If a head session is still running, resume tells you to wait until it finishes cleanly. \u2014 Enqueue many jobs from JSON (file path, inline , or attachment + caption); see Loading many jobs from JSON."},{title:"Pauses and failures",body:"Non-clean exit (non-zero exit code, or non-zero signal, e.g. SIGKILL) on the queue head \u2192 the queue pauses; waiting items stay in the FIFO until you (or fix limits and resume). Failed spawn (e.g. per-chat app limit reached) when trying to start the head \u2192 queue pauses and the item is put back; fix the error, then ."},{title:"Resource model",body:"Waiting rows: RAM only (no polling timers for the queue itself). Running head: one PTY + child process, same cost as a normal . Not persisted \u2014 If the gateway process ( ) restarts, the in-memory queue is cleared. Long-term scheduling belongs in Cowork or your own job runner."},{title:"See also",body:"System agents and User guide \u2014 shortcuts vs Interactive sessions ( )"}],keywords:["/run","queue","-q","--queue","the","run","runs","recipe","launches","one","at","time","per","chat","in","fifo","order","it","is","meant","for","back-to-back","agent","jobs","several","tasks","without","starting","multiple","ptys","once","syntax","loading","many","from","json","load","what","actually","happens","why","can","show","pending","right","after","you","queued","something","status","and","control","pauses","failures","resource","model","see","also"],relatedCommands:["/run help","/run queue","/run","/run remosh","/guides","/configuration","/to","/file","/send","/files-send-receive","/receive here","/run name","/system-agents-and-run"]},{id:"docs-features-service-from-chat",path:"docs/features/service-from-chat.md",title:"Service commands from chat (`/service`)",summary:"After WhatsApp or Telegram is connected and your identity is allowlisted, you can manage background gateway setup from the same DM thread \u2014 without SSH.",sections:[{title:"Commands",body:"Command Purpose --------- --------- Overview OS, data directory, , resolved Node + entry script paths Copy-paste steps for this host (paths filled by the running gateway) Last n lines of the default gateway log (default 80, max 120) Writes a user-level unit (Linux systemd or macOS LaunchAgent). Requires . Removes that unit (same gate). Windows: instructions only."},{title:"Trust model",body:", , and are safe to use like any other slash command (same allowlist as ). and modify login/boot integration files under your home directory. That is equivalent to shell access: anyone who can DM as an allowed identity can trigger them once is true in . Default: is . Enable only when you trust every entry on / as much as SSH. When enabled, reports a warning for ."},{title:"Relation to `CHANGE_ME` templates",body:"The contrib plist/service/XML files use placeholders for manual edits. bypasses that by injecting live paths from the running process. writes generated units with those paths automatically."},{title:"See also",body:"Background gateway and start on boot Cluster and chat configuration ( )"}],keywords:["service","commands","from","chat","/service","after","whatsapp","or","telegram","is","connected","and","your","identity","allowlisted","you","can","manage","background","gateway","setup","the","same","dm","thread","without","ssh","trust","model","relation","to","change","me","templates","see","also"],relatedCommands:["/service help","/service","/service status","/service instructions","/service logs","/service install","/config set","/service uninstall","/help","/boot integration","/contrib","/xml files"]},{id:"docs-features-sessions",path:"docs/features/sessions.md",title:"Interactive Sessions - omnish",summary:"Interactive sessions provide full terminal access within your messaging chats, enabling you to run TUI applications, REPLs, and interactive tools directly from WhatsApp or Telegram.",sections:[{title:"Overview",body:"Interactive sessions use to create pseudo-terminal sessions that mimic real terminal behavior. Each chat can maintain multiple named sessions with independent state. Key Features PTY-based: Full terminal emulation Named sessions: Multiple sessions per chat Focus management: One session attached at a time Output streaming: Real-time output with debouncing ANSI support: Color output and formatting Session persistence: State maintained across chats"},{title:"Session Management",body:"Starting Sessions Session Limits Per chat: Default 5 sessions (configurable) Global: Default 20 sessions (configurable) Named: Each session has a unique name per chat Plain messages and free shell For a single incoming line with no , , or , the router sends text to the attached session first when it is running; free shell mode ( ) applies only if nothing is attached (or the focused session is not running). Use for a one-off sync shell line while attached. Session States Created: Session initialized but not started Running: Session active and accepting input Attached: Session is focused for input Detached: Session running but not focused Stopped: Graceful shutdown requested Killed: Force termination Exited: Process terminated normally"},{title:"Session Commands",body:"Basic Operations Input Control Output Management Session Information"},{title:"Configuration",body:"Session Limits Terminal Settings Behavior Settings When , , or similar shows a password prompt, omnish detects it in recent terminal output and does not send the readline clear keys ( ) for your next reply \u2014 those keys break no-echo password readers and often appear as literal in chat. A one-time hint is sent unless is false. Passwords are still written to the session log on disk."},{title:"Use Cases",body:"Development Workflows ```text"}],keywords:["interactive","sessions","omnish","provide","full","terminal","access","within","your","messaging","chats","enabling","you","to","run","tui","applications","repls","and","tools","directly","from","whatsapp","or","telegram","overview","session","management","commands","configuration","use","cases"],relatedCommands:["/apps help","/apps start","/apps list","/apps attach","/apps detach","/apps stop","/apps kill","/apps send","/apps key","/apps tail","/apps since","/apps mute","/apps raw"]},{id:"docs-features-tunneling",path:"docs/features/tunneling.md",title:"Tunneling \u2014 omnish",summary:"omnish tunneling publishes a public URL that forwards to a local HTTP or TCP port on the machine running the tunnel client. The default relay is .",sections:[{title:"CLI (primary)",body:"Secrets are stored in (mode ) or . Override the relay with , in , or on expose commands."},{title:"Chat (optional)",body:"Login, logout, and status from chat work whenever the gateway runs (even if is false): , , . The bot reply does not echo your token; the inbound chat message still contains it (WhatsApp/Telegram history) \u2014 prefer on the host for highly sensitive tokens. When is in , allowlisted users can also run: Chat tunnels run inside the gateway process ( ) and share the same relay token as the CLI."},{title:"Security",body:"A tunnel URL is a capability: anyone who can open the URL can reach the forwarded service. Dev servers are often unauthenticated; treat tunneling like exposing a port on the public internet. Chat tunneling uses the same trust model as : allowlisted identities can open tunnels as the gateway OS user. in chat stores the bearer token on the gateway host but leaves a copy in the messaging transcript; use host CLI login if that risk matters for your threat model. See Security model."},{title:"Self-hosted relay",body:"For development or private deployments, run the relay in and point omnish at it with or . Operators: testing and operations (health checks, smoke, production VPS layout)."}],keywords:["tunneling","omnish","publishes","public","url","that","forwards","to","local","http","or","tcp","port","on","the","machine","running","tunnel","client","default","relay","is","cli","primary","chat","optional","security","self-hosted"],relatedCommands:["/tunnel help","/tunnels","/config help","/tunnel","/guides","/tunnel-setup-from-zero","/tunnel-auth","/tunnel login","/tunnel logout","/tunnel status","/telegram history","/tunnel http","/tunnel tcp","/tunnel stop"]},{id:"docs-features-watch",path:"docs/features/watch.md",title:"Watch \u2014 OS event eye",summary:"Lightweight OS event subscriptions that notify you on WhatsApp or Telegram when something changes on the machine running .",sections:[{title:"Quick start",body:"Enable watching: Or: or Run . Add rules: You should receive debounced messages like:"},{title:"Device-wide rules",body:"Watch rules are shared on the host, not private to one chat: One namespace per machine in (max 20 rules per device). Any allowlisted peer can , edit, pause, or remove any rule. Rule names must be unique on the device (two peers cannot each have a rule named ). (the default on new rules) alerts the peer who created the rule. Use , , or to reach more recipients. shows the creator peer key. If you had duplicate names from an older per-chat layout, omnish renames extras to on load."},{title:"Runtime model",body:"Watch is not a separate daemon or timed session. It runs inside the gateway process ( , foreground or ). Topic Behavior -------- ---------- How long Indefinite while the gateway runs, is true, and the rule is enabled and not paused. There is no session timeout. Debounce (default 2s, range 500ms\u201360s) coalesces bursts before chat notify. Rate cap per rule (default 30). Service polls Each rule runs / / about every 30 seconds. Background Same Node process as the gateway; keeps Watch alive like foreground. Not a separate OS service unless you installed the gateway as one."},{title:"Cowork, `/bg`, and Watch",body:"Feature What it does Relation to Watch --------- ---------------- ------------------- Watch OS events (FS, package logs, services) \u2192 chat alerts \u2014 Cowork Scheduled or on-demand shell commands while the gateway runs Parallel \u2014 same gateway, shared notify routing only. Does not trigger or consume watch rules. Background shell jobs from chat Unrelated \u2014 no integration with watch adapters. See Cowork for task schedules and heartbeats."},{title:"Persistence and restart",body:'Watch rules are not memory-only. They are saved on disk and survive gateway and service restarts. Data Path ------ ------ Rules (paths, excludes, paused, notify, \u2026) Recent events and state-change keys Global on/off and tuning ( , , debounce, rate cap) On gateway boot: If and (default), omnish reloads and starts adapters for rules that are enabled and not paused. If , rules remain on disk but adapters do not run until . If , rules persist but you must (or change a rule) to start adapters without restarting the whole gateway. Use to see file paths, saved rule counts, and adapter health. Troubleshooting persistence Symptom Check --------- -------- "Rules gone" after restart \u2014 they should still be there No alerts after restart in config; paused/disabled rules; Rules on disk but nothing running or ; if'},{title:"Rule lifecycle",body:"State What it means Command ------- ---------------- --------- Active Adapter running, alerts on or Paused Rule kept, adapter stopped, no alerts or Disabled Rule kept in list, Removed Deleted from Global off All adapters stopped After pause, stop, disable, or rm, pending debounced messages are cancelled so you should not get a late alert. pause / stop \u2014 same effect: stop watching, keep the rule for later. resume \u2014 start again if the rule is enabled. enable / disable \u2014 per-rule on/off (distinct from global and ). Check state: , , ."},{title:"Filesystem watches",body:"Add Root path \u2014 first path after the name ( , ). Events \u2014 optional comma list (default: ). Excludes (optional, both supported): Syntax Example -------- --------- or You can combine them: between exclude clauses is also accepted as a separator. Manage excludes later Excludes are applied twice for efficiency: native watcher ignore list + post-filter before notify."},{title:"Package and service watches",body:"Kind Command ------ --------- Packages \u2014 install/remove from OS logs Services \u2014 state changes on named units Use on noisy service checks. Discover services Finding the right unit name is easier with discovery commands (read-only; no rules are created): Command What you get --------- ---------------- Bulleted services with state, then a second message with copy-paste lines Template lines only (no list) Package log path for this OS + existing FS directories you can watch caps at 40 matches; narrow with a filter (e.g. ). Running units are listed first. Example second message after :"},{title:"Notifications",body:""},{title:"Efficiency and noise control",body:"Watch narrow directories when possible; use excludes for , caches, build output. Built-in ignores: , , , swap files, etc. Debounce \u2014 (default 2s) coalesces bursts. Rate cap \u2014 per rule (default 30). Sensitive paths ( , , keys) are blocked. Resource use: Adapter Cost --------- ------ fs Kernel-native watcher (inotify / FSEvents) \u2014 low for small trees; high if you watch all of without excludes. pkg Tails OS install logs (file watch or 2s poll fallback). svc One subprocess poll every 30s per rule \u2014 many service rules add steady CPU. Timers use so pending debounce/poll timers alone will not keep Node alive. Avoid watching all of without excludes."},{title:"Permissions",body:"Platform Packages Services ---------- ---------- ---------- Linux (often group) for named units macOS labels Windows Application log for named services"},{title:"Troubleshooting",body:"Problem What to do --------- ------------ No alerts true? Gateway running? Alerts after pause Should be fixed \u2014 if you paused mid-debounce, wait one debounce window; report if alerts continue unknown Use (name required) Path blocked Sensitive path denylist \u2014 pick another root Adapter error on status Fix log permissions or service names"},{title:"Data files",body:"Rules: Recent events:"},{title:"Config keys (chat-editable)",body:", , , \u2014 see ."},{title:"Related",body:"Cowork \u2014 scheduled tasks and heartbeat Webhook receiver \u2014 CI/CD to chat"}],keywords:["watch","os","event","eye","lightweight","subscriptions","that","notify","you","on","whatsapp","or","telegram","when","something","changes","the","machine","running","quick","start","device-wide","rules","runtime","model","cowork","/bg","and","persistence","restart","rule","lifecycle","filesystem","watches","package","service","notifications","efficiency","noise","control","permissions","troubleshooting","data","files","config","keys","chat-editable","related"],relatedCommands:["/watch help","/config","/watch on","/config set","/watch add","/deploy create","/projects","/tmp","/watch list","/home","/you","/deploy"]},{id:"docs-features-webhook-receiver",path:"docs/features/webhook-receiver.md",title:"Webhook receiver \u2014 CI/CD notifications via chat",summary:"The webhook receiver is a lightweight HTTP server built into the omnish gateway. It accepts requests with JSON payloads, formats them into concise messages, and delivers them to your WhatsApp or Telegram chat via the existing pipeline.",sections:[{title:"Prerequisites",body:"A running gateway: . set to in ."},{title:"Configuration",body:"Key Type Default Description ----- ------ --------- ------------- boolean Enable the webhook HTTP server number (random) Port to listen on. picks a random available port. string Bind address. Use to accept external connections (see security note). string Bearer token for authentication. If empty when the receiver starts, a random 32-byte hex token is generated and saved to . Example :"},{title:"Endpoint",body:"The token can also be passed as a query parameter: . Request body Any valid JSON object. The receiver formats the payload into a chat message using built-in formatters for known CI systems, or a generic format for everything else. Optional fields in the JSON body: Field Type Description ------- ------ ------------- string Target chat identity (e.g. or ). If omitted, the message goes to the first allowlisted peer. string Label for the source system (shown in the formatted message). Can also be set via query parameter or header. string Simple text message (used as-is when present). string Fallback message text. string Title line for generic payloads. string Status line for generic payloads. Response Status Body Meaning -------- ------ --------- Message delivered Invalid JSON or no target peer Missing or invalid token Not a POST request Body exceeds 256 KB failed"},{title:"Built-in CI formatters",body:"GitHub Actions When the payload contains and a object, the receiver formats: GitLab CI When the payload contains and , the receiver formats: Generic payloads For any other JSON, the receiver uses , , , and fields if present, or falls back to a truncated JSON preview."},{title:"Examples",body:"GitHub Actions workflow Add a step at the end of your workflow to notify on completion: GitLab CI Simple notification from a script"},{title:"Security",body:"The webhook server binds to 127.0.0.1 by default \u2014 only local processes can reach it. If you set to , the server is accessible from the network. Use a firewall or reverse proxy with TLS in production. The bearer token is compared using to prevent timing attacks. Maximum payload size is 256 KB."},{title:"Default peer resolution",body:"When the incoming payload does not include a , the receiver picks the first available peer from the gateway's allowlist: WhatsApp peers are checked first, then Telegram. If no peer is available, the request returns a error."},{title:"See also",body:"Configuration guide \u2014 webhook config keys Cowork heartbeat \u2014 combine with heartbeat tasks for dead-man's-switch monitoring Background jobs \u2014 per-job completion notifications"}],keywords:["webhook","receiver","ci/cd","notifications","via","chat","the","is","lightweight","http","server","built","into","omnish","gateway","it","accepts","requests","with","json","payloads","formats","them","concise","messages","and","delivers","to","your","whatsapp","or","telegram","existing","pipeline","prerequisites","configuration","endpoint","built-in","ci","formatters","examples","security","default","peer","resolution","see","also"],relatedCommands:["/help","/webhook authorization","/json","/repo","/github","/owner","/actions","/runs","/project","/checkout","/your-server","/webhook","/localhost"]},{id:"docs-guides-background-and-boot",path:"docs/guides/background-and-boot.md",title:"Background gateway and start on boot",summary:"Portable CLI (all platforms): ( ) starts the gateway detached; logs default to ; pass (alias: ) to append elsewhere; reads and stops the process. Use these for ad-hoc background runs without installing a system integration.",sections:[{title:"Linux (systemd --user)",body:"Copy contrib/omnish.service to . Set to your Node path and absolute path to , and if the data directory is not . Run: For a user service to run without an active login session, enable lingering:"},{title:"macOS (launchd)",body:"The LaunchAgent label is (reverse-DNS for omnish.dev). Older templates used ; if you already installed that, boot it out before installing the new plist (see below). Edit contrib/dev.omnish.gateway.plist: set Node path, path, and (and log paths if you use them). Copy to . Load (replace with output): To unload: Migrating from : remove the old job first, then install the new file: Alternative: add a small script that runs to System Settings \u2192 General \u2192 Login Items (simpler, no auto-restart on crash)."},{title:"Windows (Task Scheduler)",body:"Open Task Scheduler \u2192 Create Task\u2026 (not a basic task). General: run only when user is logged on (typical for WhatsApp session). Triggers: At log on for your user. Actions: Start a program Program: path to (e.g. from in ). Add arguments: (adjust the path; quotes if it contains spaces). Start in (optional): install folder. Set OMNISHHOME under user environment variables if you do not use the default data directory, or use a wrapper that then runs . Optional: import contrib/omnish-windows-task.xml after editing paths (import may need tweaks per account; the GUI is more reliable if XML import fails). Advanced: NSSM or WinSW can install Node as a Windows Service for machine-wide or headless scenarios \u2014 not maintained by this repo."},{title:"Stopping the gateway",body:"(any OS): uses the pidfile from a session. On Windows, if signaling the process fails, omnish may fall back to (best-effort shutdown). systemd / launchd / Task Scheduler: use the manager\u2019s stop / disable as usual; do not rely on from a different start method."},{title:"Environment",body:": data directory (same as elsewhere in omnish). For services, set it in the unit / plist / task environment, not only in the shell profile."}],keywords:["background","gateway","and","start","on","boot","portable","cli","all","platforms","starts","the","detached","logs","default","to","pass","alias","append","elsewhere","reads","stops","process","use","these","for","ad-hoc","runs","without","installing","system","integration","linux","systemd","--user","macos","launchd","windows","task","scheduler","stopping","environment"],relatedCommands:["/service help","/service instructions","/logs","/gateway","/service","/service logs","/service install","/features","/service-from-chat","/dist","/index","/omnish","/contrib"]},{id:"docs-guides-configuration",path:"docs/guides/configuration.md",title:"Configuration Guide - omnish",summary:"Complete configuration reference and customization guide.",sections:[{title:"Configuration Overview",body:"omnish uses a JSON configuration file with sensible defaults. You can customize every aspect of the system's behavior. Configuration File Location Default: Legacy: (if upgrading) Override: Set Check your configuration location: ```bash omnish status"}],keywords:["configuration","guide","omnish","complete","reference","and","customization","overview"],relatedCommands:["/config help","/config keys","/config","/path","/to","/dir","/bin","/bash","/features","/chat-llm-fallback","/tunnel","/login","/v1","/me"]},{id:"docs-guides-docker-gateway-golden-path",path:"docs/guides/docker-gateway-golden-path.md",title:"Docker gateway \u2014 golden path (reference)",summary:"Run omnish inside a container so a deployed app gains chat-driven shell access on that box. Reference files: .",sections:[{title:"Two ways to use omnish in Docker",body:"Path When Setup in container ------ ------ ---------------------- Attached (recommended) Platform account; messengers linked on dashboard , , , \u2014 no QR in container Standalone No hosted layer; expert / air-gapped Persistent volume + + inside container (or on host) The compose file in implements standalone by default. Use attached env vars for the platform path (see Platform attached mode, Platform reference)."},{title:"Attached mode (implemented)",body:"Add chat-driven ops to an existing app container without Baileys inside the image: Link WhatsApp/Telegram on the platform dashboard (once per account). Set allowlist on the dashboard. Deploy with the env above; outbound HTTPS to the platform only. Legacy env aliases: , . Messengers do not run inside the container; the platform routes chat to this CLI."},{title:"Standalone reference (implemented today)",body:"What you get Pinned npm version ( ). Non-root user (uid 1000), as PID 1. HEALTHCHECK ( ) \u2014 process up only; not messenger connectivity. Build and run First-time pairing (empty volume): Then as above."},{title:"Networking",body:"Concern Guidance --------- ---------- Attached Outbound HTTPS to communication layer; no Baileys in container Standalone Outbound to WhatsApp/Telegram APIs from container Tunnels Tunnel client runs in gateway namespace; see Tunnel setup from zero"},{title:"Environment",body:"Standalone today: \u2014 volume mount (compose: ) , \u2014 optional overrides Attached: , (aliases: , ) Optional /"},{title:"Security",body:"Allowlisted inbox \u2192 real shell as the container user. Protect volumes and tokens. Do not expose without TLS and auth \u2014 Webhook receiver."},{title:"See also",body:"Communication layer model Background gateway and start on boot"}],keywords:["docker","gateway","golden","path","reference","run","omnish","inside","container","so","deployed","app","gains","chat-driven","shell","access","on","that","box","files","two","ways","to","use","in","attached","mode","implemented","standalone","today","networking","environment","security","see","also"],relatedCommands:["/gateway-docker","/contrib","/architecture","/communication-layer-model","/tunnel","/telegram on","/docker-compose","/telegram apis","/tunnel-setup-from-zero","/home","/node","/features"]},{id:"docs-guides-interactive-cli",path:"docs/guides/interactive-cli.md",title:"Interactive terminal (`omnish i`)",summary:"Run the same commands you use in chat, but from your local terminal.",sections:[{title:"Commands",body:"Run from any directory. Your CLI session\u2019s working directory starts at (change it with or using your configured command prefix). Flags Flag Meaning ------ --------- Sender key for chat-driven cluster commands ( or ). If omitted, a synthetic sender tied to the CLI session is used ( ). / Run a single line and exit (non-interactive). Useful for scripts."},{title:"Trust model",body:"is not gated by the inbox allowlist. Anyone who can run commands on your machine can use it\u2014it has the same trust as your login shell. Chat interfaces remain allowlisted as before."},{title:"`/sendto`: push files or plain text to WhatsApp or Telegram",body:"When is active on this machine, you can use from to choose exactly who gets files, or to send a plain chat message (not an attachment). Syntax Files: selectors are checked from the current folder. Add an optional caption after . Plain text: same destinations; message is everything after the flag (or the value). To send a file whose name looks like a flag, use an explicit path (e.g. ). Destination forms: \u2014 all WhatsApp recipients from \u2014 all Telegram recipients from \u2014 both channels (all allowlisted WA + TG recipients) \u2014 one explicit WhatsApp recipient \u2014 explicit WhatsApp recipient list \u2014 one explicit Telegram recipient (compatibility form) Selector forms: \u2014 explicit list \u2014 cwd glob \u2014 recursive cwd glob Compatibility aliases also accepted: , , , , . Examples: Common patterns ```text"}],keywords:["interactive","terminal","omnish","run","the","same","commands","you","use","in","chat","but","from","your","local","trust","model","/sendto","push","files","or","plain","text","to","whatsapp","telegram"],relatedCommands:["/help","/sendto","/sendto wa","/photo","/sendto tg","/report","/promo","/downloads","/telegram outbound","/send","/files-send-receive","/bg","/reload"]},{id:"docs-guides-platform-attached-mode",path:"docs/guides/platform-attached-mode.md",title:"Platform attached mode",summary:"Run on a laptop, server, or container while WhatsApp and Telegram stay on the hosted omnish platform (tunnel relay). You link messengers once on the dashboard; each machine attaches with an account token and executes shell commands locally.",sections:[{title:"Standalone vs attached",body:"Standalone (default) Attached (platform) -- -------------------------- ------------------------- When No platform URL + token + (or env) set Where messengers connect Same host as Hosted relay (dashboard) Link WhatsApp on the device Dashboard QR or Link Telegram on the device Dashboard bot token Allowlist \u2192 local Dashboard allowFrom / telegramAllowFrom (authoritative) Shell execution Local Local (unchanged) If platform credentials are set, does not start local Baileys/Telegram clients \u2014 it opens a WebSocket to the platform and runs commands on this host only."},{title:"Prerequisites",body:"A platform account (signup/login on your relay \u2014 see tunnel relay README). Messengers linked on the platform (not via on the device, unless you use import \u2014 see below). Your phone or Telegram id on the platform allowlist (dashboard). Outbound HTTPS from the device to the platform URL (and correct reverse-proxy routing if you self-host)."},{title:"Step 1 \u2014 Account and token",body:"On the hosted relay (e.g. ): Sign up or log in via / , or use . Copy the account token from the response or dashboard. The same token works for: Attached gateway: + Persisted config:"},{title:"Step 2 \u2014 Link messengers on the platform",body:"Do this on the platform, not on the machine that will run shell commands (unless noted). WhatsApp Option A \u2014 Dashboard (recommended, 1 minute) Open the platform dashboard ( on your relay origin). Click Link WhatsApp \u2014 a QR appears automatically. Scan with WhatsApp \u2192 Linked devices. The phone may show \u201CLogging in\u201D for a few seconds while the platform reconnects after scan (515 restart \u2014 dashboard shows Finishing link\u2026; no second QR). When connected, use Add my number to allowlist (one click) if shown. Run on your machine. Option A2 \u2014 CLI QR in terminal Scan the ASCII QR, then: To disconnect: dashboard Unlink WhatsApp or . Option B \u2014 Import from a machine that already linked locally On a machine where you ran successfully, stop . Set platform URL + token, then: ```bash omnish platform import-whatsapp"}],keywords:["platform","attached","mode","run","on","laptop","server","or","container","while","whatsapp","and","telegram","stay","the","hosted","omnish","tunnel","relay","you","link","messengers","once","dashboard","each","machine","attaches","with","an","account","token","executes","shell","commands","locally","standalone","vs","prerequisites","step"],relatedCommands:["/help","/architecture","/communication-layer-model","/gateway-config-precedence","/contrib","/tunnel-relay","/readme","/telegram clients","/login on","/tunnel","/auth","/signup","/login"]},{id:"docs-guides-platform-reference",path:"docs/guides/platform-reference.md",title:"Platform reference (complete)",summary:"Consolidated documentation for the omnish hosted platform (tunnel relay + dashboard + attached ). For a guided walkthrough, start with Platform attached mode. For relay deployment, see contrib/tunnel-relay/README.md.",sections:[{title:"One-minute checklist",body:"Step Action ------ -------- 1 Sign up / log in at \u2014 copy account token 2 Link WhatsApp: dashboard Link WhatsApp \u2192 scan QR \u2192 Add my number to allowlist (if offered) 3 Link Telegram: paste bot token \u2192 Link / restart Telegram \u2192 DM bot \u2192 add id under Allowlists \u2192 Save allowlists 4 On your machine: (or ) 5 then 6 From allowlisted chat: or ---"},{title:"How it works",body:"Messengers terminate on the relay (Baileys + grammY). Shell runs only on machines where is attached. Policy (allowlists, ) is stored on the platform and merged into the attached CLI via (refreshed every 5 minutes and on connect). ---"},{title:"What is persisted",body:"Requires on the relay (account data). Without MongoDB, only static relay tokens work \u2014 no dashboard accounts or persistence. Data Storage Survives relay restart ------ --------- ------------------------- Account token, email MongoDB Yes , MongoDB Yes MongoDB Yes Telegram bot token MongoDB ( ) Yes (not returned by API) WhatsApp Baileys auth files Disk Yes Connector status, linked WA phone MongoDB Yes Peer \u2192 device bindings MongoDB Yes Device slots MongoDB Yes Community catalog entries MongoDB Yes On MongoDB connect, linked connectors are restored automatically ( ). Volumes (self-hosted): \u2014 WhatsApp sessions MongoDB \u2014 accounts, allowlists, connector sessions ---"},{title:"Dashboard (`/dashboard/`)",body:"After login, the dashboard loads and hydrates all forms. Account status Shows , WhatsApp/Telegram connector state, allowlist counts, default device, online device count. Devices Create device slots, set default device, see online/offline status. The first attached may auto-create a device. Routing Peer bindings map a (e.g. , ) to a specific . Routing order when a message arrives: Peer binding (if set) Single online device (if exactly one) Default device (if online) Any online device Allowlists Field Format Notes ------- -------- ------- WhatsApp E.164, comma-separated e.g. Telegram Numeric user ids Use bot in DM to discover your id Save allowlists \u2192 . Changes apply immediately (connectors reload allowlists per message). Warning: empty allowlist = any sender can run commands. Telegram connector Paste bot token from @BotFather. Link / restart Telegram \u2014 token required on first link; if already linked, empty token field reuses saved token. Users DM the bot before allowlisting to learn their numeric id. WhatsApp connector State UI ------- ----- Not linked Link WhatsApp \u2014 shows QR, auto-polls until connected; Refresh / Restart pairing while QR is active After scan, WhatsApp closes with 515 \u2014 QR hidden, \u201CFinishing link\u2026\u201D (10s); normal Runtime reconnect after a drop \u2014 QR hidden Linked Green \u201Cconnected\u201D, optional Add my number to allowlist, Unlink WhatsApp Logged out / error Reconnect (unlink + new QR) Advanced: import from local via (see CLI below). Attached CLI snippet Shows , , optional , and . ---"},{title:"CLI reference (`omnish platform \u2026`)",body:"All commands require + (config or env). Run for the latest help text. Setup and diagnostics Command Purpose --------- --------- Save credentials to Same (config CLI aliases) Account, connectors, allowlist counts, online devices URL, token source, effective platform block Test WebSocket paths before Attach device (attached mode when platform creds resolve) Allowlists (platform policy) Command Purpose --------- --------- Show WhatsApp and Telegram allowlists Merge entries into allowlists Replace one or both lists Examples: Wildcard is rejected on the platform (same security model as standalone). WhatsApp on platform Command Purpose --------- --------- Start pairing, print ASCII QR, poll until linked (includes after scan) Remove session and auth files on platform Upload local Baileys auth from on this host Stop local before import to avoid WhatsApp session conflicts. Environment variables Canonical Also accepted Purpose ----------- --------------- --------- , Relay base URL , Account bearer token in config Pin device slot on attach Precedence: environment \u2192 \u2192 (if used). Config keys: ( ), ( ), ( ). ---"},{title:"HTTP API reference",body:"Base URL: relay origin (e.g. ). Auth: for most routes. Catalog browse/download routes are public (no bearer required); publish requires a bearer token. Chat commands for the catalog: Online catalog. Auth (no bearer on signup/login body) Method Path Body Response -------- ------ ------ ---------- POST (201) POST Account Method Path Body Response -------- ------ ------ ---------- GET \u2014 , , , , , , , , , \u2026 PUT PUT GET \u2014 shape (example): means a bot token is stored; the token is never returned. Devices and routing Method Path Body Response -------- ------ ------ ---------- GET \u2014 POST (201) PUT DELETE Telegram connector Method Path Body Response -------- ------ ------ ---------- PUT required on first link; omit on later calls to reuse stored token. optional; can also use . WhatsApp connector Method Path Body Response -------- ------ ------ ---------- POST (optional legacy ) GET \u2014 POST GET \u2014 Legacy; prefer POST Status values: , , , , , , , . After a successful QR scan, Baileys typically closes with (515) and reconnects using saved creds (no second QR). The connector reports (no ) until the new socket opens as . Catalog (community templates) Public read (no bearer). Publish requires account bearer token. Method Path Auth Query / body Response -------- ------ ------ -------------- ---------- GET optional GET optional GET optional \u2014 Full entry including POST required POST optional \u2014 Full entry; increments values: , , , . Upsert on publish is per . Max payload 32 KiB. Recipe payloads must include quoted (or custom ) in the command. WebSocket (attached CLI) Path Purpose ------ --------- Primary attach path Fallback when not proxied to control port Register frame: Inbound: Outbound: Reverse proxy (required paths \u2192 control port 8788) , , , , If these hit the HTTP edge (8787) instead, attach fails with WebSocket 400. Run . ---"},{title:"Telegram `/id` command",body:"Works on platform-linked and standalone bots, before the user is on the allowlist. Mode Reply hints ------ ------------- Platform Numeric id + + dashboard / Standalone Numeric id + See Telegram integration notes. ---"},{title:"Policy in attached mode",body:"When attaches successfully: loads , , . These override local for inbound messenger policy. Host-only settings (shell, jobs, tunnels, webhooks) still come from local config. Policy refreshes every 5 minutes while attached. If fails at startup, the CLI may fall back to local allowlists until the next successful sync. You do not need on the device for platform-routed chat when platform policy loads successfully. ---"},{title:"Troubleshooting index",body:"Symptom See --------- ----- WebSocket 400 on attach Platform attached mode \u2014 Troubleshooting, relay README Device online, no command output Platform allowlist; relay logs; Troubleshooting \u2014 Attached Dashboard fields empty after login Requires MongoDB; hard-refresh; re-save allowlists Telegram token \u201Cgone\u201D after reload By design \u2014 token not shown; use empty field + Link to reuse Phone stuck on \u201CLogging in\u201D after scan Wait 15s \u2014 platform should show then connected; see Troubleshooting \u2014 Platform WhatsApp WhatsApp stuck / logged out Dashboard Reconnect or then link-whatsapp with correct allowlist WhatsApp LID resolution \u2014 update relay + CLI Empty allowlist surprises Empty = allow all senders ---"},{title:"See also",body:"Platform attached mode \u2014 guided setup Quick start \u2014 Path C Configuration \u2014 Platform / attached mode Docker gateway golden path Tunnel setup from zero \u2014 same account token for tunnels Communication layer \u2014 API sketch (future notes; implemented API is in this doc)"}],keywords:["platform","reference","complete","consolidated","documentation","for","the","omnish","hosted","tunnel","relay","dashboard","attached","guided","walkthrough","start","with","mode","deployment","see","contrib/tunnel-relay/readme","md","one-minute","checklist","how","it","works","what","is","persisted","/dashboard/","cli","http","api","telegram","/id","command","policy","in","troubleshooting","index","also"],relatedCommands:["/tunnel-relay","/readme","/contrib","/architecture","/communication-layer-model","/gateway-config-precedence","/telegram-integration-notes","/dashboard","/id","/v1","/me","/whatsapp"]},{id:"docs-guides-quick-start",path:"docs/guides/quick-start.md",title:"Quick Start Guide - omnish",summary:"",sections:[{title:"What you\u2019ll get",body:"By the end of this page you will have: A working gateway \u2014 listening for your DMs. A first win \u2014 a successful sync command (e.g. or ) with output back in chat. A path to deep control \u2014 optional: for a PTY smoke test (see the main README)."},{title:"What is omnish?",body:"A secure CLI tool that bridges messaging platforms (WhatsApp, Telegram) to your system shell. It allows allowlisted users to: Execute shell commands directly from chat Run background jobs with streaming output Start interactive terminal sessions Transfer files between your system and chats Key differentiator: No AI layer - direct, deterministic shell access with explicit security controls."},{title:"One phone with WhatsApp",body:"You do not need a second phone. Host \u2014 omnish runs on a laptop, desktop, home server, or VPS (Linux, macOS, or Windows). It is a CLI gateway, not a phone app. Phone \u2014 you use the same WhatsApp account: scan the QR once under Linked devices, then send command DMs from that app (for example Message yourself). Linking works like WhatsApp Web: your account is the phone plus linked devices. Session files live under (see Comprehensive documentation). The phone stays the primary device; omnish on the host is an extra linked device. Keep the phone online as WhatsApp expects for multi-device; Meta\u2019s caps on linked devices still apply. Follow Path A: WhatsApp below for install, , , and . Private chats only \u2014 groups are ignored. No computer handy? You still need a machine that stays on to run ; a small VPS is typical. If you prefer not to use WhatsApp linked devices, you can use Telegram instead ( ); see Telegram integration notes. Security Treat allowlisted numbers (and Telegram ids) like remote shell passwords \u2014 only add identities you fully trust. See the main README and Security model."},{title:"Installation",body:"From npm (recommended) From source ```bash git clone https://github.com/eligapris/omnish.git cd omnish pnpm install"}],keywords:["quick","start","guide","omnish","what","you","ll","get","is","one","phone","with","whatsapp","installation"],relatedCommands:["/help","/wa help","/tg help","/omnish","/media","/logo-horizontal","/apps start","/readme","/auth","/comprehensive-documentation","/telegram-integration-notes","/architecture","/security","/github","/eligapris"]},{id:"docs-guides-system-agents-and-run",path:"docs/guides/system-agents-and-run.md",title:"System agents, multi-agent, and `/run`",summary:"omnish is built for agents on your machine \u2014 not another hosted model or subscription. You run on hardware you control; from chat you drive system agents: CLIs and TUIs such as , , your own scripts, or multi-step orchestrators. Omnish forwards deterministic shell and PTY to those tools; it does not replace them.",sections:[{title:"Default macro command (`recipesMacroDefaultCommand`)",body:"In , is the shell command used when a macro-style recipe stores a long body as a . The default targets the Claude CLI agent: Point it at any agent or orchestrator that reads the task from the environment, for example a wrapper that fans out to multiple local agents: Restart the gateway after editing (or use when supported)."},{title:"Per-chat recipes with `/run add`",body:"The stored command must reference (or your custom ). Example: Cursor / other CLI agents Example: Claude (default-class agent CLI) Patterns are the same for any binary on the gateway host: omnish only spawns the process you configure."},{title:"Optional: local model CLIs",body:"If your agent stack sometimes calls a local inference CLI (e.g. Ollama), you can still register it in a recipe \u2014 same pattern. That is optional plumbing, not the core story: Prefer scripts on disk for awkward HTTP/JSON so you are not pasting payloads into chat."},{title:"Limits and safety",body:"\u2014 see configuration. \u2014 gated built-ins; leave off unless you understand the risk. Secrets: use env vars on the gateway host or your secret store; do not paste keys into chat."},{title:"Discoverability",body:", Cowork: \u2014 scheduled and on-demand tasks, notifications, logs (name unchanged)."},{title:"See also",body:"User guide \u2014 shortcuts vs Configuration Interactive sessions Background jobs vs Cowork"}],keywords:["system","agents","multi-agent","and","/run","omnish","is","built","for","on","your","machine","not","another","hosted","model","or","subscription","you","run","hardware","control","from","chat","drive","clis","tuis","such","as","own","scripts","multi-step","orchestrators","forwards","deterministic","shell","pty","to","those","tools","it","does","replace","them","default","macro","command","recipesmacrodefaultcommand","per-chat","recipes","with","add","optional","local","limits","safety","discoverability","see","also"],relatedCommands:["/run help","/apps help","/run","/apps start","/apps attach","/jobs","/cowork","/cw","/features","/sessions","/apps","/run queue","/run-queue"]},{id:"docs-guides-tunnel-setup-from-zero",path:"docs/guides/tunnel-setup-from-zero.md",title:"Tunnel setup from zero (gateway host)",summary:"This guide is for someone who wants public URLs for apps running on the machine where executes, using WhatsApp or Telegram (optional commands) or the CLI.",sections:[{title:"What runs where",body:"Piece Where Role ------- -------- ------ Relay VPS (e.g. ) or your own Docker host Accepts HTTPS/WSS, forwards to connected clients omnish gateway Your PC / server \u2014 runs shell, chat commands, and tunnel client Your app Same host as the gateway (usually) , etc. The default public relay is . You can self-host from and point clients at your origin."},{title:"Checklist",body:"Install omnish on the gateway host Native modules may require a normal install (not ). See the project README. Link WhatsApp and/or Telegram, allow yourself ```bash omnish link omnish allow +YOURE164"}],keywords:["tunnel","setup","from","zero","gateway","host","this","guide","is","for","someone","who","wants","public","urls","apps","running","on","the","machine","where","executes","using","whatsapp","or","telegram","optional","commands","cli","what","runs","checklist"],relatedCommands:["/tunnel help","/tunnel login","/tunnel","/features","/tunneling","/contrib","/tunnel-relay","/docs","/testing-and-operations","/docker-gateway-golden-path","/wss","/or telegram","/auth","/signup"]},{id:"docs-guides-ui",path:"docs/guides/ui.md",title:"Browser setup UI (`omnish ui`)",summary:"Local-first configuration panel that edits the same as the CLI and chat commands. Use it when you want to finish basics from a phone on your LAN before touching WhatsApp/Telegram.",sections:[{title:"Quick start",body:"Default bind is (reachable on your LAN). The CLI prints: A setup token (saved under your data dir as ; legacy installs may have had , which is migrated automatically) URLs on localhost and discovered IPv4 LAN addresses A quick link that includes so your phone can authenticate in one tap Then open the UI in a browser, unlock with the token, edit core settings, and Save."},{title:"Run the gateway from the UI",body:"After configuration (and WhatsApp pairing if you use it), you can start the chat gateway without going back to the terminal: Under Host snapshot, use Start gateway. This is equivalent to : a detached process runs with the same data directory, and stdout/stderr append to the default log file ( \u2014 the panel shows the resolved path). Stop gateway sends SIGTERM to the background gateway tracked by , same idea as (including stale pidfile cleanup when the process is already gone). Starting the gateway does not stop the setup UI. You can Stop setup server and leave the gateway running in the background. Authenticated session cookies are required (same as the rest of the panel): and . Port reclaim and stopping the UI The host writes in your data dir with the last process PID and bind port. When you start again on the same (default 3789), the new process terminates the previous one if it is still running, so the port is usually free without manual . A different does not kill another UI instance on a different port (the state file is overwritten when the new server starts). In the browser, Stop setup server (Core settings) ends the HTTP process after your session authenticates \u2014 the page will disconnect; run on the machine again to continue. Same trust as the rest of the panel: only someone with the setup token can unlock a session that can call ."},{title:"WhatsApp pairing (QR in the browser)",body:"You can link this host to WhatsApp without a terminal QR: Stop the gateway on this machine if it is running: use Stop gateway in the UI, , or stop your service. The UI checks for a live process and refuses browser pairing while it is present \u2014 two Baileys clients must not use the same directory at once. Unlock the UI with your setup token. Under Host snapshot, open WhatsApp pairing (QR) and choose Start QR pairing. On your phone: WhatsApp \u2192 Settings \u2192 Linked devices \u2192 Link a device, then scan the QR shown in the browser. If this host is already linked and you need a fresh login, enable Replace session (clears saved WhatsApp auth on disk, same idea as ) and confirm. You can still use from a shell on the host if you prefer the terminal QR. Pairing events are delivered over an authenticated same-origin event stream ( ) after you call . Traffic remains plain HTTP on the LAN \u2014 same trust model as the rest of the UI."},{title:"Security model (read this)",body:"Same trust as editing config on disk. Anyone who can change settings here could affect gateway behavior after reload/restart. LAN exposure: binding to all interfaces means anyone on your Wi\u2011Fi/Ethernet who can reach the port must not guess the token. Treat the token like a password. Not HTTPS: traffic is plain HTTP on your network segment v1. Run behind a reverse proxy with TLS only if you extend exposure beyond the LAN. Reduce exposure \u2014 loopback only (no phone from LAN). Firewall \u2014 block TCP 3789 (or your ) from the WAN side of your router. Guest Wi\u2011Fi \u2014 avoid running on networks shared with untrusted devices."},{title:"Flags",body:"Flag Meaning ------ --------- / Bind address (default ). / TCP port (default ). / Set or rotate the setup token (stored in )."},{title:"Environment",body:"Variable Meaning ---------- --------- Absolute path to a directory containing (advanced override for custom builds). Legacy alias for ."},{title:"Build note for contributors",body:"The UI is built into during / . If static files are missing, run the repo build from the project root so Vite runs before the esbuild CLI bundle."}],keywords:["browser","setup","ui","omnish","local-first","configuration","panel","that","edits","the","same","as","cli","and","chat","commands","use","it","when","you","want","to","finish","basics","from","phone","on","your","lan","before","touching","whatsapp/telegram","quick","start","run","gateway","whatsapp","pairing","qr","in","security","model","read","this","flags","environment","build","note","for","contributors"],relatedCommands:["/help","/configuration","/config set","/telegram","/stderr append","/logs","/gateway","/api","/start","/stop","/shutdown","/wa","/link"]},{id:"docs-guides-user-guide",path:"docs/guides/user-guide.md",title:"User Guide - omnish",summary:"Complete manual for using omnish's features.",sections:[{title:"Table of Contents",body:"Introduction Basic Shell Commands Background Jobs Interactive Sessions Terminal interactive ( ) File Transfer System Commands User Shortcuts User shortcuts vs recipes Cowork (scheduled tasks) Working with Multiple Platforms Configuration Management Chat LLM fallback (optional) Best Practices"},{title:"Introduction",body:"omnish bridges your messaging chats to your system shell with these main execution surfaces: Sync Shell: Immediate command execution (prefix: ) Background Jobs: Asynchronous commands with streaming output ( ) Cowork: Saved commands on a schedule or on demand ( ) while the gateway runs \u2014 see Cowork (scheduled tasks) Interactive Sessions: Terminal sessions via PTY ( ) Terminal REPL ( ): The same slash/ interface in your local shell\u2014see Interactive terminal (not gated by the inbox allowlist; same trust as your login session). Each chat maintains its own working directory and state, making it perfect for team workflows and personal use. The CLI REPL uses a dedicated session key and starts in your current directory when you launch ."},{title:"Basic Shell Commands",body:"Synchronous Execution Commands prefixed with execute immediately in the shell: Working Directories Each chat maintains its own working directory: Use to change directories Changes persist across commands View current directory with Command Prefix The prefix can be customized in : Without prefix, use free shell mode (see below)."},{title:"Background Jobs",body:"Start long-running commands without blocking your chat. Full detail: Background jobs. Starting Jobs Job Management Job IDs are 8-character hex strings; you can also assign a name with (or ). / / accept either form. Use (or ) to get a chat message when the job exits. Job Output and limits Logs persist under with matching . Jobs use the chat session cwd; they inherit the gateway process environment (not a prior from another shell). applies to sync commands only, not to children."},{title:"Interactive Sessions",body:'Start full terminal sessions in your chat: Starting Sessions Session Management Session Features Maximum 5 sessions per chat (configurable) One session "attached" at a time Plain text goes to attached session Output debounced and chunked ANSI codes can be preserved Session Commands Session Lifecycle'},{title:"Terminal interactive (`omnish i`)",body:"From a terminal on the same machine, run (or ) to type the same commands you would send in WhatsApp or Telegram: , , , , , etc. Trust: The inbox allowlist does not apply\u2014only local OS users who can run processes as you can start . Outbound files: Use or while is active; see Interactive terminal and Files send/receive. Media: , , \u2014 download, transcribe, and edit media (yt-dlp + ffmpeg + optional Whisper) \u2014 see media commands. Jobs: in this REPL belongs only to this process, not to the gateway\u2019s job list. Full reference: Interactive terminal."},{title:"File Transfer",body:'Sending Files Receiving Files When someone sends media files: Files are automatically downloaded Saved to organized directory structure Reply with "Saved: /path/to/file" File Location Control'},{title:"System Commands",body:"Gateway Control Notes: Aliases: and work like . is accepted as an alias for . prints command help. Service management from chat and are gated by (same trust as shell). Allowlist Management Cluster (optional, chat-driven) When is true on multiple machines linked to the same WhatsApp number, only the active host replies. Coordination flows through the chat itself \u2014 no shared file, no Syncthing. Shorthand: \u2026 and the long form \u2026 (only the exact token matches; does not). See Cluster and chat configuration. Config from chat View or change many keys without SSH (same trust as shell): Allowlists stay / , not . Full list: Cluster and chat configuration. Other Commands"},{title:"User Shortcuts",body:"Create chat-specific command aliases: Creating Shortcuts Using Shortcuts Managing Shortcuts Scope model: / : private to this chat. / : shared on this gateway across chats. Resolution order for or : chat shortcut first, then shared."},{title:"User shortcuts vs `/run` recipes",body:"Both features are per-chat (stored on the gateway host), but they solve different problems: Shortcuts ( , , ) recipes ( , ) --- ----------------------------------------------- ----------------------------------- Purpose One-line alias: expand to a saved message once (e.g. , ). Parameterized runner: inject your task text into a CLI via (and optional ), then start a detached app session (PTY) by default \u2014 use or when you want plain DMs to go to the agent. still attaches on start. Typical use Jump to a directory, repeat a favorite sync command, short macros. Drive system agents (e.g. , , your orchestrators) from chat with a task each time. Invocation Bare token only: or (no extra words on the line). \u2014 task is required for named recipes. Shortcuts do not add a task environment variable. Recipes require the stored command to reference (or a custom ). For system agents, multi-agent PTY, and , see System agents and . Queued recipe runs (same chat, one PTY at a time): or , then for status. You can also batch-enqueue from JSON with (file path, inline JSON, or an uploaded file with that caption); see Run queue for the exact JSON shape and limits. Multi-step runbooks: create a recipe with sequential steps using : Steps are separated by or newlines. Each step runs in order; if any step fails (non-zero exit), remaining steps are skipped. Prefix a step with to continue on failure: When you invoke a runbook ( ), it runs in the background and sends a formatted per-step summary when complete. Use to see the step listing. Useful management forms:"},{title:"Cowork (scheduled tasks)",body:"Cowork saves shell commands and runs them on a schedule or on demand while is active. Commands use the same shell, timeout, and byte limits as sync commands. Alias: . Default log directory: (override with ). Missed scheduled times catch up when the gateway returns (one oldest slot per tick). Disabled tasks reject until re-enabled. Conditional notifications: only sends a notification when the task fails. tracks ok/fail transitions and only alerts on flips (useful for frequent health checks). Full reference: Cowork feature doc (notifications, data files, cluster and WhatsApp caveats)."},{title:"Working with Multiple Platforms",body:"Multi-Platform Setup Configure for both platforms: Platform Differences Feature WhatsApp Telegram --------- ---------- ---------- Message Limit 3500 chars 4096 chars Format Plain text HTML support File Types All media types Photo, document, video, audio Authentication QR code Bot token Best Practices Use different users for different platforms Keep allowlists minimal Monitor usage across platforms Use platform-specific features appropriately"},{title:"Chat LLM fallback (optional)",body:"If is true in on the gateway host, plain messages that would otherwise show \u201CNo command matched\u201D are answered asynchronously by a subprocess you configure ( ). Replies go back to the same WhatsApp/Telegram chat (or to the terminal in ). Where to configure: the same file as the rest of omnish \u2014 default , or if set. You edit JSON on the machine where runs; API keys must be available in that process\u2019s environment if your wrapper needs them (restart the gateway after changing systemd/shell env). Full step-by-step instructions, stdin vs PTY, and environment forwarding: Chat LLM fallback."},{title:"Configuration Management",body:"Configuration File Location Default: Override with environment variable Legacy: Uses if doesn't exist Editing from chat Most tunables support (whitelist). See Cluster and chat configuration and Configuration guide. Key Configuration Options Environment Variables : Override data directory : Enable debug logging (legacy: ) : Override bot token"},{title:"Best Practices",body:`Security Minimal Allowlists: Only add trusted users No Wildcards: Explicit phone numbers only Monitor Usage: Regularly check allowlists Session Isolation: Each chat is isolated Performance Output Chunking: Large outputs automatically split Session Limits: Don't exceed max sessions Timeout Settings: Adjust for your use case Log Rotation: Monitor log file sizes Usage Patterns Use Sessions for Interactive Apps: Vim, Python REPL, etc. Use Jobs for Long Operations: Builds, data processing Use Shortcuts for Common Tasks: Reduce typing Organize with Working Directories: Keep related work together Troubleshooting "Not in allowlist": Check E.164 format (+15551234567) "Maximum sessions reached": Stop unused sessions Messages cut off: Check Connection issues: Use for debug logs`},{title:"Next Steps",body:"Configuration: Customize for your workflow Security: Learn about the security model Features: Explore advanced features Integration: Set up for your team"}],keywords:["user","guide","omnish","complete","manual","for","using","features","table","of","contents","introduction","basic","shell","commands","background","jobs","interactive","sessions","terminal","file","transfer","system","shortcuts","vs","/run","recipes","cowork","scheduled","tasks","working","with","multiple","platforms","chat","llm","fallback","optional","configuration","management","best","practices","next","steps"],relatedCommands:["/help","/docs help","/run","/bg","/cowork","/apps","/tmp","/path","/var","/log","/system","/features","/background-jobs","/bg npm"]}]};var mn=Mh;function gi(e,t){return`${e.replace(/\/$/,"")}/${t}`}function Dv(e){return e.toLowerCase().replace(/[^a-z0-9\s/-]/g," ").split(/\s+/).filter(t=>t.length>1)}function Uv(e,t,n){if(t.length===0)return null;let o=e.title.toLowerCase(),r=e.summary.toLowerCase(),s=new Set(e.keywords),i=0,a,l=n.toLowerCase().trim();l.length>=3&&o.includes(l)&&(i+=40);for(let c of t){o.includes(c)&&(i+=12),r.includes(c)&&(i+=4),s.has(c)&&(i+=6),e.path.toLowerCase().includes(c)&&(i+=3);for(let d of e.sections){let u=d.title.toLowerCase(),m=d.body.toLowerCase();u.includes(c)&&(i+=5,a??=d.title),m.includes(c)&&(i+=2,a??=d.title)}}return i<=0?null:{entry:e,score:i,matchedSection:a}}function yi(e,t=12){let n=e.trim();if(!n)return[];let o=Dv(n),r=[];for(let s of mn.entries){let i=Uv(s,o,n);i&&r.push(i)}return r.sort((s,i)=>i.score-s.score||s.entry.title.localeCompare(i.entry.title)),r.slice(0,t)}function wi(e){let{entry:t,matchedSection:n}=e,o=n?`${t.summary.slice(0,120)} (${n})`:t.summary.slice(0,160);return{id:t.id,path:t.path,title:t.title,summary:o,relatedCommands:t.relatedCommands}}function ur(e){return mn.entries.find(t=>t.id===e)}function bi(e){let t=e.replace(/\\/g,"/").replace(/^\.\//,"");return mn.entries.find(n=>n.path===t||n.id===t)}function ki(e,t=1800){let n=[];e.summary&&n.push(e.summary);for(let r of e.sections){if(n.join(`
339
364
 
340
365
  `).length>=t)break;n.push(`${r.title}
341
366
  ${r.body.slice(0,600)}`)}let o=n.join(`
342
367
 
343
- `).trim();return o.length>t&&(o=`${o.slice(0,t-1)}\u2026`),o}function ni(e){return e.relatedCommands.find(n=>/\bhelp\b/i.test(n))??e.relatedCommands[0]}function jm(){return p(["Documentation search (offline)","","/docs search <topic> \u2014 find guides by keyword","/docs list \u2014 repeat last search results","/docs <n> \u2014 excerpt + links for result #n","/docs show <n> \u2014 same as /docs <n>","/docs follow <n> \u2014 run primary related slash help","/help search <topic> \u2014 alias for /docs search","","Also: omnish docs search <topic> on the host terminal"].join(`
344
- `))}function xl(e,t){if(e.length===0)return p(`${t}
368
+ `).trim();return o.length>t&&(o=`${o.slice(0,t-1)}\u2026`),o}function vi(e){return e.relatedCommands.find(n=>/\bhelp\b/i.test(n))??e.relatedCommands[0]}function Ph(){return p(["Documentation search (offline)","","/docs search <topic> \u2014 find guides by keyword","/docs list \u2014 repeat last search results","/docs <n> \u2014 excerpt + links for result #n","/docs show <n> \u2014 same as /docs <n>","/docs follow <n> \u2014 run primary related slash help","/help search <topic> \u2014 alias for /docs search","","Also: omnish docs search <topic> on the host terminal"].join(`
369
+ `))}function ql(e,t){if(e.length===0)return p(`${t}
345
370
  (no results)`);let n=[t,""];return e.forEach((o,r)=>{let s=o.relatedCommands[0],i=s?` \u2014 try ${s}`:"";n.push(`${r+1}. ${o.title}${i}`),n.push(` ${o.path}`),o.summary&&n.push(` ${o.summary.slice(0,140)}`)}),n.push("","Read: /docs <n> \xB7 Run help: /docs follow <n>"),p(n.join(`
346
- `))}function Cl(e,t){let n=Vs(sn.repoUrl,e.path),o=ti(e),r=e.relatedCommands.slice(0,8),s=ni(e),a=[t!==void 0?`${t}. ${e.title}`:e.title,e.path,n,"",o,"","Try:",...r.length?r.map(l=>` ${l}`):[" /help \u2014 general commands"],"",s?`Follow: /docs follow ${t??"<n>"} (runs ${s})`:"Follow: /docs follow <n> when a related command is listed"].map(l=>l.startsWith("http")||l.includes("/")?Re(l):l).join(`
347
- `);return p(a)}var Rl=new Map;function Gm(e,t){Rl.set(e,t)}function qm(e){return Rl.get(e)}function Tl(e,t){let n=Number.parseInt(t,10);if(!Number.isFinite(n)||n<1)return null;let o=Rl.get(e);return!o||n>o.length?null:o[n-1]}var oi;function Jm(e){oi=e}function zm(e){let t=Number.parseInt(e,10);return!Number.isFinite(t)||t<1||!oi||t>oi.length?null:oi[t-1]}async function $l(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return{kind:"text",body:jm()};if(/^list\s*$/i.test(o)){let l=qm(t);return l?.length?{kind:"text",body:xl(l,"Last search")}:{kind:"text",body:p("No cached list. /docs search <topic>")}}let r=/^search\s+([\s\S]+)$/i.exec(o);if(r){let l=r[1].trim();if(!l)return{kind:"text",body:p("Usage: /docs search <topic>")};let u=Xs(l).map(Zs);return Gm(t,u),{kind:"text",body:xl(u,`Search: ${l}`)}}let s=/^follow\s+(\d+)\s*$/i.exec(o);if(s){let l=Tl(t,s[1]);if(!l)return{kind:"text",body:p(`No result #${s[1]}. Run /docs search <topic> first.`)};let d=Xo(l.id);if(!d)return{kind:"text",body:p("Entry missing from index.")};let u=ni(d);if(!u)return{kind:"text",body:p(`No related command for "${l.title}". Try /docs ${s[1]} for the doc excerpt.`)};let c=u.startsWith("/")?u:`/${u}`,m=await n(c);return m?.kind==="text",m}let i=/^(?:show\s+)?(\d+)\s*$/i.exec(o);if(i){let l=Tl(t,i[1]);if(!l)return{kind:"text",body:p(`No result #${i[1]}. Run /docs search <topic> first.`)};let d=Xo(l.id);return d?{kind:"text",body:Cl(d,Number.parseInt(i[1],10))}:{kind:"text",body:p("Entry missing from index.")}}let a=ei(o);return a?{kind:"text",body:Cl(a)}:{kind:"text",body:p("Unknown /docs command. /docs help")}}function Ml(e){let t=e.trim();if(t==="/computers"||t.startsWith("/computers "))return t.slice(10).trim();if(t==="/pcs"||t.startsWith("/pcs "))return t.slice(4).trim();let n=t.match(/^\/c(?:$|\s+(.*))/);return n?(n[1]??"").trim():null}function F(e){return{kind:"text",body:e}}function Pk(e,t){return`${e}:${t}`}function Ek(e,t){return`${e}:apps:${t}`}async function Ak(e,t){let n=e.trim();if(n===""||/^status$/i.test(n)||/^show$/i.test(n)||/^get$/i.test(n)){let a=S();return su({gatewayMode:a.gatewayMode,authPresent:lt(),tokenSet:!!Me(a),allowN:a.allowFrom.length,tgAllowN:a.telegramAllowFrom.length,updateBrief:_s(Go())})}if(/^help$/i.test(n))return X(Di());let o=En(n);if(!o)return vu();gr(o);let r=S(),s,i=!1;if(t?.reload){let a=await t.reload();s=a.ok?a.summary:`Reload failed: ${a.error}`}else i=!0;return du(r.gatewayMode,s,i)}function Ik(e){let t=e.trim();if(!gt(t))return uu();let n=Ht(t),o=[];return typeof process.env.TELEGRAM_BOT_TOKEN=="string"&&process.env.TELEGRAM_BOT_TOKEN.trim()&&o.push("TELEGRAM_BOT_TOKEN is set in the environment and overrides config until unset."),n.gatewayMode==="whatsapp"&&o.push('Set gatewayMode to "telegram" or "both" for Telegram to receive messages.'),cu(o)}async function Km(e,t,n){let o=ie(t),r=Jc(n);if(r!==null){let a=zc(o.cwd,r),l=Kc(a);return l.ok?(Rr(t,a),p(`cwd: ${a}`)):p(`cd: ${l.error}`)}let s=await wn(e.shell,n,{timeoutMs:e.syncTimeoutMs,maxBytes:e.syncMaxBytes,cwd:o.cwd}),i=[];return i.push(`$ ${n}`),s.stdout.trim()&&i.push(s.stdout.trimEnd()),s.stderr.trim()&&(i.push("\u2014 stderr \u2014"),i.push(s.stderr.trimEnd())),s.timedOut?i.push(`Timed out (${Math.round(e.syncTimeoutMs/1e3)}s limit).`):s.code!==0&&s.code!==null&&i.push(`Exit ${s.code}`),p(i.join(`
348
- `))}async function an(e,t,n,o,r,s,i,a,l=null,d=!1,u){let c=s.text.trim(),m=s.peerKey,h=c.match(/^!!\s*(start|stop)\s*$/i);if(h)return h[1].toLowerCase()==="start"?(r.set(m,!0),F(wu())):(r.set(m,!1),F(p("Free shell mode off.")));if(c.startsWith(e.commandPrefix)){let k=c.slice(e.commandPrefix.length).trim();if(!k)return F(p(`Send ${e.commandPrefix}<command> or /help`));if(!d&&em(k)){let M=ll(m,k);if(M!==void 0)return await an(e,t,n,o,r,{...s,text:M},i,a,l,!0,u)}return F(await Km(e,m,k))}if(/^\/help\s+files$/i.test(c.trim()))return F(Hi());let f=c.match(/^\/help\s+search\b(?:\s+([\s\S]*))?$/i);if(f){let k=(f[1]??"").trim();return await $l(k?`search ${k}`:"help",m,R=>an(e,t,n,o,r,{...s,text:R},i,a,l,d,u))}let g=c.match(/^\/docs\b(?:\s+([\s\S]*))?$/i);if(g)return await $l(g[1]??"help",m,M=>an(e,t,n,o,r,{...s,text:M},i,a,l,d,u));if(c==="/help"||c==="help")return F(X(_n(e)));if(c.startsWith("/")){if(/^\/files(?:\s+help)?$/i.test(c.trim()))return F(Hi());let k=c.match(/^\/receive\b(?:\s+(\S+))?$/i);if(k){let C=(k[1]??"status").toLowerCase(),x=new Set(["here","cwd","session","dir"]),Y=new Set(["default","global","reset"]);if(x.has(C)){Ei(m,"sessionCwd");let oe=ie(m).cwd;return F(p(`Inbound files will save under this chat\u2019s session folder:
371
+ `))}function zl(e,t){let n=gi(mn.repoUrl,e.path),o=ki(e),r=e.relatedCommands.slice(0,8),s=vi(e),a=[t!==void 0?`${t}. ${e.title}`:e.title,e.path,n,"",o,"","Try:",...r.length?r.map(l=>` ${l}`):[" /help \u2014 general commands"],"",s?`Follow: /docs follow ${t??"<n>"} (runs ${s})`:"Follow: /docs follow <n> when a related command is listed"].map(l=>l.startsWith("http")||l.includes("/")?Te(l):l).join(`
372
+ `);return p(a)}var Kl=new Map;function Eh(e,t){Kl.set(e,t)}function Ah(e){return Kl.get(e)}function Yl(e,t){let n=Number.parseInt(t,10);if(!Number.isFinite(n)||n<1)return null;let o=Kl.get(e);return!o||n>o.length?null:o[n-1]}var Si;function Ih(e){Si=e}function Lh(e){let t=Number.parseInt(e,10);return!Number.isFinite(t)||t<1||!Si||t>Si.length?null:Si[t-1]}async function Ql(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return{kind:"text",body:Ph()};if(/^list\s*$/i.test(o)){let l=Ah(t);return l?.length?{kind:"text",body:ql(l,"Last search")}:{kind:"text",body:p("No cached list. /docs search <topic>")}}let r=/^search\s+([\s\S]+)$/i.exec(o);if(r){let l=r[1].trim();if(!l)return{kind:"text",body:p("Usage: /docs search <topic>")};let d=yi(l).map(wi);return Eh(t,d),{kind:"text",body:ql(d,`Search: ${l}`)}}let s=/^follow\s+(\d+)\s*$/i.exec(o);if(s){let l=Yl(t,s[1]);if(!l)return{kind:"text",body:p(`No result #${s[1]}. Run /docs search <topic> first.`)};let c=ur(l.id);if(!c)return{kind:"text",body:p("Entry missing from index.")};let d=vi(c);if(!d)return{kind:"text",body:p(`No related command for "${l.title}". Try /docs ${s[1]} for the doc excerpt.`)};let u=d.startsWith("/")?d:`/${d}`,m=await n(u);return m?.kind==="text",m}let i=/^(?:show\s+)?(\d+)\s*$/i.exec(o);if(i){let l=Yl(t,i[1]);if(!l)return{kind:"text",body:p(`No result #${i[1]}. Run /docs search <topic> first.`)};let c=ur(l.id);return c?{kind:"text",body:zl(c,Number.parseInt(i[1],10))}:{kind:"text",body:p("Entry missing from index.")}}let a=bi(o);return a?{kind:"text",body:zl(a)}:{kind:"text",body:p("Unknown /docs command. /docs help")}}function Vl(e){let t=e.trim();if(t==="/computers"||t.startsWith("/computers "))return t.slice(10).trim();if(t==="/pcs"||t.startsWith("/pcs "))return t.slice(4).trim();let n=t.match(/^\/c(?:$|\s+(.*))/);return n?(n[1]??"").trim():null}function F(e){return{kind:"text",body:e}}function Bv(e,t){return`${e}:${t}`}function Hv(e,t){return`${e}:apps:${t}`}async function jv(e,t){let n=e.trim();if(n===""||/^status$/i.test(n)||/^show$/i.test(n)||/^get$/i.test(n)){let a=S();return Mu({gatewayMode:a.gatewayMode,authPresent:lt(),tokenSet:!!Me(a),allowN:a.allowFrom.length,tgAllowN:a.telegramAllowFrom.length,updateBrief:ti(nr())})}if(/^help$/i.test(n))return ee(ra());let o=Bn(n);if(!o)return Gu();$r(o);let r=S(),s,i=!1;if(t?.reload){let a=await t.reload();s=a.ok?a.summary:`Reload failed: ${a.error}`}else i=!0;return Ou(r.gatewayMode,s,i)}function Gv(e){let t=e.trim();if(!wt(t))return Lu();let n=qt(t),o=[];return typeof process.env.TELEGRAM_BOT_TOKEN=="string"&&process.env.TELEGRAM_BOT_TOKEN.trim()&&o.push("TELEGRAM_BOT_TOKEN is set in the environment and overrides config until unset."),n.gatewayMode==="whatsapp"&&o.push('Set gatewayMode to "telegram" or "both" for Telegram to receive messages.'),Iu(o)}async function Oh(e,t,n){let o=ae(t),r=fu(n);if(r!==null){let a=gu(o.cwd,r),l=yu(a);return l.ok?(Fr(t,a),p(`cwd: ${a}`)):p(`cd: ${l.error}`)}let s=await $n(e.shell,n,{timeoutMs:e.syncTimeoutMs,maxBytes:e.syncMaxBytes,cwd:o.cwd}),i=[];return i.push(`$ ${n}`),s.stdout.trim()&&i.push(s.stdout.trimEnd()),s.stderr.trim()&&(i.push("\u2014 stderr \u2014"),i.push(s.stderr.trimEnd())),s.timedOut?i.push(`Timed out (${Math.round(e.syncTimeoutMs/1e3)}s limit).`):s.code!==0&&s.code!==null&&i.push(`Exit ${s.code}`),p(i.join(`
373
+ `))}async function hn(e,t,n,o,r,s,i,a,l=null,c=!1,d){let u=s.text.trim(),m=s.peerKey,h=u.match(/^!!\s*(start|stop)\s*$/i);if(h)return h[1].toLowerCase()==="start"?(r.set(m,!0),F(Bu())):(r.set(m,!1),F(p("Free shell mode off.")));if(u.startsWith(e.commandPrefix)){let k=u.slice(e.commandPrefix.length).trim();if(!k)return F(p(`Send ${e.commandPrefix}<command> or /help`));if(!c&&Nm(k)){let R=El(m,k);if(R!==void 0)return await hn(e,t,n,o,r,{...s,text:R},i,a,l,!0,d)}return F(await Oh(e,m,k))}if(/^\/help\s+files$/i.test(u.trim()))return F(aa());let f=u.match(/^\/help\s+search\b(?:\s+([\s\S]*))?$/i);if(f){let k=(f[1]??"").trim();return await Ql(k?`search ${k}`:"help",m,T=>hn(e,t,n,o,r,{...s,text:T},i,a,l,c,d))}let g=u.match(/^\/docs\b(?:\s+([\s\S]*))?$/i);if(g)return await Ql(g[1]??"help",m,R=>hn(e,t,n,o,r,{...s,text:R},i,a,l,c,d));if(u==="/help"||u==="help")return F(ee(Kn(e)));if(u.startsWith("/")){if(/^\/files(?:\s+help)?$/i.test(u.trim()))return F(aa());let k=u.match(/^\/receive\b(?:\s+(\S+))?$/i);if(k){let $=(k[1]??"status").toLowerCase(),E=new Set(["here","cwd","session","dir"]),j=new Set(["default","global","reset"]);if(E.has($)){Yi(m,"sessionCwd");let oe=ae(m).cwd;return F(p(`Inbound files will save under this chat\u2019s session folder:
349
374
  ${oe}
350
375
  (layout: \u2026/<peer>/<date>/<file> under that root).
351
376
 
352
- Change folder with ${e.commandPrefix}cd \u2026 Send /receive default to use the server config again.`))}return Y.has(C)?(Ei(m,"default"),F(p("Per-chat inbound folder cleared. Uploads now follow fileReceiveRootMode in config.json (/files)."))):F(C==="help"?ji():C==="status"?fu(e,m):ji())}if(c==="/reload"||c==="/restart"){if(!a?.reload)return F(p("Reload is only available while the omnish gateway (omnish run) is running."));let C=await a.reload();return F(p(C.ok?C.summary:`Reload failed: ${C.error}`))}if(c==="/updates"||c.startsWith("/updates ")){let C=c.slice(8).trim().toLowerCase();if(C==="cached"||C==="last"){let Y=Go();return F(Y?Co(Y):p("No update snapshot yet. Send /updates (live check) once, or enable updateCheckEnabled and wait for the scheduled check."))}let x=await qo(ot(),S());return F(Co(x))}let M=c.match(/^\/security(?:\s+(\S+))?\s*$/i);if(M){let C=(M[1]??"").toLowerCase(),x=S(),Y=At(x);return F(C==="help"||C==="?"?X(Lu()):C==="summary"||C==="brief"?p(tu(Y,"Send /security for the full report.")):C==="tips"?X(Iu()):C===""||C==="full"||C==="report"?Au(Y):p("Unknown /security subcommand. Try /security, /security summary, /security tips, or /security help"))}let R=c.match(/^\/(gateway|gw|mode)\b(?:\s+(.*))?$/i);if(R){let C=(R[2]??"").trim();return F(await Ak(C,a))}if(c==="/config"||c.startsWith("/config ")){let C=c.slice(7).trim();return F(await Wu(C,a))}let L=am(c);if(L!==null)return F(await lm(e,L));let $=cm(c);if($!==null)return await pl(e,$,m,t);let N=Ml(c);if(N!==null){let C=Zu(e,N,l);return C===null?null:F(C)}let A=c.match(/^\/(send|file)\b(?:\s+([\s\S]+))?$/i);if(A){let C=(A[2]??"").trim();if(!C)return F(Bi());let x=Ds(C);if(!x)return F(Bi());let Y=ie(m).cwd,oe=await Jo(Y,x.selectorPart);if(oe.length===0)return F(p(`No files matched: ${x.selectorPart}`));let ye=await zo(oe);if(!ye.ok)return F(p(ye.error));let ne=ae()?oo(e):e.fileSendMaxBytes,Se=[];for(let Ge of oe){let mt=St(Ge,ne);if("error"in mt)return F(p(mt.error));Se.push({absPath:mt.absPath,category:mt.category,mimetype:mt.mimetype,displayName:mt.displayName,caption:x.caption})}return Se.length===1?{kind:"file",spec:Se[0]}:{kind:"files",specs:Se}}if(c==="/allowlist"){let C=S();return F(lu([{label:"allowFrom (WhatsApp)",items:C.allowFrom},{label:"telegramAllowFrom",items:C.telegramAllowFrom}]))}let z=c.match(/^\/allow\b\s+(.+)$/i);if(z)try{let C=hr(z[1].trim());return F(Ui(C))}catch(C){return F(p(String(C)))}if(/^\/allow\b\s*$/i.test(c))return F(pu());let Z=c.match(/^\/deny\b\s+(.+)$/i);if(Z)try{let C=fr(Z[1].trim());return F(Ui(C))}catch(C){return F(p(String(C)))}if(/^\/deny\b\s*$/i.test(c))return F(mu());let ce=c.match(/^\/(whatsapp|wa|telegram|tg)\b(?:\s+(.*))?$/i);if(ce){let C=ce[1].toLowerCase(),x=(ce[2]??"").trim(),Y=C==="whatsapp"||C==="wa";if(C==="telegram"||C==="tg"){let ye=x.match(/^token\s+(\S+)\s*$/i);return ye?F(Ik(ye[1]??"")):x===""||/^help$/i.test(x)?F(X(au(S()))):F(yu())}if(Y)return x===""||/^help$/i.test(x)?F(X(iu(e))):F(gu())}let ve=c.match(/^\/(cowork|cw)\b(?:\s+([\s\S]*))?$/i);if(ve){let C=(ve[2]??"").trim();return F(await Lm(C,m,e))}let H=c.match(/^\/watch\b(?:\s+([\s\S]*))?$/i);if(H){let C=(H[1]??"").trim(),x=Um(C,m);return x.replies.length===1?F(x.replies[0]):{kind:"texts",bodies:x.replies}}if(c==="/apps"||c.startsWith("/apps "))return F(await Wk(c,m,e,i,o));let he=Lk(c);if(he!==null)return F(await Fk(he,m,e,i,s.mediaSavedPath,u));if(c.startsWith("/bg")){let C=c.slice(3).trim();if(!C)return F(hu());let x=Jd(C);if("error"in x)return F(p(x.error==="empty"?"Usage: /bg <command> or /bg -n <name> <command>.":x.error));let Y=ie(m).cwd,oe=x.notify&&u?.sendToPeer?mt=>{let qh=zd(mt);u.sendToPeer(m,qh).catch(()=>{})}:void 0,{id:ye,meta:ne}=t.spawnJob(e.shell,x.cmd,{cwd:Y,name:x.name,notifyPeerKey:x.notify?m:null,onComplete:oe}),Se=ne.name?`${ye} (${ne.name})`:ye,Ge=x.notify?`
353
- Notify on completion: on`:"";return F(p(`Job ${Se} started.
354
- [cwd: ${Y}]
355
- /log ${ne.name??ye}
356
- /tail ${ne.name??ye}${Ge}`))}if(c==="/tunnels")return F(await Sl("list",e,Cn()));let ee=c.match(/^\/tunnel\b(?:\s+([\s\S]*))?$/i);if(ee){let C=(ee[1]??"").trim();return F(await Sl(C||"help",e,Cn()))}if(c==="/jobs"){let C=t.list().slice(0,20);return C.length===0?F(p("(no jobs yet)")):F(p(C.map(x=>{let Y=x.finishedAt&&x.startedAt?`${((Date.parse(x.finishedAt)-Date.parse(x.startedAt))/1e3).toFixed(1)}s`:"\u2026";return`${x.name?`${x.id} ${x.name}`:x.id} ${x.status} exit=${x.exitCode??"?"} ${Y}
357
- ${x.cmd.slice(0,120)}${x.cmd.length>120?"\u2026":""}`}).join(`
358
-
359
- `)))}let E=c.match(/^\/log\s+(\S+)(?:\s+(\d+))?\s*$/i);if(E){let C=E[1],x=t.resolveJobRef(C);if(!x.ok)return F(p(x.error));let Y=x.id,oe=E[2]?Number.parseInt(E[2],10):e.jobLogTailLines,ye=Number.isFinite(oe)&&oe>0?Math.min(oe,500):e.jobLogTailLines;return F(p(t.tailLog(Y,ye)))}let j=c.match(/^\/tail\s+(\S+)\s*$/i);if(j){let C=j[1],x=t.resolveJobRef(C);if(!x.ok)return F(p(x.error));let Y=x.id,oe=Pk(m,Y),ye=n.get(oe)??0,{text:ne,nextOffset:Se}=t.readSince(Y,ye);return n.set(oe,Se),F(ne?p(ne.trimEnd()||"(no new output)"):p(`(no new output; offset ${Se})`))}let K=c.match(/^\/kill\s+(\S+)\s*$/i);if(K){let C=K[1],x=t.resolveJobRef(C);return x.ok?F(p(t.kill(x.id))):F(p(x.error))}let J=c.match(/^\/(shortcut|shortcuts|alias|aliases)\b(?:\s+([\s\S]*))?$/i);if(J){let C=J[1].toLowerCase(),x=(J[2]??"").trim();return C==="shortcuts"&&!x?x="list":((C==="alias"||C==="aliases")&&!x||C==="shortcut"&&!x)&&(x="help"),F(await _k(x,m,e))}if(!d){let C=c.trim().match(/^\/([a-zA-Z0-9][a-zA-Z0-9_-]{0,31})\s*$/);if(C){let x=C[1],Y=ll(m,x);if(Y!==void 0)return await an(e,t,n,o,r,{...s,text:Y},i,a,l,!0,u)}}return F(bu(e))}let y=c.match(/^>(\S+)\s*(.*)$/s);if(y){let k=y[1],M=y[2]??"",R=await i.writeNamedLine(m,k,M);return R?F(p(R)):null}if(await i.writeFocusedLine(m,c))return null;if(r.get(m)&&c)return F(await Km(e,m,c));let b=await um(e,c,m,t);return b!==null?b:e.chatLlmFallbackEnabled&&e.chatLlmShellCommand.trim().length>0&&u?.onPlainTextLlmFallback?(u.onPlainTextLlmFallback(m,c),null):F(ku(e,c))}function Lk(e){return e==="/run"||e.startsWith("/run ")?e.slice(4).trim():e==="/r"||e.startsWith("/r ")?e.slice(2).trim():null}function Ok(e){let t=e.trim();if(!t)return null;let n=!1,o=null,r=!0;for(;r;){r=!1;let s=t.trim(),i=s.match(/^(?:--queue|-q)\s+([\s\S]+)$/i);if(i){n=!0,t=i[1].trim(),r=!0;continue}if(/^(?:--attach|-a)(?:\s+|$)/i.test(s)){o=!0,t=s.replace(/^(?:--attach|-a)\s*/i,"").trim(),r=!0;continue}if(/^(?:--detach|-d)(?:\s+|$)/i.test(s)){o=!1,t=s.replace(/^(?:--detach|-d)\s*/i,"").trim(),r=!0;continue}}return t?{task:t,queued:n,attach:o}:null}function Nk(e){let n=e.trim().match(/^(\S+)\s+([\s\S]+)$/);if(!n)return null;let o=Ok(n[2]);return o?{recipe:n[1],task:o.task,queued:o.queued,attach:o.attach}:null}async function Fk(e,t,n,o,r,s){let i=e.trim();if(!i||/^help$/i.test(i))return X(Tu());let a=/^online\b([\s\S]*)$/i.exec(i);if(a)return Tm((a[1]??"").trim(),t,n);let l=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(i);if(l){let b=$m(t,n,l[1],l[2]??"");if(!b.ok)return p(b.error);let k=await mo(b.body);return p(k.ok?k.message:k.error)}let d=/^list\b([\s\S]*)$/i.exec(i);if(d){let b=(d[1]??"").trim(),{filter:k,bad:M}=wd(b);if(M)return p(`Unknown /run list suffix: "${M}". Use: list | list --chat | list -p | list --global | list -g`);if(k==="merged")return $u(Sd(t,n));let R=kd(t,n,k);return Eu(R,k)}let u=/^show\b([\s\S]*)$/i.exec(i);if(u){let b=(u[1]??"").trim(),{mode:k,remainder:M}=yd(b),R=/^(\S+)\s*$/i.exec(M);if(!R?.[1])return p("Usage: /run show <name> \u2014 or show --global|-g|--chat|-p <name> (-g shared, -p private)");let L=R[1];if(k==="resolved"){let ce=Ye(t,n,L);return ce?Ji(ce):zi(L)}let $=k==="global"?"global":"chat",N=Nt($,t,n,L);if(N)return Ji(N,$==="global"?"From gateway-shared recipes (--global).":"From this chat only (--chat).");let A=$==="chat"&&Nt("global",t,n,L)?`
360
- (Gateway-shared recipe exists: /run show --global ${L})`:"",z=$==="global"&&Nt("chat",t,n,L)?`
361
- (This chat stores an override: /run show --chat ${L})`:"",Z=$==="global"?`Unknown recipe "${L}" in gateway-shared storage.`:`Unknown recipe "${L}" in this chat storage.`;return p(`${Z}${A}${z}`)}let c=/^add\b([\s\S]*)$/i.exec(i);if(c){let{scope:b,remainder:k}=Ca((c[1]??"").trim()),M=k.match(/^(\S+)\s+([\s\S]+)$/);if(!M)return p(b==="global"?'Usage: /run add --global <name> <command\u2026> [--template "\u2026"] \u2014 cmd must reference "$OMNISH_TASK"':"Usage: /run add [--global|-g|--chat|-p] <name> <command\u2026> \u2014 see /run help");try{let R=Ra(M[2],n.recipesMacroDefaultCommand);zn(t,M[1],R,b);let L=vt(M[1]),$=L.ok?L.normalized:M[1].toLowerCase(),N=Nt(b,t,n,$)??Ye(t,n,$);return N?To($,N,b):p("Recipe save failed.")}catch(R){return p(String(R))}}let m=/^set\b([\s\S]*)$/i.exec(i);if(m){let{scope:b,remainder:k,explicit:M}=xa((m[1]??"").trim()),R=bd(k);if(R){if(M)return p("Cannot combine a leading scope flag (-g, --global, --chat, -p) with a trailing scope flag on the same line.");let N=vt(R.name);if(!N.ok)return p(N.error);let A=Ta(t,N.normalized,R.target,n);if(!A.ok)return p(A.error);if(A.kind==="noop")return p(A.message);let z=Nt(A.target,t,n,N.normalized)??Ye(t,n,N.normalized);return z?To(N.normalized,z,A.target):p("Recipe scope update failed.")}let L=k.match(/^(\S+)\s*$/);if(L?.[1]&&M){let N=vt(L[1]);if(!N.ok)return p(N.error);let A=Ta(t,N.normalized,b,n);if(!A.ok)return p(A.error);if(A.kind==="noop")return p(A.message);let z=Nt(A.target,t,n,N.normalized)??Ye(t,n,N.normalized);return z?To(N.normalized,z,A.target):p("Recipe scope update failed.")}let $=k.match(/^(\S+)\s+([\s\S]+)$/);if(!$)return p(b==="global"?'Usage: /run set --global <name> <command\u2026> [--template "\u2026"] \u2014 or scope-only: /run set --global <name> | /run set <name> -g':"Usage: /run set [--global|-g|--chat|-p] <name> <command\u2026> \u2014 or move scope without changing the body: /run set -g <name> | /run set <name> -p \u2014 see /run help");try{let N=Ra($[2],n.recipesMacroDefaultCommand);zn(t,$[1],N,b);let A=vt($[1]),z=A.ok?A.normalized:$[1].toLowerCase(),Z=Nt(b,t,n,z)??Ye(t,n,z);return Z?To(z,Z,b):p("Recipe save failed.")}catch(N){return p(String(N))}}let h=/^(?:remove|rm|del)\b([\s\S]*)$/i.exec(i);if(h){let{scope:b,remainder:k}=Ca((h[1]??"").trim()),M=k.match(/^(\S+)\s*$/);if(!M?.[1])return p(b==="global"?"Usage: /run remove --global <name> (aliases: rm, del)":"Usage: /run remove [--global|-g|--chat|-p] <name>");let R=M[1];return vd(t,R,b)?Mu(R,b):b==="chat"&&Nt("global",t,n,R)?p(`No recipe "${R}" in this chat. There is a gateway-shared recipe with that name \u2014 remove it with:
362
- /run remove --global ${R}`):Pu(R,b)}let f=/^queue\s+load\b([\s\S]*)$/i.exec(i);if(f){let b=(f[1]??"").trim(),k=om(n),M=null,R=/^json(?:\s+([\s\S]+))?$/i.exec(b);if(R){let A=(R[1]??"").trim();if(!A)return p('Usage: /run queue load json [{"recipe":"\u2026","task":"\u2026"}, \u2026] \u2014 or { "tasks": [ \u2026 ] }');M=A}else if(b.length>0){let A=b;(A.startsWith('"')&&A.endsWith('"')||A.startsWith("'")&&A.endsWith("'"))&&(A=A.slice(1,-1));let z=ie(t).cwd,Z=await Jo(z,A);if(Z.length===0)return p(`No files matched: ${A}`);if(Z.length>1)return p("Queue load: specify a single JSON file.");let ce=await zo(Z);if(!ce.ok)return p(ce.error);let ve=ul(Z[0],k);if(!ve.ok)return p(ve.error);M=ve.text}else if(r){let A=ul(r,k);if(!A.ok)return p(A.error);M=A.text}else return p("Usage: /run queue load <file.json> \u2014 or /run queue load json [\u2026] \u2014 or attach a file with caption /run queue load");let L=rm(M);if(!L.ok)return p(L.error);let $=sm(t,n,L.jobs);if(!$.ok)return p($.error);let N=[];for(let A of $.items)N.push(o.enqueueQueuedRun(t,A,n));return p(N.join(`
363
- `))}if(/^queue$/i.test(i))return p(o.runQueueStatus(t));if(/^queue\s+resume\s*$/i.test(i))return p(o.resumeRunQueue(t,n));let g=Nk(i);if(g){let{recipe:b,task:k,queued:M,attach:R}=g,L=Ma(R,n),$=Ye(t,n,b);if(!$)return zi(b);if($.steps&&$.steps.length>0){let ve=ie(t).cwd,H=$.steps.length,he=$.steps;if(s?.sendToPeer){let ee=s.sendToPeer;(async()=>{let E=[],j=!1;for(let J=0;J<he.length;J++){let C=he[J];if(j){E.push({index:J,label:C.label??`step ${J+1}`,cmd:C.cmd,exitCode:null,timedOut:!1,skipped:!0,output:""});continue}let x=await wn(n.shell,C.cmd,{timeoutMs:n.syncTimeoutMs,maxBytes:n.syncMaxBytes,cwd:ve}),Y=[x.stdout,x.stderr].filter(Boolean).join(`
364
- `).trim(),oe=x.code===0&&!x.timedOut;E.push({index:J,label:C.label??`step ${J+1}`,cmd:C.cmd,exitCode:x.code,timedOut:x.timedOut,skipped:!1,output:Y}),!oe&&!C.continueOnFail&&(j=!0)}let K=xd(b,E);await ee(t,K)})().catch(()=>{})}return p(`Runbook "${b}" started (${H} steps). Results will be sent when complete.`)}let N=$.taskEnv??"OMNISH_TASK";if(!Jn($.command,N))return p(`Recipe "${b}" command must reference "$${N}".`);let A=Kr(k,n.recipesMaxTaskChars);if(!A.ok)return p(A.error);let z=$.promptTemplate?Yr($.promptTemplate,N,A.task):A.task,Z={[N]:z};if(M){let ve={command:$.command,extraEnv:Z,recipeLabel:b,startOptions:L};return p(o.enqueueQueuedRun(t,ve,n))}let ce=Qr(b);return p(o.start(t,ce,$.command,n,Z,L))}let y=i.match(/^(\S+)$/);if(y){let b=y[1],k=b.toLowerCase();return k==="add"||k==="set"?p('Usage: /run add <name> cmd [--template "\u2026"] \u2014 cmd must include "$OMNISH_TASK"'):k==="show"?p("Usage: /run show <name> \u2014 optional show --global|-g|--chat|-p <name>"):k==="remove"||k==="rm"||k==="del"?p("Usage: /run remove [--global|-g|--chat|-p] <name>"):Ye(t,n,k)?p(`Usage: /run ${b} <task text\u2026> \u2014 replaces <<<OMNISH_TASK>>> / $OMNISH_TASK in stored templates`):p(`Unknown recipe "${b}". /run list`)}return p("/run: could not parse. /run help")}async function _k(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return X(Gi());let r=/^online\b([\s\S]*)$/i.exec(o);if(r)return po((r[1]??"").trim(),t,n,Rm);let s=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(o);if(s){let c=Mm(t,s[1],s[2]??"");if(!c.ok)return p(c.error);let m=await mo(c.body);return p(m.ok?m.message:m.error)}let i=/^list\b([\s\S]*)$/i.exec(o);if(i){let c=(i[1]??"").trim(),{filter:m,bad:h}=Qp(c);return h?p(`Unknown /shortcut list suffix: "${h}". Use: list | list --chat | list -p | list --global | list -g`):Su(Xp(t,m))}let a=/^show\b([\s\S]*)$/i.exec(o);if(a){let c=(a[1]??"").trim(),{mode:m,remainder:h}=Yp(c),f=/^(\S+)\s*$/i.exec(h);if(!f?.[1])return p("Usage: /shortcut show <name> \u2014 or show --global|-g|--chat|-p <name> (-g shared, -p private)");let g=f[1];if(m==="resolved"){let L=Hs(t,g);if(!L)return Cu(g);let $=L.scope==="global"?"Shared shortcut (all chats use it unless this chat overrides the name).":"This chat only.";return qi(g,L.body,$)}let y=m==="global"?"global":"chat",b=_t(y,t,g);if(b!==void 0)return qi(g,b,y==="global"?"From the shared shortcut list (--global / -g).":"From this chat only (--chat / -p).");let k=y==="chat"&&_t("global",t,g)!==void 0?`
365
- (Shared shortcut exists: /shortcut show --global ${g})`:"",M=y==="global"&&_t("chat",t,g)!==void 0?`
366
- (This chat overrides the name: /shortcut show --chat ${g})`:"",R=y==="global"?`Unknown shortcut "${g}" in shared shortcuts.`:`Unknown shortcut "${g}" in this chat.`;return p(`${R}${k}${M}`)}let l=/^add\b([\s\S]*)$/i.exec(o);if(l){let{scope:c,remainder:m}=al((l[1]??"").trim()),h=m.match(/^(\S+)\s+([\s\S]+)$/);if(!h)return p(c==="global"?"Usage: /shortcut add --global <name> <command\u2026> (short: add -g <name> <command\u2026>) \u2014 -g shared, -p private":"Usage: /shortcut add [--global|-g|--chat|-p] <name> <command\u2026> \u2014 -g shared, -p private");try{Yo(t,h[1],h[2],c);let f=Ct(h[1]),g=f.ok?f.normalized:h[1].trim().toLowerCase(),y=_t(c,t,g)??"";return Ro(g,y,c)}catch(f){return p(String(f))}}let d=/^set\b([\s\S]*)$/i.exec(o);if(d){let{scope:c,remainder:m,explicit:h}=il((d[1]??"").trim()),f=Vp(m);if(f){if(h)return p("Cannot combine a leading scope flag (-g, --global, --chat, -p) with a trailing flag on the same line.");let b=Ct(f.name);if(!b.ok)return p(b.error);let k=cl(t,b.normalized,f.target);if(!k.ok)return p(k.error);if(k.kind==="noop")return p(k.message);let M=_t(k.target,t,b.normalized)??"";return Ro(b.normalized,M,k.target)}let g=m.match(/^(\S+)\s*$/);if(g?.[1]&&h){let b=Ct(g[1]);if(!b.ok)return p(b.error);let k=cl(t,b.normalized,c);if(!k.ok)return p(k.error);if(k.kind==="noop")return p(k.message);let M=_t(k.target,t,b.normalized)??"";return Ro(b.normalized,M,k.target)}let y=m.match(/^(\S+)\s+([\s\S]+)$/);if(!y)return p(c==="global"?"Usage: /shortcut set --global <name> <command\u2026> \u2014 or scope-only: /shortcut set -g <name> | /shortcut set <name> -g (-g shared)":"Usage: /shortcut set [--global|-g|--chat|-p] <name> <command\u2026> \u2014 or move scope: /shortcut set -g <name> | /shortcut set <name> -p (-g shared, -p private)");try{Yo(t,y[1],y[2],c);let b=Ct(y[1]),k=b.ok?b.normalized:y[1].trim().toLowerCase(),M=_t(c,t,k)??"";return Ro(k,M,c)}catch(b){return p(String(b))}}let u=/^(?:remove|rm|del)\b([\s\S]*)$/i.exec(o);if(u){let{scope:c,remainder:m}=al((u[1]??"").trim()),h=m.match(/^(\S+)\s*$/);if(!h?.[1])return p(c==="global"?"Usage: /shortcut remove --global <name> (aliases: rm, del)":"Usage: /shortcut remove [--global|-g|--chat|-p] <name>");let f=h[1];return Zp(t,f,c)?xu(f,c):c==="chat"&&_t("global",t,f)!==void 0?p(`No shortcut "${f}" in this chat. There is a shared shortcut with that name \u2014 remove it with:
367
- /shortcut remove --global ${f}`):Ru(f,c)}return X(Gi())}async function Wk(e,t,n,o,r){let s=e.slice(5).trim(),i=s.toLowerCase();if(!i||i==="help")return X(Ki());let a=s.match(/^(\S+)\s*(.*)$/s);if(!a)return X(Ki());let l=a[1].toLowerCase(),d=(a[2]??"").trim();if(l==="online")return po(d,t,n,xm);let u=d.match(/^publish\b([\s\S]*)$/i);if(u){let c=l,m=o.getSessionCommand(t,c);if(!m)return p(`No running session "${c}" with a command. /apps list`);let h=Em(c,m,u[1]??"");if(!h.ok)return p(h.error);let f=await mo(h.body);return p(f.ok?f.message:f.error)}switch(l){case"start":{let c=d.match(/^(\S+)\s+([\s\S]+)$/);return c?p(o.start(t,c[1],c[2],n)):p("Usage: /apps start <name> <command\u2026>")}case"attach":{let c=d.split(/\s+/)[0];return c?p(o.attach(t,c)):p("Usage: /apps attach <name>")}case"detach":return p(o.detach(t));case"list":return p(o.list(t));case"info":case"get":{let c=d.split(/\s+/)[0];return p(o.info(t,c||void 0))}case"send":{let c=d.match(/^(\S+)\s+([\s\S]+)$/);return c?p(await o.sendText(t,c[1],c[2])):p("Usage: /apps send <name> <text\u2026>")}case"key":{let c=d.match(/^(\S+)\s+([\s\S]+)$/);return c?p(o.sendKey(t,c[1],c[2].trim())):p("Usage: /apps key <name> <KEY[,KEY\u2026]>")}case"tail":{let c=d.match(/^(\S+)(?:\s+(\d+))?\s*$/);if(!c)return p("Usage: /apps tail <name> [lines]");let m=c[2]?Number.parseInt(c[2],10):n.appsLogTailLines;return p(o.tail(t,c[1],m))}case"since":{let c=d.split(/\s+/)[0];if(!c)return p("Usage: /apps since <name>");let m=Ek(t,c),h=r.get(m)??0,{text:f,nextOffset:g}=o.readSince(t,c,h);return r.set(m,g),p(f.trimEnd()||"(no new log bytes)")}case"mute":{let c=d.split(/\s+/)[0];return c?p(o.mute(t,c)):p("Usage: /apps mute <name>")}case"unmute":{let c=d.split(/\s+/)[0];return c?p(o.unmute(t,c)):p("Usage: /apps unmute <name>")}case"raw":{let c=d.match(/^(\S+)\s+(on|off)\s*$/i);return c?p(o.setRaw(t,c[1],c[2].toLowerCase()==="on")):p("Usage: /apps raw <name> on|off")}case"resize":{let c=d.trim().split(/\s+/).filter(Boolean);if(c.length<3)return p("Usage: /apps resize <name> <cols> <rows>");let m=c[0],h=Number(c[1]),f=Number(c[2]);return!Number.isFinite(h)||!Number.isFinite(f)?p("cols and rows must be numbers."):p(o.resize(t,m,h,f))}case"stop":{let c=d.split(/\s+/)[0];return c?p(o.stop(t,c)):p("Usage: /apps stop <name>")}case"kill":{let c=d.split(/\s+/)[0];return c?p(o.kill(t,c)):p("Usage: /apps kill <name>")}case"rm":{let c=d.split(/\s+/)[0];return c?p(o.rm(t,c)):p("Usage: /apps rm <name>")}default:return p(`Unknown /apps subcommand "${l}". /apps help`)}}function Ym(e,t,n){return!e.clusterEnabled||Ml(t.trim())!==null?!0:n?Xu(n,e):!1}xe();Xe();function Qm(){return p("Not allowlisted on this omnish device.")}async function Zo(e,t,n,o,r,s,i,a,l,d,u,c){if((c?.surface??(s.peerKey.startsWith("tg:")?"telegram":"whatsapp"))==="telegram"){let h=pr(e.telegramAllowFrom),f=s.peerKey.startsWith("tg:")?s.peerKey.slice(3):"";if(!f||!h.has(f)){P.warn({denied:s.peerKey,uid:f},"telegram denied"),await u({kind:"text",body:Qm()});return}}else{let h=dr(e.allowFrom),f=l.startsWith("wa:")&&/^wa:\+\d+$/.test(l)?l.slice(3):s.peerKey.replace(/^wa:/,""),g=te(f)||"";if(!g||!h.has(g)){P.warn({denied:s.peerKey,phone:g,senderKey:l},"denied"),await u({kind:"text",body:Qm()});return}}try{if(!!!(s.mediaSavedPath||s.mediaError)&&!Ym(e,s.text,l))return;if(s.mediaError&&await u({kind:"text",body:p(s.mediaError)}),s.mediaSavedPath&&await u({kind:"text",body:p(`Saved: ${s.mediaSavedPath}`)}),s.text.trim()){let f=await an(e,t,n,o,r,s,i,a,l,!1,d);if(f!==null)if(f.kind==="texts")for(let g of f.bodies)await u({kind:"text",body:g});else await u(f)}}catch(h){P.error({err:String(h)},"inbound handler error"),await u({kind:"text",body:p(`Error: ${String(h)}`)}).catch(()=>{})}}function Uk(){if(process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{Pl.readFileSync(de,"utf8").trim()===String(process.pid)&&Pl.unlinkSync(de)}catch{}}async function Vm(e){let t=new Ft,n=new Map,o=new Map,r=new Map,s=null,i=new Map,a=async(H,he)=>{let ee=i.get(H);s?.sendReply(H,he,ee)},l=async(H,he)=>{let ee=H.startsWith("tg:")?"telegram":"whatsapp",E=i.get(H),j=Ua(H,{kind:"file",spec:he},E,ee);s?.sendRoutedReply(H,j)},d=async(H,he)=>{let ee=i.get(H),E=H.startsWith("tg:")?"telegram":"whatsapp";s?.sendReply(H,Te(p(he),E).text,ee)},u={onPlainTextLlmFallback(H,he){Yn(S(),H,he,ee=>a(H,ee))},sendToPeer:a},c=new en(()=>S(),a),m={async reload(){return{ok:!0,summary:"Attached mode: config reloaded from disk. Messengers run on the omnish platform; this device executes commands locally."}}};if(s=new Ps({env:e,onReplyError:async(H,he,ee)=>{let E=H.startsWith("tg:")?"telegram":"whatsapp";s?.sendReply(H,Te(p(`Error sending: ${he}`),E).text,ee)},onMessage:async H=>{let he=Ya(),ee=H.peerKey;i.set(ee,H.messageId);let E=H.surface==="telegram"?"telegram":"whatsapp",j=H.senderE164&&E==="whatsapp"?`wa:${H.senderE164}`:ee;An()&&P.info({peerKey:ee,senderKey:j,surface:E},"platform inbound message");let K=H.mediaSavedPath,J=H.mediaError;if(H.inboundMedia){let x=Cp(he,ee,H.inboundMedia);K=x.mediaSavedPath??K,J=x.mediaError??J}let C={peerKey:ee,text:H.text,...K?{mediaSavedPath:K}:{},...J?{mediaError:J}:{}};await Zo(he,t,n,o,r,C,c,m,j,u,async x=>{try{if(x.kind==="texts"){for(let oe of x.bodies){let ye=Te(oe,E);s?.sendReply(ee,ye.text,H.messageId)}return}if(x.kind==="text"){let oe=Te(x.body,E);s?.sendReply(ee,oe.text,H.messageId);return}let Y=Ua(ee,x,H.messageId,E);s?.sendRoutedReply(ee,Y)}catch(Y){s?.sendReply(ee,`Error sending file: ${String(Y)}`,H.messageId)}},{surface:E})}}),process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{Pl.writeFileSync(de,`${process.pid}
368
- `,{mode:384})}catch(H){P.warn({err:String(H)},"could not write gateway pidfile")}xs({getCfg:()=>S(),getWaOutbound:()=>null,getTgSendMedia:()=>null,getTgSendText:()=>null,sendPlatformMedia:l,sendPlatformText:d});let h=null,f=S();if(f.webhookEnabled){let H=f.webhookToken||Dk.randomBytes(32).toString("hex");f.webhookToken||O({webhookToken:H}),h=Cs({port:f.webhookPort,host:f.webhookHost,token:H},{sendToPeer:a,getDefaultPeerKey:()=>null}).stop}let g=Ws({getRunningVersion:ot,getConfig:S,log:P}),y=ds({getConfig:S,sendToPeer:a,sendMediaToPeer:l}),b=Cr({getConfig:S,sendToPeer:a}),{deviceId:k,account:M}=await s.connect(),R=await $s(e,M??null),L=5*60*1e3,$=setInterval(()=>{$s(e,null).catch(()=>{})},L);$.unref?.();let N=e.deviceId?.trim();if(N){N!==k&&P.warn({configuredDeviceId:N,registeredDeviceId:k},"platform_device_id does not match registered device id");try{await Va(e,N)}catch(H){P.warn({err:String(H)},"could not set platform default device")}}let A=R?.gatewayMode??M?.gatewayMode,z=R?.connectors.whatsapp??M?.connectors?.whatsapp,Z=R?.connectors.telegram??M?.connectors?.telegram,ce=[z?`whatsapp:${z.linked?"linked":z.status}`:null,Z?`telegram:${Z.linked?"linked":Z.status}`:null].filter(Boolean).join(", ");console.error(`omnish attached to platform (device ${k})`+(A?` \u2014 gatewayMode=${A}`:"")+(ce?` \u2014 ${ce}`:""));let ve=()=>{clearInterval($),y(),b(),g?.(),h?.(),Uk(),ro(),s?.stop(),c.dispose(),t.killAllRunning(),Cn().stopAll().catch(()=>{}),console.error(`
369
- ${T(process.stderr,"shutting down\u2026")}`),process.exit(0)};process.on("SIGINT",ve),process.on("SIGTERM",ve),await new Promise(()=>{})}function El(e,t){return async n=>{if(t.surface==="whatsapp"&&t.waJid){if(n.kind==="file")await e.sendWaMedia?.(t.waJid,n.spec);else if(n.kind==="files")for(let o of n.specs)await e.sendWaMedia?.(t.waJid,o);else if(n.kind==="texts")for(let o of n.bodies)await e.sendWaText?.(t.waJid,Te(o,"whatsapp").text);else await e.sendWaText?.(t.waJid,Te(n.body,"whatsapp").text);return}if(t.surface==="telegram"&&e.sendTg)if(n.kind==="texts")for(let o of n.bodies)await e.sendTg({kind:"text",body:o});else await e.sendTg(n)}}pe();st();G();function ri(){if(ae()){let i=S(),a=At(i);return xo(a)?{ok:!1,message:`Fix security errors before starting the gateway.
370
-
371
- ${a.filter(u=>u.severity==="error").map(u=>{let c=[u.message];return u.detail&&c.push(u.detail),u.fixHint&&c.push(`Fix: ${u.fixHint}`),c.join(`
377
+ Change folder with ${e.commandPrefix}cd \u2026 Send /receive default to use the server config again.`))}return j.has($)?(Yi(m,"default"),F(p("Per-chat inbound folder cleared. Uploads now follow fileReceiveRootMode in config.json (/files)."))):F($==="help"?la():$==="status"?_u(e,m):la())}if(u==="/reload"||u==="/restart"){if(!a?.reload)return F(p("Reload is only available while the omnish gateway (omnish run) is running."));let $=await a.reload();return F(p($.ok?$.summary:`Reload failed: ${$.error}`))}if(u==="/updates"||u.startsWith("/updates ")){let $=u.slice(8).trim().toLowerCase();if($==="cached"||$==="last"){let j=nr();return F(j?No(j):p("No update snapshot yet. Send /updates (live check) once, or enable updateCheckEnabled and wait for the scheduled check."))}let E=await or(rt(),S());return F(No(E))}let R=u.match(/^\/security(?:\s+(\S+))?\s*$/i);if(R){let $=(R[1]??"").toLowerCase(),E=S(),j=Ot(E);return F($==="help"||$==="?"?ee(nd()):$==="summary"||$==="brief"?p(Cu(j,"Send /security for the full report.")):$==="tips"?ee(td()):$===""||$==="full"||$==="report"?ed(j):p("Unknown /security subcommand. Try /security, /security summary, /security tips, or /security help"))}let T=u.match(/^\/(gateway|gw|mode)\b(?:\s+(.*))?$/i);if(T){let $=(T[2]??"").trim();return F(await jv($,a))}if(u==="/config"||u.startsWith("/config ")){let $=u.slice(7).trim();return F(await ad($,a))}let L=Hm(u);if(L!==null)return F(await jm(e,L));let x=Gm(u);if(x!==null)return await Nn(e,x,m,t,d);let O=Jm(u);if(O!==null)return await Nl(e,O,m,t,d);let A=qm(u);if(A!==null)return await Km(e,A,m,t,d);let Y=zm(u);if(Y!==null)return await Ym(e,Y,m,t,d);let te=Vl(u);if(te!==null){let $=Sd(e,te,l);return $===null?null:F($)}let ue=u.match(/^\/(send|file)\b(?:\s+([\s\S]+))?$/i);if(ue){let $=(ue[2]??"").trim();if(!$)return F(ia());let E=oi($);if(!E)return F(ia());let j=ae(m).cwd,oe=await rr(j,E.selectorPart);if(oe.length===0)return F(p(`No files matched: ${E.selectorPart}`));let de=await sr(oe);if(!de.ok)return F(p(de.error));let jt=re()?rn(e):e.fileSendMaxBytes,$t=[];for(let Lf of oe){let _n=xt(Lf,jt);if("error"in _n)return F(p(_n.error));$t.push({absPath:_n.absPath,category:_n.category,mimetype:_n.mimetype,displayName:_n.displayName,caption:E.caption})}return $t.length===1?{kind:"file",spec:$t[0]}:{kind:"files",specs:$t}}if(u==="/allowlist"){let $=S();return F(Au([{label:"allowFrom (WhatsApp)",items:$.allowFrom},{label:"telegramAllowFrom",items:$.telegramAllowFrom}]))}let xe=u.match(/^\/allow\b\s+(.+)$/i);if(xe)try{let $=Rr(xe[1].trim());return F(sa($))}catch($){return F(p(String($)))}if(/^\/allow\b\s*$/i.test(u))return F(Nu());let W=u.match(/^\/deny\b\s+(.+)$/i);if(W)try{let $=Tr(W[1].trim());return F(sa($))}catch($){return F(p(String($)))}if(/^\/deny\b\s*$/i.test(u))return F(Fu());let fe=u.match(/^\/(whatsapp|wa|telegram|tg)\b(?:\s+(.*))?$/i);if(fe){let $=fe[1].toLowerCase(),E=(fe[2]??"").trim(),j=$==="whatsapp"||$==="wa";if($==="telegram"||$==="tg"){let de=E.match(/^token\s+(\S+)\s*$/i);return de?F(Gv(de[1]??"")):E===""||/^help$/i.test(E)?F(ee(Eu(S()))):F(Uu())}if(j)return E===""||/^help$/i.test(E)?F(ee(Pu(e))):F(Du())}let V=u.match(/^\/(cowork|cw)\b(?:\s+([\s\S]*))?$/i);if(V){let $=(V[2]??"").trim();return F(await bh($,m,e))}let P=u.match(/^\/watch\b(?:\s+([\s\S]*))?$/i);if(P){let $=(P[1]??"").trim(),E=Th($,m);return E.replies.length===1?F(E.replies[0]):{kind:"texts",bodies:E.replies}}if(u==="/apps"||u.startsWith("/apps "))return F(await Qv(u,m,e,i,o));let G=Jv(u);if(G!==null)return F(await Kv(G,m,e,i,s.mediaSavedPath,d));if(u.startsWith("/bg")){let $=u.slice(3).trim();if(!$)return F(Wu());let E=Lp($);if("error"in E)return F(p(E.error==="empty"?"Usage: /bg <command> or /bg -n <name> <command>.":E.error));let j=ae(m).cwd,{id:oe,meta:de}=t.spawnJob(e.shell,E.cmd,{cwd:j,name:E.name,notifyPeerKey:E.notify?m:null}),jt=de.name?`${oe} (${de.name})`:oe,$t=E.notify?`
378
+ Notify on completion: on`:"";return F(p(`Job ${jt} started.
379
+ [cwd: ${j}]
380
+ /log ${de.name??oe}
381
+ /tail ${de.name??oe}${$t}`))}if(u==="/tunnels")return F(await Jl("list",e,Ln()));let Q=u.match(/^\/tunnel\b(?:\s+([\s\S]*))?$/i);if(Q){let $=(Q[1]??"").trim();return F(await Jl($||"help",e,Ln()))}if(u==="/jobs"){let $=t.list().slice(0,20);return $.length===0?F(p("(no jobs yet)")):F(p($.map(E=>{let j=E.finishedAt&&E.startedAt?`${((Date.parse(E.finishedAt)-Date.parse(E.startedAt))/1e3).toFixed(1)}s`:"\u2026";return`${E.name?`${E.id} ${E.name}`:E.id} ${E.status} exit=${E.exitCode??"?"} ${j}
382
+ ${E.cmd.slice(0,120)}${E.cmd.length>120?"\u2026":""}`}).join(`
383
+
384
+ `)))}let J=u.match(/^\/log\s+(\S+)(?:\s+(\d+))?\s*$/i);if(J){let $=J[1],E=t.resolveJobRef($);if(!E.ok)return F(p(E.error));let j=E.id,oe=J[2]?Number.parseInt(J[2],10):e.jobLogTailLines,de=Number.isFinite(oe)&&oe>0?Math.min(oe,500):e.jobLogTailLines;return F(p(t.tailLog(j,de)))}let le=u.match(/^\/tail\s+(\S+)\s*$/i);if(le){let $=le[1],E=t.resolveJobRef($);if(!E.ok)return F(p(E.error));let j=E.id,oe=Bv(m,j),de=n.get(oe)??0,{text:jt,nextOffset:$t}=t.readSince(j,de);return n.set(oe,$t),F(jt?p(jt.trimEnd()||"(no new output)"):p(`(no new output; offset ${$t})`))}let K=u.match(/^\/kill\s+(\S+)\s*$/i);if(K){let $=K[1],E=t.resolveJobRef($);return E.ok?F(p(t.kill(E.id))):F(p(E.error))}let _e=u.match(/^\/(shortcut|shortcuts|alias|aliases)\b(?:\s+([\s\S]*))?$/i);if(_e){let $=_e[1].toLowerCase(),E=(_e[2]??"").trim();return $==="shortcuts"&&!E?E="list":(($==="alias"||$==="aliases")&&!E||$==="shortcut"&&!E)&&(E="help"),F(await Yv(E,m,e))}if(!c){let $=u.trim().match(/^\/([a-zA-Z0-9][a-zA-Z0-9_-]{0,31})\s*$/);if($){let E=$[1],j=El(m,E);if(j!==void 0)return await hn(e,t,n,o,r,{...s,text:j},i,a,l,!0,d)}}return F(Hu(e))}let y=u.match(/^>(\S+)\s*(.*)$/s);if(y){let k=y[1],R=y[2]??"",T=await i.writeNamedLine(m,k,R);return T?F(p(T)):null}if(await i.writeFocusedLine(m,u))return null;if(r.get(m)&&u)return F(await Oh(e,m,u));let b=await Qm(e,u,m,t,d);return b!==null?b:e.chatLlmFallbackEnabled&&e.chatLlmShellCommand.trim().length>0&&d?.onPlainTextLlmFallback?(d.onPlainTextLlmFallback(m,u),null):F(ju(e,u))}function Jv(e){return e==="/run"||e.startsWith("/run ")?e.slice(4).trim():e==="/r"||e.startsWith("/r ")?e.slice(2).trim():null}function qv(e){let t=e.trim();if(!t)return null;let n=!1,o=null,r=!0;for(;r;){r=!1;let s=t.trim(),i=s.match(/^(?:--queue|-q)\s+([\s\S]+)$/i);if(i){n=!0,t=i[1].trim(),r=!0;continue}if(/^(?:--attach|-a)(?:\s+|$)/i.test(s)){o=!0,t=s.replace(/^(?:--attach|-a)\s*/i,"").trim(),r=!0;continue}if(/^(?:--detach|-d)(?:\s+|$)/i.test(s)){o=!1,t=s.replace(/^(?:--detach|-d)\s*/i,"").trim(),r=!0;continue}}return t?{task:t,queued:n,attach:o}:null}function zv(e){let n=e.trim().match(/^(\S+)\s+([\s\S]+)$/);if(!n)return null;let o=qv(n[2]);return o?{recipe:n[1],task:o.task,queued:o.queued,attach:o.attach}:null}async function Kv(e,t,n,o,r,s){let i=e.trim();if(!i||/^help$/i.test(i))return ee(Yu());let a=/^online\b([\s\S]*)$/i.exec(i);if(a)return ph((a[1]??"").trim(),t,n);let l=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(i);if(l){let b=mh(t,n,l[1],l[2]??"");if(!b.ok)return p(b.error);let k=await Co(b.body);return p(k.ok?k.message:k.error)}let c=/^list\b([\s\S]*)$/i.exec(i);if(c){let b=(c[1]??"").trim(),{filter:k,bad:R}=ip(b);if(R)return p(`Unknown /run list suffix: "${R}". Use: list | list --chat | list -p | list --global | list -g`);if(k==="merged")return Qu(up(t,n));let T=lp(t,n,k);return Zu(T,k)}let d=/^show\b([\s\S]*)$/i.exec(i);if(d){let b=(d[1]??"").trim(),{mode:k,remainder:R}=sp(b),T=/^(\S+)\s*$/i.exec(R);if(!T?.[1])return p("Usage: /run show <name> \u2014 or show --global|-g|--chat|-p <name> (-g shared, -p private)");let L=T[1];if(k==="resolved"){let ue=Ye(t,n,L);return ue?da(ue):pa(L)}let x=k==="global"?"global":"chat",O=_t(x,t,n,L);if(O)return da(O,x==="global"?"From gateway-shared recipes (--global).":"From this chat only (--chat).");let A=x==="chat"&&_t("global",t,n,L)?`
385
+ (Gateway-shared recipe exists: /run show --global ${L})`:"",Y=x==="global"&&_t("chat",t,n,L)?`
386
+ (This chat stores an override: /run show --chat ${L})`:"",te=x==="global"?`Unknown recipe "${L}" in gateway-shared storage.`:`Unknown recipe "${L}" in this chat storage.`;return p(`${te}${A}${Y}`)}let u=/^add\b([\s\S]*)$/i.exec(i);if(u){let{scope:b,remainder:k}=Qa((u[1]??"").trim()),R=k.match(/^(\S+)\s+([\s\S]+)$/);if(!R)return p(b==="global"?'Usage: /run add --global <name> <command\u2026> [--template "\u2026"] \u2014 cmd must reference "$OMNISH_TASK"':"Usage: /run add [--global|-g|--chat|-p] <name> <command\u2026> \u2014 see /run help");try{let T=Va(R[2],n.recipesMacroDefaultCommand);lo(t,R[1],T,b);let L=Ct(R[1]),x=L.ok?L.normalized:R[1].toLowerCase(),O=_t(b,t,n,x)??Ye(t,n,x);return O?Wo(x,O,b):p("Recipe save failed.")}catch(T){return p(String(T))}}let m=/^set\b([\s\S]*)$/i.exec(i);if(m){let{scope:b,remainder:k,explicit:R}=Ya((m[1]??"").trim()),T=ap(k);if(T){if(R)return p("Cannot combine a leading scope flag (-g, --global, --chat, -p) with a trailing scope flag on the same line.");let O=Ct(T.name);if(!O.ok)return p(O.error);let A=Xa(t,O.normalized,T.target,n);if(!A.ok)return p(A.error);if(A.kind==="noop")return p(A.message);let Y=_t(A.target,t,n,O.normalized)??Ye(t,n,O.normalized);return Y?Wo(O.normalized,Y,A.target):p("Recipe scope update failed.")}let L=k.match(/^(\S+)\s*$/);if(L?.[1]&&R){let O=Ct(L[1]);if(!O.ok)return p(O.error);let A=Xa(t,O.normalized,b,n);if(!A.ok)return p(A.error);if(A.kind==="noop")return p(A.message);let Y=_t(A.target,t,n,O.normalized)??Ye(t,n,O.normalized);return Y?Wo(O.normalized,Y,A.target):p("Recipe scope update failed.")}let x=k.match(/^(\S+)\s+([\s\S]+)$/);if(!x)return p(b==="global"?'Usage: /run set --global <name> <command\u2026> [--template "\u2026"] \u2014 or scope-only: /run set --global <name> | /run set <name> -g':"Usage: /run set [--global|-g|--chat|-p] <name> <command\u2026> \u2014 or move scope without changing the body: /run set -g <name> | /run set <name> -p \u2014 see /run help");try{let O=Va(x[2],n.recipesMacroDefaultCommand);lo(t,x[1],O,b);let A=Ct(x[1]),Y=A.ok?A.normalized:x[1].toLowerCase(),te=_t(b,t,n,Y)??Ye(t,n,Y);return te?Wo(Y,te,b):p("Recipe save failed.")}catch(O){return p(String(O))}}let h=/^(?:remove|rm|del)\b([\s\S]*)$/i.exec(i);if(h){let{scope:b,remainder:k}=Qa((h[1]??"").trim()),R=k.match(/^(\S+)\s*$/);if(!R?.[1])return p(b==="global"?"Usage: /run remove --global <name> (aliases: rm, del)":"Usage: /run remove [--global|-g|--chat|-p] <name>");let T=R[1];return cp(t,T,b)?Vu(T,b):b==="chat"&&_t("global",t,n,T)?p(`No recipe "${T}" in this chat. There is a gateway-shared recipe with that name \u2014 remove it with:
387
+ /run remove --global ${T}`):Xu(T,b)}let f=/^queue\s+load\b([\s\S]*)$/i.exec(i);if(f){let b=(f[1]??"").trim(),k=_m(n),R=null,T=/^json(?:\s+([\s\S]+))?$/i.exec(b);if(T){let A=(T[1]??"").trim();if(!A)return p('Usage: /run queue load json [{"recipe":"\u2026","task":"\u2026"}, \u2026] \u2014 or { "tasks": [ \u2026 ] }');R=A}else if(b.length>0){let A=b;(A.startsWith('"')&&A.endsWith('"')||A.startsWith("'")&&A.endsWith("'"))&&(A=A.slice(1,-1));let Y=ae(t).cwd,te=await rr(Y,A);if(te.length===0)return p(`No files matched: ${A}`);if(te.length>1)return p("Queue load: specify a single JSON file.");let ue=await sr(te);if(!ue.ok)return p(ue.error);let xe=Il(te[0],k);if(!xe.ok)return p(xe.error);R=xe.text}else if(r){let A=Il(r,k);if(!A.ok)return p(A.error);R=A.text}else return p("Usage: /run queue load <file.json> \u2014 or /run queue load json [\u2026] \u2014 or attach a file with caption /run queue load");let L=Dm(R);if(!L.ok)return p(L.error);let x=Um(t,n,L.jobs);if(!x.ok)return p(x.error);let O=[];for(let A of x.items)O.push(o.enqueueQueuedRun(t,A,n));return p(O.join(`
388
+ `))}if(/^queue$/i.test(i))return p(o.runQueueStatus(t));if(/^queue\s+resume\s*$/i.test(i))return p(o.resumeRunQueue(t,n));let g=zv(i);if(g){let{recipe:b,task:k,queued:R,attach:T}=g,L=el(T,n),x=Ye(t,n,b);if(!x)return pa(b);if(x.steps&&x.steps.length>0){let xe=ae(t).cwd,W=x.steps.length,fe=x.steps;if(s?.sendToPeer){let V=s.sendToPeer;(async()=>{let P=[],G=!1;for(let J=0;J<fe.length;J++){let le=fe[J];if(G){P.push({index:J,label:le.label??`step ${J+1}`,cmd:le.cmd,exitCode:null,timedOut:!1,skipped:!0,output:""});continue}n.progressUpdates&&await V(t,`Step ${J+1}/${W}: ${le.label??`step ${J+1}`}\u2026`).catch(()=>{});let K=await $n(n.shell,le.cmd,{timeoutMs:n.syncTimeoutMs,maxBytes:n.syncMaxBytes,cwd:xe}),_e=[K.stdout,K.stderr].filter(Boolean).join(`
389
+ `).trim(),$=K.code===0&&!K.timedOut,E={index:J,label:le.label??`step ${J+1}`,cmd:le.cmd,exitCode:K.code,timedOut:K.timedOut,skipped:!1,output:_e};P.push(E),n.progressUpdates&&await V(t,dp(b,E,J,W)).catch(()=>{}),!$&&!le.continueOnFail&&(G=!0)}let Q=pp(b,P);await V(t,Q)})().catch(()=>{})}return p(`Runbook "${b}" started (${W} steps). Results will be sent when complete.`)}let O=x.taskEnv??"OMNISH_TASK";if(!ao(x.command,O))return p(`Recipe "${b}" command must reference "$${O}".`);let A=ms(k,n.recipesMaxTaskChars);if(!A.ok)return p(A.error);let Y=x.promptTemplate?hs(x.promptTemplate,O,A.task):A.task,te={[O]:Y};if(R){let xe={command:x.command,extraEnv:te,recipeLabel:b,startOptions:L};return p(o.enqueueQueuedRun(t,xe,n))}let ue=fs(b);return p(o.start(t,ue,x.command,n,te,L))}let y=i.match(/^(\S+)$/);if(y){let b=y[1],k=b.toLowerCase();return k==="add"||k==="set"?p('Usage: /run add <name> cmd [--template "\u2026"] \u2014 cmd must include "$OMNISH_TASK"'):k==="show"?p("Usage: /run show <name> \u2014 optional show --global|-g|--chat|-p <name>"):k==="remove"||k==="rm"||k==="del"?p("Usage: /run remove [--global|-g|--chat|-p] <name>"):Ye(t,n,k)?p(`Usage: /run ${b} <task text\u2026> \u2014 replaces <<<OMNISH_TASK>>> / $OMNISH_TASK in stored templates`):p(`Unknown recipe "${b}". /run list`)}return p("/run: could not parse. /run help")}async function Yv(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return ee(ca());let r=/^online\b([\s\S]*)$/i.exec(o);if(r)return xo((r[1]??"").trim(),t,n,dh);let s=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(o);if(s){let u=hh(t,s[1],s[2]??"");if(!u.ok)return p(u.error);let m=await Co(u.body);return p(m.ok?m.message:m.error)}let i=/^list\b([\s\S]*)$/i.exec(o);if(i){let u=(i[1]??"").trim(),{filter:m,bad:h}=Am(u);return h?p(`Unknown /shortcut list suffix: "${h}". Use: list | list --chat | list -p | list --global | list -g`):Ju(Lm(t,m))}let a=/^show\b([\s\S]*)$/i.exec(o);if(a){let u=(a[1]??"").trim(),{mode:m,remainder:h}=Em(u),f=/^(\S+)\s*$/i.exec(h);if(!f?.[1])return p("Usage: /shortcut show <name> \u2014 or show --global|-g|--chat|-p <name> (-g shared, -p private)");let g=f[1];if(m==="resolved"){let L=ii(t,g);if(!L)return zu(g);let x=L.scope==="global"?"Shared shortcut (all chats use it unless this chat overrides the name).":"This chat only.";return ua(g,L.body,x)}let y=m==="global"?"global":"chat",b=Ut(y,t,g);if(b!==void 0)return ua(g,b,y==="global"?"From the shared shortcut list (--global / -g).":"From this chat only (--chat / -p).");let k=y==="chat"&&Ut("global",t,g)!==void 0?`
390
+ (Shared shortcut exists: /shortcut show --global ${g})`:"",R=y==="global"&&Ut("chat",t,g)!==void 0?`
391
+ (This chat overrides the name: /shortcut show --chat ${g})`:"",T=y==="global"?`Unknown shortcut "${g}" in shared shortcuts.`:`Unknown shortcut "${g}" in this chat.`;return p(`${T}${k}${R}`)}let l=/^add\b([\s\S]*)$/i.exec(o);if(l){let{scope:u,remainder:m}=Pl((l[1]??"").trim()),h=m.match(/^(\S+)\s+([\s\S]+)$/);if(!h)return p(u==="global"?"Usage: /shortcut add --global <name> <command\u2026> (short: add -g <name> <command\u2026>) \u2014 -g shared, -p private":"Usage: /shortcut add [--global|-g|--chat|-p] <name> <command\u2026> \u2014 -g shared, -p private");try{ar(t,h[1],h[2],u);let f=Tt(h[1]),g=f.ok?f.normalized:h[1].trim().toLowerCase(),y=Ut(u,t,g)??"";return Fo(g,y,u)}catch(f){return p(String(f))}}let c=/^set\b([\s\S]*)$/i.exec(o);if(c){let{scope:u,remainder:m,explicit:h}=Ml((c[1]??"").trim()),f=Im(m);if(f){if(h)return p("Cannot combine a leading scope flag (-g, --global, --chat, -p) with a trailing flag on the same line.");let b=Tt(f.name);if(!b.ok)return p(b.error);let k=Al(t,b.normalized,f.target);if(!k.ok)return p(k.error);if(k.kind==="noop")return p(k.message);let R=Ut(k.target,t,b.normalized)??"";return Fo(b.normalized,R,k.target)}let g=m.match(/^(\S+)\s*$/);if(g?.[1]&&h){let b=Tt(g[1]);if(!b.ok)return p(b.error);let k=Al(t,b.normalized,u);if(!k.ok)return p(k.error);if(k.kind==="noop")return p(k.message);let R=Ut(k.target,t,b.normalized)??"";return Fo(b.normalized,R,k.target)}let y=m.match(/^(\S+)\s+([\s\S]+)$/);if(!y)return p(u==="global"?"Usage: /shortcut set --global <name> <command\u2026> \u2014 or scope-only: /shortcut set -g <name> | /shortcut set <name> -g (-g shared)":"Usage: /shortcut set [--global|-g|--chat|-p] <name> <command\u2026> \u2014 or move scope: /shortcut set -g <name> | /shortcut set <name> -p (-g shared, -p private)");try{ar(t,y[1],y[2],u);let b=Tt(y[1]),k=b.ok?b.normalized:y[1].trim().toLowerCase(),R=Ut(u,t,k)??"";return Fo(k,R,u)}catch(b){return p(String(b))}}let d=/^(?:remove|rm|del)\b([\s\S]*)$/i.exec(o);if(d){let{scope:u,remainder:m}=Pl((d[1]??"").trim()),h=m.match(/^(\S+)\s*$/);if(!h?.[1])return p(u==="global"?"Usage: /shortcut remove --global <name> (aliases: rm, del)":"Usage: /shortcut remove [--global|-g|--chat|-p] <name>");let f=h[1];return Om(t,f,u)?qu(f,u):u==="chat"&&Ut("global",t,f)!==void 0?p(`No shortcut "${f}" in this chat. There is a shared shortcut with that name \u2014 remove it with:
392
+ /shortcut remove --global ${f}`):Ku(f,u)}return ee(ca())}async function Qv(e,t,n,o,r){let s=e.slice(5).trim(),i=s.toLowerCase();if(!i||i==="help")return ee(ma());let a=s.match(/^(\S+)\s*(.*)$/s);if(!a)return ee(ma());let l=a[1].toLowerCase(),c=(a[2]??"").trim();if(l==="online")return xo(c,t,n,ch);let d=c.match(/^publish\b([\s\S]*)$/i);if(d){let u=l,m=o.getSessionCommand(t,u);if(!m)return p(`No running session "${u}" with a command. /apps list`);let h=gh(u,m,d[1]??"");if(!h.ok)return p(h.error);let f=await Co(h.body);return p(f.ok?f.message:f.error)}switch(l){case"start":{let u=c.match(/^(\S+)\s+([\s\S]+)$/);return u?p(o.start(t,u[1],u[2],n)):p("Usage: /apps start <name> <command\u2026>")}case"attach":{let u=c.split(/\s+/)[0];return u?p(o.attach(t,u)):p("Usage: /apps attach <name>")}case"detach":return p(o.detach(t));case"list":return p(o.list(t));case"info":case"get":{let u=c.split(/\s+/)[0];return p(o.info(t,u||void 0))}case"send":{let u=c.match(/^(\S+)\s+([\s\S]+)$/);return u?p(await o.sendText(t,u[1],u[2])):p("Usage: /apps send <name> <text\u2026>")}case"key":{let u=c.match(/^(\S+)\s+([\s\S]+)$/);return u?p(o.sendKey(t,u[1],u[2].trim())):p("Usage: /apps key <name> <KEY[,KEY\u2026]>")}case"tail":{let u=c.match(/^(\S+)(?:\s+(\d+))?\s*$/);if(!u)return p("Usage: /apps tail <name> [lines]");let m=u[2]?Number.parseInt(u[2],10):n.appsLogTailLines;return p(o.tail(t,u[1],m))}case"since":{let u=c.split(/\s+/)[0];if(!u)return p("Usage: /apps since <name>");let m=Hv(t,u),h=r.get(m)??0,{text:f,nextOffset:g}=o.readSince(t,u,h);return r.set(m,g),p(f.trimEnd()||"(no new log bytes)")}case"mute":{let u=c.split(/\s+/)[0];return u?p(o.mute(t,u)):p("Usage: /apps mute <name>")}case"unmute":{let u=c.split(/\s+/)[0];return u?p(o.unmute(t,u)):p("Usage: /apps unmute <name>")}case"raw":{let u=c.match(/^(\S+)\s+(on|off)\s*$/i);return u?p(o.setRaw(t,u[1],u[2].toLowerCase()==="on")):p("Usage: /apps raw <name> on|off")}case"resize":{let u=c.trim().split(/\s+/).filter(Boolean);if(u.length<3)return p("Usage: /apps resize <name> <cols> <rows>");let m=u[0],h=Number(u[1]),f=Number(u[2]);return!Number.isFinite(h)||!Number.isFinite(f)?p("cols and rows must be numbers."):p(o.resize(t,m,h,f))}case"stop":{let u=c.split(/\s+/)[0];return u?p(o.stop(t,u)):p("Usage: /apps stop <name>")}case"kill":{let u=c.split(/\s+/)[0];return u?p(o.kill(t,u)):p("Usage: /apps kill <name>")}case"rm":{let u=c.split(/\s+/)[0];return u?p(o.rm(t,u)):p("Usage: /apps rm <name>")}default:return p(`Unknown /apps subcommand "${l}". /apps help`)}}function Nh(e,t,n){return!e.clusterEnabled||Vl(t.trim())!==null?!0:n?vd(n,e):!1}async function Fh(e,t){if(t.kind==="bundle"){for(let n of t.texts??[])await e({kind:"text",body:n});for(let n of t.files??[])await e({kind:"file",spec:n});return}if(t.kind==="texts"){for(let n of t.bodies)await e({kind:"text",body:n});return}await e(t)}Ce();Ze();function Wh(){return p("Not allowlisted on this omnish device.")}async function dr(e,t,n,o,r,s,i,a,l,c,d,u){if((u?.surface??(s.peerKey.startsWith("tg:")?"telegram":"whatsapp"))==="telegram"){let h=xr(e.telegramAllowFrom),f=s.peerKey.startsWith("tg:")?s.peerKey.slice(3):"";if(!f||!h.has(f)){M.warn({denied:s.peerKey,uid:f},"telegram denied"),await d({kind:"text",body:Wh()});return}}else{let h=Sr(e.allowFrom),f=l.startsWith("wa:")&&/^wa:\+\d+$/.test(l)?l.slice(3):s.peerKey.replace(/^wa:/,""),g=ne(f)||"";if(!g||!h.has(g)){M.warn({denied:s.peerKey,phone:g,senderKey:l},"denied"),await d({kind:"text",body:Wh()});return}}try{if(!!!(s.mediaSavedPath||s.mediaError)&&!Nh(e,s.text,l))return;if(s.mediaError&&await d({kind:"text",body:p(s.mediaError)}),s.mediaSavedPath&&await d({kind:"text",body:p(`Saved: ${s.mediaSavedPath}`)}),s.text.trim()){let f=await hn(e,t,n,o,r,s,i,a,l,!1,c);f!==null&&await Fh(d,f)}}catch(h){M.error({err:String(h)},"inbound handler error"),await d({kind:"text",body:p(`Error: ${String(h)}`)}).catch(()=>{})}}function Xv(){if(process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{Xl.readFileSync(ge,"utf8").trim()===String(process.pid)&&Xl.unlinkSync(ge)}catch{}}async function _h(e){let t=new Map,n=new Map,o=new Map,r=null,s=new Map,i=async(W,fe)=>{let V=s.get(W);r?.sendReply(W,fe,V)},a=new Dt({onJobExit(W){W.notifyPeerKey&&i(W.notifyPeerKey,Is(W))}}),l=async(W,fe)=>{let V=W.startsWith("tg:")?"telegram":"whatsapp",P=s.get(W),G=es(W,{kind:"file",spec:fe},P,V);r?.sendRoutedReply(W,G)},c=async(W,fe)=>{let V=s.get(W),P=W.startsWith("tg:")?"telegram":"whatsapp";r?.sendReply(W,me(p(fe),P).text,V)},d={onPlainTextLlmFallback(W,fe){uo(S(),W,fe,V=>i(W,V))},sendToPeer:i},u=new cn(()=>S(),i),m={async reload(){return{ok:!0,summary:"Attached mode: config reloaded from disk. Messengers run on the omnish platform; this device executes commands locally."}}};if(r=new zs({env:e,onReplyError:async(W,fe,V)=>{let P=W.startsWith("tg:")?"telegram":"whatsapp";r?.sendReply(W,me(p(`Error sending: ${fe}`),P).text,V)},onMessage:async W=>{let fe=yl(),V=W.peerKey;s.set(V,W.messageId);let P=W.surface==="telegram"?"telegram":"whatsapp",G=W.senderE164&&P==="whatsapp"?`wa:${W.senderE164}`:V;Hn()&&M.info({peerKey:V,senderKey:G,surface:P},"platform inbound message");let Q=W.mediaSavedPath,J=W.mediaError;if(W.inboundMedia){let K=sm(fe,V,W.inboundMedia);Q=K.mediaSavedPath??Q,J=K.mediaError??J}let le={peerKey:V,text:W.text,...Q?{mediaSavedPath:Q}:{},...J?{mediaError:J}:{}};await dr(fe,a,t,n,o,le,u,m,G,d,async K=>{try{if(K.kind==="bundle"){for(let $ of K.texts??[]){let E=me($,P);r?.sendReply(V,E.text,W.messageId)}for(let $ of K.files??[]){let E=es(V,{kind:"file",spec:$},W.messageId,P);r?.sendRoutedReply(V,E)}return}if(K.kind==="texts"){for(let $ of K.bodies){let E=me($,P);r?.sendReply(V,E.text,W.messageId)}return}if(K.kind==="text"){let $=me(K.body,P);r?.sendReply(V,$.text,W.messageId);return}let _e=es(V,K,W.messageId,P);r?.sendRoutedReply(V,_e)}catch(_e){r?.sendReply(V,`Error sending file: ${String(_e)}`,W.messageId)}},{surface:P})}}),process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{Xl.writeFileSync(ge,`${process.pid}
393
+ `,{mode:384})}catch(W){M.warn({err:String(W)},"could not write gateway pidfile")}Bs({getCfg:()=>S(),getWaOutbound:()=>null,getTgSendMedia:()=>null,getTgSendText:()=>null,sendPlatformMedia:l,sendPlatformText:c});let h=null,f=S();if(f.webhookEnabled){let W=f.webhookToken||Vv.randomBytes(32).toString("hex");f.webhookToken||N({webhookToken:W}),h=Hs({port:f.webhookPort,host:f.webhookHost,token:W},{sendToPeer:i,getDefaultPeerKey:()=>null}).stop}let g=ni({getRunningVersion:rt,getConfig:S,log:M}),y=Ps({getConfig:S,sendToPeer:i,sendMediaToPeer:l}),b=Nr({getConfig:S,sendToPeer:i}),{deviceId:k,account:R}=await r.connect(),T=await Js(e,R??null),L=5*60*1e3,x=setInterval(()=>{Js(e,null).catch(()=>{})},L);x.unref?.();let O=e.deviceId?.trim();if(O){O!==k&&M.warn({configuredDeviceId:O,registeredDeviceId:k},"platform_device_id does not match registered device id");try{await bl(e,O)}catch(W){M.warn({err:String(W)},"could not set platform default device")}}let A=T?.gatewayMode??R?.gatewayMode,Y=T?.connectors.whatsapp??R?.connectors?.whatsapp,te=T?.connectors.telegram??R?.connectors?.telegram,ue=[Y?`whatsapp:${Y.linked?"linked":Y.status}`:null,te?`telegram:${te.linked?"linked":te.status}`:null].filter(Boolean).join(", ");console.error(`omnish attached to platform (device ${k})`+(A?` \u2014 gatewayMode=${A}`:"")+(ue?` \u2014 ${ue}`:""));let xe=()=>{clearInterval(x),y(),b(),g?.(),h?.(),Xv(),wo(),r?.stop(),u.dispose(),a.killAllRunning(),Ln().stopAll().catch(()=>{}),console.error(`
394
+ ${C(process.stderr,"shutting down\u2026")}`),process.exit(0)};process.on("SIGINT",xe),process.on("SIGTERM",xe),await new Promise(()=>{})}function Zl(e,t){return async n=>{if(t.surface==="whatsapp"&&t.waJid){if(n.kind==="bundle"){for(let o of n.texts??[])await e.sendWaText?.(t.waJid,me(o,"whatsapp").text);for(let o of n.files??[])await e.sendWaMedia?.(t.waJid,o)}else if(n.kind==="file")await e.sendWaMedia?.(t.waJid,n.spec);else if(n.kind==="files")for(let o of n.specs)await e.sendWaMedia?.(t.waJid,o);else if(n.kind==="texts")for(let o of n.bodies)await e.sendWaText?.(t.waJid,me(o,"whatsapp").text);else n.kind==="text"&&await e.sendWaText?.(t.waJid,me(n.body,"whatsapp").text);return}if(t.surface==="telegram"&&e.sendTg)if(n.kind==="bundle"){for(let o of n.texts??[])await e.sendTg({kind:"text",body:o});for(let o of n.files??[])await e.sendTg({kind:"file",spec:o})}else if(n.kind==="texts")for(let o of n.bodies)await e.sendTg({kind:"text",body:o});else await e.sendTg(n)}}pe();Ve();q();function xi(){if(re()){let i=S(),a=Ot(i);return Oo(a)?{ok:!1,message:`Fix security errors before starting the gateway.
395
+
396
+ ${a.filter(d=>d.severity==="error").map(d=>{let u=[d.message];return d.detail&&u.push(d.detail),d.fixHint&&u.push(`Fix: ${d.fixHint}`),u.join(`
372
397
  `)}).join(`
373
398
 
374
399
  `)}`}:{ok:!0}}let e=S(),t=e.gatewayMode,n=t==="whatsapp"||t==="both",o=t==="telegram"||t==="both",r=Me(e);if(o&&!r)return{ok:!1,message:`Telegram enabled (gatewayMode) but no bot token. Set telegramBotToken in config or TELEGRAM_BOT_TOKEN.
375
- config: ${_}`};if(n&&!lt())return{ok:!1,message:"WhatsApp enabled but no session. Run `omnish link` first."};let s=At(e);return xo(s)?{ok:!1,message:`Fix security errors before starting the gateway (or change gatewayMode / token).
400
+ config: ${_}`};if(n&&!lt())return{ok:!1,message:"WhatsApp enabled but no session. Run `omnish link` first."};let s=Ot(e);return Oo(s)?{ok:!1,message:`Fix security errors before starting the gateway (or change gatewayMode / token).
376
401
 
377
- ${s.filter(l=>l.severity==="error").map(l=>{let d=[l.message];return l.detail&&d.push(l.detail),l.fixHint&&d.push(`Fix: ${l.fixHint}`),d.join(`
402
+ ${s.filter(l=>l.severity==="error").map(l=>{let c=[l.message];return l.detail&&c.push(l.detail),l.fixHint&&c.push(`Fix: ${l.fixHint}`),c.join(`
378
403
  `)}).join(`
379
404
 
380
- `)}`}:{ok:!0}}st();G();st();import li from"node:fs";import ci from"node:path";pe();import{spawn as Bk}from"node:child_process";import Xm from"node:fs";import{stdout as Hk}from"node:process";st();G();pn();var Al=["tunnelRelayUrl","platformToken","platformDeviceId"],Zm=[{label:"Platform (attached gateway + tunnel)",keys:["tunnelRelayUrl","platformToken","platformDeviceId"]},{label:"Tunnel",keys:["tunnelEnabled","tunnelMaxActive"]},{label:"Gateway",keys:["gatewayMode","commandPrefix","shell"]},{label:"Cluster",keys:["clusterEnabled","clusterRole","clusterLabel","clusterSenderBindings"]},{label:"Telegram",keys:["telegramBotToken"]},{label:"Apps",keys:["appsCols","appsRows","appsFlushMs","appsMinIntervalMs","appsMaxFlushBytes","appsMaxSessions","appsMaxSessionsTotal","appsMaxWaChars","appsLogTailLines","appsSubmitDelayMs","appsClearInput","appsClearInputDelayMs","appsClearInputSequence","appsSkipClearOnPasswordPrompt","appsPasswordPromptHint"]},{label:"Files",keys:["fileSendMaxBytes","fileReceiveMaxBytes","fileInboxSubdir","fileReceiveRootMode","fileReceiveRootPath"]},{label:"Recipes",keys:["recipesAllowDangerousBuiltins","recipesMaxTaskChars","recipesMacroDefaultCommand","recipesRunAttach"]},{label:"Sync / jobs",keys:["syncTimeoutMs","syncMaxBytes","jobLogTailLines"]},{label:"Service / updates",keys:["serviceInstallFromChat","updateCheckEnabled","updateCheckIntervalMs","updateCheckPackageName","updateInfoUrl"]},{label:"Chat LLM fallback",keys:["chatLlmFallbackEnabled","chatLlmShellCommand","chatLlmTimeoutMs","chatLlmMaxInputChars","chatLlmMaxOutputChars","chatLlmNeedsTty","chatLlmWorkDir"]}];function ii(e){let t=[q(e,"omnish config"),w(e,"Manage ~/.omnish/config.json (env vars still override for platform credentials)."),"",q(e,"Usage:"),` ${v(e,"omnish config add <key> <value> [key value ...]")}`,` ${v(e,"omnish config show <key>[,<key>|*]")}`,` ${v(e,"omnish config edit")}`,` ${v(e,"omnish config edit <key> <value>")}`,` ${v(e,"omnish config delete <key>[,<key>]")}`,"",w(e,"Platform aliases: platform_url \u2192 tunnelRelayUrl, platform_token \u2192 platformToken,"),w(e," platform_device_id \u2192 platformDeviceId"),` ${v(e,"omnish config show platform")} ${w(e,"\u2014 attached-mode diagnostics (URL, token, devices)")}`,w(e," Full setup: omnish help platform"),w(e,"add sets/overwrites scalar keys (not array append). show * lists all keys."),""];console.log(t.join(`
381
- `))}function eh(e){return e.split(/[,\s]+/).map(t=>t.trim()).filter(Boolean)}function Il(e){if(e.length===0)return[];let t=e.join(" ");if(t.includes(",")){let n=[];for(let o of t.split(",")){let r=o.trim();if(!r)continue;let s=r.split(/\s+/);if(s.length<2)throw new Error(`Invalid pair "${r}" \u2014 expected "key value"`);n.push({key:s[0],value:s.slice(1).join(" ")})}return n}if(e.length===2)return[{key:e[0],value:e[1]}];if(e.length%2===0){let n=[];for(let o=0;o<e.length;o+=2)n.push({key:e[o],value:e[o+1]});return n}return[{key:e[0],value:e.slice(1).join(" ")}]}function si(e,t){if(e==="clusterSenderBindings")return JSON.stringify(t.clusterSenderBindings);let n=String(t[e]??"");return e==="platformToken"||e==="telegramBotToken"?hn(e,n):n}function th(e){return e==="tunnelRelayUrl"?{value:Ho(),source:io()}:e==="platformToken"?{value:hn(e,ja()),source:so()}:e==="platformDeviceId"?{value:(Ga()??"")||"(empty)",source:qa()}:null}function jk(e){if(e!=="platformToken"&&e!=="tunnelRelayUrl")return;let n=S().platformToken.trim()||Pt()?.token?.trim()||"";n&&bt({token:n,relayUrl:Ho()})}async function Gk(){let e=process.env.VISUAL?.trim()||process.env.EDITOR?.trim()||(process.platform==="win32"?"notepad":"nano"),t=Xm.readFileSync(_,"utf8");await new Promise((n,o)=>{let r=Bk(e,[_],{stdio:"inherit"});r.on("error",o),r.on("exit",s=>{s===0?n():o(new Error(`Editor exited with code ${s??1}`))})});try{S()}catch(n){throw Xm.writeFileSync(_,t,{mode:384}),new Error(`Invalid config after edit: ${n instanceof Error?n.message:String(n)}`)}}async function nh(e){let t=Hk,n=process.stderr,o=(e[0]??"").trim().toLowerCase(),r=e.slice(1);if(!o||o==="help"||o==="--help"||o==="-h"){ii(t);return}if(o==="add"||o==="edit"){try{if(o==="edit"&&r.length===0){await Gk(),console.log(D(t,`Updated ${_}`));return}let s=Il(r);if(s.length===0){console.error(T(n,"Expected at least one key/value pair.")),process.exitCode=1;return}for(let{key:i,value:a}of s){let l=Lr(i);if(!l){console.error(T(n,`Unknown key "${i}". Try: ${Du().slice(0,8).join(", ")}\u2026 (omnish config show *)`)),process.exitCode=1;return}Dn(l,a),jk(l);let d=S(),u=l==="tunnelRelayUrl"?d.tunnelRelayUrl:Al.includes(l)?hn(l,String(d[l]??"")):si(l,d);console.log(D(t,`${i} \u2192 ${u}`))}console.log(D(t,_))}catch(s){console.error(T(n,s instanceof Error?s.message:String(s))),process.exitCode=1}return}if(o==="show"){let s=r.join(" ").trim()||"*";if(s==="*"){let l=S(),d=[q(t,"Config"),w(t,_),""];for(let m of Zm){d.push(q(t,m.label));for(let h of m.keys){let f=si(h,l),g=th(h);g&&Al.includes(h)?d.push(` ${v(t,h)}: ${f} ${w(t,`(effective: ${g.value}, source: ${g.source})`)}`):d.push(` ${v(t,h)}: ${f}`)}d.push("")}let u=new Set(Zm.flatMap(m=>m.keys)),c=Yi.filter(m=>!u.has(m));if(c.length>0){d.push(q(t,"Other"));for(let m of c)d.push(` ${v(t,m)}: ${si(m,l)}`)}console.log(d.join(`
382
- `));return}let i=eh(s),a=S();for(let l of i){let d=Lr(l);if(!d){console.error(T(n,`Unknown key "${l}".`)),process.exitCode=1;return}let u=si(d,a),c=th(d);c&&Al.includes(d)?console.log(`${l}: ${u} (effective: ${c.value}, source: ${c.source})`):console.log(`${l}: ${u}`)}return}if(o==="delete"){let s=r.join(" ").trim();if(!s){console.error(T(n,"Expected at least one key to delete.")),process.exitCode=1;return}if(s==="*"){console.error(T(n,'Refusing "delete *". List keys explicitly.')),process.exitCode=1;return}try{for(let i of eh(s)){let a=Lr(i);if(!a){console.error(T(n,`Unknown key "${i}".`)),process.exitCode=1;return}Uu(a),console.log(D(t,`cleared ${i}`))}console.log(D(t,_))}catch(i){console.error(T(n,i instanceof Error?i.message:String(i))),process.exitCode=1}return}console.error(T(n,`Unknown subcommand "${o}".`)),ii(t),process.exitCode=1}async function oh(e){let t=ae(),n=[];if(n.push(q(e,"platform")),!t){n.push(` ${w(e,"attached:")} ${we(e,"no")} \u2014 set platform_url + platform_token (omnish config add) or env`);let a=io(),l=so();return(a==="env"||l==="env")&&n.push(` ${w(e,"note:")} ${Q(e,"partial env override (need URL + token)")}`),n}let o=io(),r=so(),s=qa(),i=o==="env"||r==="env"||s==="env"?` ${w(e,"(env overrides config)")}`:"";n.push(` ${w(e,"attached:")} ${v(e,"yes")}${i}`),n.push(` ${w(e,"url:")} ${v(e,t.platformUrl)} ${Q(e,`[${o}]`)}`),n.push(` ${w(e,"token:")} ${v(e,hn("platformToken",t.token))} ${Q(e,`[${r}]`)}`),t.deviceId&&n.push(` ${w(e,"device:")} ${v(e,t.deviceId)} ${Q(e,`[${s}]`)}`);try{let{fetchPlatformAccount:a}=await Promise.resolve().then(()=>(jo(),$p)),l=await a(t);n.push(` ${w(e,"gatewayMode:")} ${v(e,l.gatewayMode)} ${Q(e,"(platform)")}`);let d=c=>{let m=l.connectors[c];return m?m.linked?v(e,"linked"):Q(e,m.status):we(e,"idle")};n.push(` ${w(e,"whatsapp:")} ${d("whatsapp")}`),n.push(` ${w(e,"telegram:")} ${d("telegram")}`);let u=l.routing.onlineCount;n.push(` ${w(e,"routing:")} default=${l.routing.defaultDeviceId??Q(e,"(none)")} online=${u}`),t.deviceId&&l.routing.defaultDeviceId&&t.deviceId!==l.routing.defaultDeviceId&&n.push(` ${w(e,"warn:")} ${we(e,"platform_device_id \u2260 platform defaultDeviceId \u2014 run omnish run or set default on dashboard")}`),u===0&&n.push(` ${w(e,"warn:")} ${we(e,"no device online on platform")}`)}catch{n.push(` ${w(e,"account:")} ${Q(e,"(could not fetch /v1/me \u2014 check token and URL)")}`)}return n}jo();st();function sh(){let e=ae();return e||(console.error(T(process.stderr,"Set platform_url + platform_token (omnish config add) or OMNISH_PLATFORM_URL + OMNISH_TOKEN.")),process.exitCode=1,null)}function qk(e){let t=e.trim();return t?t.startsWith("wa:")?{kind:"wa",value:t.slice(3).trim()}:t.startsWith("tg:")?{kind:"tg",value:t.slice(3).trim()}:/^\+?\d{8,}$/.test(t.replace(/\s/g,""))?{kind:"wa",value:t}:/^\d+$/.test(t)?{kind:"tg",value:t}:null:null}function rh(e){return e.replace(/\D/g,"")}async function ih(){let e=sh();if(!e)return;let t=await xn(e),n=t.connectors.whatsapp,o=t.connectors.telegram;console.log(D(process.stdout,"Platform account")),console.log(` gatewayMode: ${t.gatewayMode}`),console.log(` whatsapp: ${n?.linked?"linked":n?.status??"idle"} (allowlist: ${t.allowFrom.length})`);let r=o?.tokenConfigured?"token saved":"no token";console.log(` telegram: ${o?.linked?"linked":o?.status??"idle"} (${r}, allowlist: ${t.telegramAllowFrom.length})`),console.log(` defaultDeviceId: ${t.defaultDeviceId??"\u2014"}`),console.log(` online devices: ${t.routing.onlineCount}`),t.allowFrom.length&&console.log(` allowFrom: ${t.allowFrom.map(s=>`+${s}`).join(", ")}`),t.telegramAllowFrom.length&&console.log(` telegramAllowFrom: ${t.telegramAllowFrom.join(", ")}`)}async function ah(e){let t=sh();if(!t)return;let n=(e[0]??"").toLowerCase();if(!n||n==="-h"||n==="--help"){console.log(["Usage:"," omnish platform allow list"," omnish platform allow add wa:+15551234567 tg:123456789"," omnish platform allow set --wa +1555,... --tg 123,...",""].join(`
383
- `));return}if(n==="list"){let o=await xn(t);console.log(D(process.stdout,"Platform allowlists")),console.log(` WhatsApp (${o.allowFrom.length}): ${o.allowFrom.length?o.allowFrom.map(r=>`+${r}`).join(", "):"(empty \u2014 any sender)"}`),console.log(` Telegram (${o.telegramAllowFrom.length}): ${o.telegramAllowFrom.length?o.telegramAllowFrom.join(", "):"(empty \u2014 any sender)"}`);return}if(n==="add"){let o=await xn(t),r=[...o.allowFrom],s=[...o.telegramAllowFrom];for(let a of e.slice(1)){let l=qk(a);if(!l){console.error(T(process.stderr,`Unrecognized entry: ${a}`)),process.exitCode=1;return}if(l.kind==="wa"){let d=rh(l.value);d&&!r.includes(d)&&r.push(d)}else{let d=l.value.replace(/\D/g,"");d&&!s.includes(d)&&s.push(d)}}let i=await Ts(t,{allowFrom:r,telegramAllowFrom:s});console.log(D(process.stdout,`Allowlists updated (${i.allowFrom.length} WhatsApp, ${i.telegramAllowFrom.length} Telegram).`));return}if(n==="set"){let o,r;for(let a=1;a<e.length;a++){let l=e[a];l==="--wa"&&e[a+1]?o=e[++a].split(",").map(d=>rh(d.trim())).filter(Boolean):l==="--tg"&&e[a+1]&&(r=e[++a].split(",").map(d=>d.trim().replace(/^tg:/i,"").replace(/\D/g,"")).filter(Boolean))}if(o===void 0&&r===void 0){console.error(T(process.stderr,"Use --wa and/or --tg with comma-separated values.")),process.exitCode=1;return}let s={};o!==void 0&&(s.allowFrom=o),r!==void 0&&(s.telegramAllowFrom=r);let i=await Ts(t,s);console.log(D(process.stdout,`Allowlists set (${i.allowFrom.length} WhatsApp, ${i.telegramAllowFrom.length} Telegram).`));return}console.error(T(process.stderr,`Unknown allow subcommand: ${e[0]}`)),process.exitCode=1}async function lh(e){let t="",n="",o="";for(let r=0;r<e.length;r++){let s=e[r];(s==="--url"||s==="-u")&&e[r+1]?t=e[++r].trim():(s==="--token"||s==="-t")&&e[r+1]?n=e[++r].trim():s==="--device-id"&&e[r+1]&&(o=e[++r].trim())}if(!t||!n){let r=Il(e);for(let s of r)s.key==="platform_url"&&(t=s.value),s.key==="platform_token"&&(n=s.value),s.key==="platform_device_id"&&(o=s.value)}if(!t||!n){console.error(T(process.stderr,"Usage: omnish platform login --url <url> --token <token> [--device-id <id>]")),process.exitCode=1;return}Dn("tunnelRelayUrl",t.replace(/\/$/,"")),Dn("platformToken",n),o&&Dn("platformDeviceId",o),console.log(D(process.stdout,"Platform credentials saved to config.json.")),console.log(" Run: omnish platform status && omnish run")}import Jk from"qrcode-terminal";st();var zk=1e3,Kk=120;function ch(){let e=ae();return e||(console.error(T(process.stderr,"Set platform_url + platform_token (omnish config add) or OMNISH_PLATFORM_URL + OMNISH_TOKEN.")),process.exitCode=1,null)}function Ol(e){let t=e.replace(/\/$/,"");return/^https?:\/\//i.test(t)?t:`http://${t}`}async function uh(e){let t=`${Ol(e.platformUrl)}/v1/connectors/whatsapp/status`,n=await fetch(t,{headers:{Authorization:`Bearer ${e.token}`}}),o=await n.json().catch(()=>({}));if(!n.ok)throw new Error(o.error||`HTTP ${n.status}`);return o}async function Yk(e){let t=await fetch(`${Ol(e.platformUrl)}/v1/connectors/whatsapp/start`,{method:"POST",headers:{"content-type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify({})}),n=await t.json().catch(()=>({}));if(!t.ok)throw new Error(n.error||`HTTP ${t.status}`);return n}function dh(e){let t=process.stdout,n=w(t,"\xB7".repeat(42));console.log(Q(t,"Scan with WhatsApp \u2192 Linked devices")),console.log(n),Jk.generate(e,{small:!0}),console.log(n)}var ai="",Ll=!1;function Qk(e){let t=e.status??"";return t==="qr"||t==="connecting"||t==="reconnecting"||t==="pairing_restart"}async function Vk(e){for(let t=0;t<Kk;t++){let n=await uh(e);if(n.status==="linked"||n.linked)return n;if(n.status==="pairing_restart"&&!Ll&&(Ll=!0,console.log(w(process.stdout,"Finishing WhatsApp link after scan (server restart) \u2014 wait a few seconds\u2026"))),n.qr&&n.qr!==ai&&(ai=n.qr,dh(n.qr)),!Qk(n))throw new Error(n.error||n.statusMessage||`Unexpected status: ${n.status}`);await new Promise(o=>setTimeout(o,zk))}throw new Error("Timed out waiting for WhatsApp to link (scan the QR within ~2 minutes).")}async function ph(){let e=ch();if(!e)return;let t=await uh(e);if(t.status==="linked"||t.linked){console.log(D(process.stdout,"WhatsApp already linked on the platform.")),t.linkedPhoneE164&&(console.log(` Phone: +${t.linkedPhoneE164}`),console.log(` Hint: omnish platform allow add wa:+${t.linkedPhoneE164}`));return}console.log(D(process.stdout,"Starting WhatsApp pairing on the platform\u2026")),Ll=!1,ai="",t=await Yk(e),t.qr&&(dh(t.qr),ai=t.qr),t=await Vk(e),console.log(D(process.stdout,"WhatsApp linked on the platform.")),t.linkedPhoneE164&&(console.log(` Phone: +${t.linkedPhoneE164}`),console.log(` Next: omnish platform allow add wa:+${t.linkedPhoneE164}`),console.log(" Then: omnish run"))}async function mh(){let e=ch();if(!e)return;let t=await fetch(`${Ol(e.platformUrl)}/v1/connectors/whatsapp/unlink`,{method:"POST",headers:{"content-type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify({})}),n=await t.json().catch(()=>({}));if(!t.ok){console.error(T(process.stderr,n.error||`HTTP ${t.status}`)),process.exitCode=1;return}console.log(D(process.stdout,`WhatsApp unlinked (status: ${n.status??"idle"}).`))}import Xk from"ws";function hh(e,t,n){return new Promise(o=>{let r=new Xk(e,{headers:{Authorization:`Bearer ${t}`}}),s=setTimeout(()=>{r.terminate(),o({ok:!1,message:"timeout"})},n);r.once("open",()=>{clearTimeout(s),r.close(),o({ok:!0})}),r.once("unexpected-response",(i,a)=>{clearTimeout(s),o({ok:!1,status:a.statusCode,message:`HTTP ${a.statusCode} ${a.statusMessage}`})}),r.once("error",i=>{clearTimeout(s),o({ok:!1,message:String(i?.message??i)})})})}async function fh(e,t,n=12e3){let o=e.replace(/\/$/,""),r=ao(o),s=await hh(r,t,n),i,a,l="";for(let d of Ms(o)){let u=await hh(d,t,n);if(u.ok)return i=new URL(d).pathname,{ok:!0,controlWsOk:s.ok,deviceWsOk:!0,deviceWsPath:i};a=u.status,l=u.message}return a===400?{ok:!1,controlWsOk:s.ok,deviceWsOk:!1,deviceWsStatus:400,error:"Device WebSocket returned HTTP 400 on /platform/device and could not use /control/device. Redeploy the latest relay image (adds /control/device), or route /platform/* to port 8788 in Caddy/EasyPanel."}:{ok:!1,controlWsOk:s.ok,deviceWsOk:!1,deviceWsStatus:a,error:`Device WebSocket failed: ${l}`}}var Zk=400,ev=8*1024*1024,tv=2*1024*1024;function nv(e){let t=le,n=!1,o=!1;for(let r=0;r<e.length;r++){let s=e[r];s==="-h"||s==="--help"?o=!0:s==="--force"?n=!0:(s==="--auth-dir"||s==="--authDir")&&e[r+1]&&(t=ci.resolve(e[++r]))}return{authDir:t,force:n,help:o}}function ov(e){let t={},n=0,o=0;function r(s,i){let a;try{a=li.readdirSync(i,{withFileTypes:!0})}catch{return}for(let l of a){let d=l.name;if(d==="."||d==="..")continue;let u=ci.join(i,d),m=(s?`${s}/${d}`:d).split(ci.sep).join("/");if(!l.isSymbolicLink()){if(l.isDirectory())r(m,u);else if(l.isFile()){let h=li.readFileSync(u);if(h.length>tv)throw new Error(`file too large: ${m}`);if(n+=h.length,n>ev)throw new Error("total auth size exceeds limit (8 MiB)");if(o+=1,o>Zk)throw new Error("too many files (max 400)");t[m]=h.toString("base64")}}}}return r("",e),t}function ui(e){console.log([me(e,"omnish platform"),w(e,"Attached mode: messengers on the hosted platform; shell on this machine (omnish run)."),"",q(e,"When to use"),w(e," Link WhatsApp/Telegram once on the platform dashboard. Run omnish run on laptops, servers, or containers without local Baileys."),"",q(e,"Prerequisites"),w(e," 1. Platform account token (signup/login on relay dashboard)."),w(e," 2. Messengers linked on the platform (dashboard QR or import-whatsapp)."),w(e," 3. allowFrom / telegramAllowFrom on dashboard Allowlists (Telegram: DM bot /id for your user id)."),"",q(e,"Configure"),` ${v(e,"omnish config add platform_url <url> platform_token <token>")}`,w(e," Optional: platform_device_id <id>"),w(e," Env overrides: OMNISH_PLATFORM_URL, OMNISH_TOKEN, OMNISH_DEVICE_ID"),w(e," (aliases: OMNISH_COMM_LAYER_URL, OMNISH_DEVICE_TOKEN, OMNISH_TUNNEL_*)"),"",q(e,"Workflow"),` ${v(e,"omnish platform login --url <url> --token <t>")} ${w(e,"\u2014 save credentials (or config add)")}`,` ${v(e,"omnish platform status")} ${w(e,"\u2014 account, connectors, allowlists")}`,` ${v(e,"omnish platform allow add tg:<id>")} ${w(e,"\u2014 or dashboard + bot /id")}`,` ${v(e,"omnish platform probe")} ${w(e,"\u2014 test WebSocket routing")}`,` ${v(e,"omnish run")} ${w(e,"\u2014 attach device; expect 'omnish attached to platform'")}`,"",q(e,"Subcommands"),v(e,"omnish platform login --url <url> --token <token> [--device-id <id>]"),w(e," Save platform URL and token to config.json."),"",v(e,"omnish platform status"),w(e," Show gatewayMode, connector status, allowlists, online devices."),"",v(e,"omnish platform allow list|add|set"),w(e," Manage platform allowlists (PUT /v1/me/allowlists)."),"",v(e,"omnish platform probe"),w(e," Test control-plane WebSocket routing (diagnose attached omnish run 400 errors)."),"",v(e,"omnish platform link-whatsapp"),w(e," Pair WhatsApp on the platform (terminal QR + poll until linked)."),"",v(e,"omnish platform unlink-whatsapp"),w(e," Remove platform WhatsApp session and auth files."),"",v(e,"omnish platform import-whatsapp [--auth-dir <path>] [--force]"),w(e," Upload local Baileys auth (after omnish link on this host). Use link-whatsapp or dashboard QR when possible."),w(e," Stop local omnish run before import to avoid WhatsApp session conflicts."),"",w(e,"Standalone omnish link on this host is not used for platform WhatsApp when attached."),w(e,"Docs: docs/guides/platform-reference.md (full API/CLI)"),w(e," docs/guides/platform-attached-mode.md (walkthrough)"),""].join(`
384
- `))}async function rv(e){let{authDir:t,force:n,help:o}=nv(e);if(o){console.log(["Usage: omnish platform import-whatsapp [--auth-dir <path>] [--force]","","Requires OMNISH_PLATFORM_URL and OMNISH_TOKEN.","Default auth directory: ~/.omnish/auth (Baileys useMultiFileAuthState).","Stop `omnish run` on this machine before importing to avoid WhatsApp session conflicts.",""].join(`
385
- `));return}let r=ae();if(!r){console.error(T(process.stderr,"Set OMNISH_PLATFORM_URL (control plane URL) and OMNISH_TOKEN (from dashboard signup/login).")),process.exitCode=1;return}if(!li.existsSync(t)){console.error(T(process.stderr,`Auth directory not found: ${t}`)),process.exitCode=1;return}se();let s=ci.join(t,"creds.json");if(!li.existsSync(s)){console.error(T(process.stderr,`No creds.json in ${t} \u2014 link WhatsApp locally first (omnish link).`)),process.exitCode=1;return}let i;try{i=ov(t)}catch(h){console.error(T(process.stderr,String(h))),process.exitCode=1;return}if(Object.keys(i).length===0){console.error(T(process.stderr,"No files collected (empty auth directory?).")),process.exitCode=1;return}let a=r.platformUrl.replace(/\/$/,""),d=`${/^https?:\/\//i.test(a)?a:`http://${a}`}/v1/connectors/whatsapp/import`,u=await fetch(d,{method:"POST",headers:{"content-type":"application/json",Authorization:`Bearer ${r.token}`},body:JSON.stringify({files:i,force:n})}),c=await u.json().catch(()=>({}));if(!u.ok){console.error(T(process.stderr,c.error||`HTTP ${u.status}`)),process.exitCode=1;return}let m=c.qr?" QR emitted \u2014 open the platform dashboard if you need to scan.":"";console.log(D(process.stdout,`Uploaded Baileys auth. Platform connector status: ${c.status??"unknown"}.${m}`))}async function sv(){let e=process.stdout,t=process.stderr,n=ae();if(!n){console.error(T(t,"Set platform_url + platform_token (omnish config add) or OMNISH_PLATFORM_URL + OMNISH_TOKEN.")),process.exitCode=1;return}console.log(`${me(e,"Platform probe")} ${w(e,n.platformUrl)}`);let o=await fh(n.platformUrl,n.token);console.log(` ${w(e,"wss /control:")} ${o.controlWsOk?v(e,"ok"):we(e,"fail")}`);for(let r of["/platform/device","/control/device"]){if(o.ok&&o.deviceWsPath&&o.deviceWsPath!==r){console.log(` ${w(e,`wss ${r}:`)} ${Q(e,"skipped (fallback used)")}`);continue}if(o.ok&&o.deviceWsPath===r){console.log(` ${w(e,`wss ${r}:`)} ${v(e,"ok")}`);continue}!o.ok&&r==="/platform/device"&&console.log(` ${w(e,`wss ${r}:`)} ${we(e,"fail")}${o.deviceWsStatus?` (HTTP ${o.deviceWsStatus})`:""}`),!o.ok&&r==="/control/device"&&console.log(` ${w(e,`wss ${r}:`)} ${we(e,"fail")}`)}if(o.ok){o.deviceWsPath==="/control/device"&&console.log(w(e," Using /control/device fallback (/platform/* not on control port \u2014 redeploy Caddy when convenient).")),console.log(D(e,"Attached omnish run should be able to connect."));return}console.error(T(t,o.error??"Platform probe failed.")),o.controlWsOk&&!o.deviceWsOk&&console.error(T(t,"Fix: redeploy the latest tunnel-relay image (enables wss /control/device), update omnish CLI, then retry. Also route /platform/* to 8788 when you can (Caddyfile.standalone).")),process.exitCode=1}async function gh(e){let t=(e[0]??"").toLowerCase();if(!t||t==="-h"||t==="--help"){ui(process.stdout);return}if(t==="login"){await lh(e.slice(1));return}if(t==="status"){await ih();return}if(t==="allow"){await ah(e.slice(1));return}if(t==="probe"){await sv();return}if(t==="link-whatsapp"){await ph();return}if(t==="unlink-whatsapp"){await mh();return}if(t==="import-whatsapp"){await rv(e.slice(1));return}console.error(T(process.stderr,`Unknown platform subcommand: ${e[0]}`)),ui(process.stderr),process.exitCode=1}G();import bh from"node:crypto";import $n from"node:fs";import kh from"node:path";function iv(){return bh.randomBytes(24).toString("hex")}function yh(){return bh.randomBytes(32).toString("hex")}function wh(e){try{let t=$n.readFileSync(e,"utf8"),n=JSON.parse(t);if(typeof n.token=="string"&&n.token.length>=16&&typeof n.secret=="string"&&n.secret.length>=16)return{token:n.token,secret:n.secret}}catch{}return null}function av(){let e=wh(ht);if(e)return e;if(e=wh(fo),e){B(kh.dirname(ht)),$n.writeFileSync(ht,JSON.stringify(e,null,2)+`
386
- `,{mode:384});try{$n.unlinkSync(fo)}catch{}return e}return null}function vh(e){B(kh.dirname(ht));let t=av(),n=(e??"").trim();if(n){let r={token:n,secret:t?.secret??yh()};$n.writeFileSync(ht,JSON.stringify(r,null,2)+`
387
- `,{mode:384});try{$n.existsSync(fo)&&$n.unlinkSync(fo)}catch{}return r}if(t)return t;let o={token:iv(),secret:yh()};return $n.writeFileSync(ht,JSON.stringify(o,null,2)+`
388
- `,{mode:384}),o}pe();import ln from"node:fs";import mv from"node:http";import _e from"node:path";import pt from"node:process";import{fileURLToPath as hv}from"node:url";import fv from"node:os";Xe();G();import Nl from"node:crypto";var Fl="omnish_cfg_sess",Sh=7*24*60*60*1e3;function _l(e){let t=Date.now()+Sh,n=Buffer.from(JSON.stringify({exp:t}),"utf8").toString("base64url"),o=Nl.createHmac("sha256",e).update(n).digest("hex");return`${n}.${o}`}function xh(e,t){let n=lv(t??"")[Fl];if(!n||!n.includes("."))return!1;let o=n.lastIndexOf("."),r=n.slice(0,o),s=n.slice(o+1),i=Nl.createHmac("sha256",e).update(r).digest("hex");try{let a=Buffer.from(s,"hex"),l=Buffer.from(i,"hex");if(a.length!==l.length||!Nl.timingSafeEqual(a,l))return!1}catch{return!1}try{let a=JSON.parse(Buffer.from(r,"base64url").toString("utf8"));return!(typeof a.exp!="number"||a.exp<Date.now())}catch{return!1}}function Wl(e){let t=Math.floor(Sh/1e3);return`${Fl}=${e}; HttpOnly; Path=/; SameSite=Lax; Max-Age=${t}`}function Ch(){return`${Fl}=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0`}function lv(e){let t={};for(let n of e.split(";")){let o=n.indexOf("=");if(o===-1)continue;let r=n.slice(0,o).trim(),s=n.slice(o+1).trim();r&&(t[r]=decodeURIComponent(s))}return t}G();import Dl from"node:fs";import di from"node:process";function Rh(e){try{return di.kill(e,0),!0}catch{return!1}}function Th(e){let t=Date.now()+e;for(;Date.now()<t;);}function cv(){try{let e=Dl.readFileSync(sr,"utf8"),t=JSON.parse(e);if(!t||typeof t!="object")return null;let n=t,o=typeof n.pid=="number"?n.pid:Number(n.pid),r=typeof n.port=="number"?n.port:Number(n.port),s=typeof n.host=="string"?n.host:"",i=typeof n.startedAt=="string"?n.startedAt:"";return!Number.isFinite(o)||o<=0||!Number.isFinite(r)||r<=0||r>65535?null:{pid:o,port:r,host:s,startedAt:i}}catch{return null}}function $h(e){Dl.writeFileSync(sr,`${JSON.stringify(e)}
389
- `,{mode:384})}function er(){try{Dl.unlinkSync(sr)}catch{}}function Mh(e){let t=cv();if(t&&t.port===e&&t.pid!==di.pid){if(!Rh(t.pid)){er();return}try{di.kill(t.pid,"SIGTERM")}catch{}if(Th(350),Rh(t.pid)){try{di.kill(t.pid,"SIGKILL")}catch{}Th(100)}er()}}pe();G();import Ul from"node:fs";import uv from"node:process";function dv(e){try{return uv.kill(e,0),!0}catch{return!1}}function pv(){try{let e=Ul.readFileSync(de,"utf8").trim(),t=Number.parseInt(e,10);return Number.isFinite(t)&&t>0?t:null}catch{return null}}function pi(){let e=pv();return e===null?!1:dv(e)}var Bl=class{busy=!1;subscribers=new Set;abort=null;currentSock=null;subscribe(t){return this.subscribers.add(t),()=>{this.subscribers.delete(t)}}emit(t){for(let n of this.subscribers)try{n(t)}catch{}}requestCancel(){this.abort?.abort(),this.currentSock&&(eo(this.currentSock),this.currentSock=null)}beginPairing(t){if(this.busy)throw new Error("Pairing already in progress.");if(pi())throw new Error("Gateway appears to be running (gateway.pid). Stop `omnish run` or your service before pairing from the browser.");if(!t.force&&lt())throw new Error("WhatsApp session already linked. Use \u201CReplace session\u201D or run `omnish link --force` from the CLI.");se(),t.force&&(Ul.rmSync(le,{recursive:!0,force:!0}),Ul.mkdirSync(le,{recursive:!0,mode:448})),this.busy=!0,this.abort=new AbortController;let n=this.abort.signal;(async()=>{try{await gp({authDir:le,verbose:!1,printQr:!1,signal:n,onQr:o=>this.emit({type:"qr",payload:o}),onRestart515:()=>this.emit({type:"restart"}),onSocketReady:o=>{this.currentSock=o},onSocketClosed:()=>{this.currentSock=null}}),this.emit({type:"open"}),this.emit({type:"done",ok:!0})}catch(o){let r=o instanceof Error?o.message:String(o);r==="Pairing cancelled."?this.emit({type:"error",message:"Cancelled."}):this.emit({type:"error",message:r}),this.emit({type:"done",ok:!1})}finally{this.busy=!1,this.abort=null,this.currentSock=null}})()}},tr=new Bl;var ue="application/json; charset=utf-8",nr=null;function gv(){er();let e=nr;nr=null,e?e.close(()=>{pt.exit(0)}):pt.exit(0),setTimeout(()=>pt.exit(0),4e3).unref()}function yv(){let e=pt.env.OMNISH_CONFIG_UI_STATIC?.trim(),t=pt.env.OMNISH_UI_STATIC?.trim()||e;if(t&&ln.existsSync(_e.join(t,"index.html")))return t;let n=_e.dirname(hv(import.meta.url)),o=_e.join(n,"ui");if(ln.existsSync(_e.join(o,"index.html")))return o;let r=_e.join(n,"..","..","dist","ui");if(ln.existsSync(_e.join(r,"index.html")))return r;let s=_e.join(pt.cwd(),"dist","ui");if(ln.existsSync(_e.join(s,"index.html")))return s;throw new Error("omnish ui static files not found (expected dist/ui/index.html). Run `pnpm build`.")}function wv(e){let t=_e.extname(e).toLowerCase();return{".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".ico":"image/x-icon",".woff2":"font/woff2",".json":"application/json; charset=utf-8"}[t]??"application/octet-stream"}function bv(e,t){let o=decodeURIComponent(t.split("?")[0]??"").replace(/^\/+/,""),r=_e.normalize(_e.join(e,o));return!r.startsWith(_e.normalize(e+_e.sep))&&r!==_e.normalize(e)?null:r}async function Hl(e,t=512e3){let n=[],o=0;for await(let s of e){if(o+=s.length,o>t)throw new Error("Body too large.");n.push(s)}if(n.length===0)return;let r=Buffer.concat(n).toString("utf8");return JSON.parse(r)}function kv(e){return Vt.includes(e)}function jl(e){let t=(e.telegramBotToken??"").trim(),n=t.length===0?"":t.length<=8?"(set)":`${t.slice(0,4)}\u2026${t.slice(-4)}`;return{...e,telegramBotToken:n,telegramBotTokenConfigured:!!Me(e),telegramBotTokenEnvOverride:typeof pt.env.TELEGRAM_BOT_TOKEN=="string"&&pt.env.TELEGRAM_BOT_TOKEN.trim().length>0}}function vv(){let e=S();return{version:ot(),dataDir:W,configPath:_,waAuthDir:le,whatsappLinked:lt(),gatewayPidHint:ln.existsSync(_e.join(W,"gateway.pid")),gatewayRunning:pi(),gatewayLogFile:Ue,gatewayMode:e.gatewayMode,telegramBotTokenMasked:Me(e).length===0?"":jl(e).telegramBotToken,telegramBotTokenEnvOverride:typeof pt.env.TELEGRAM_BOT_TOKEN=="string"&&pt.env.TELEGRAM_BOT_TOKEN.trim().length>0}}function Sv(e,t){if(!Array.isArray(e)||!Array.isArray(t))throw new Error("allowFrom and telegramAllowFrom must be arrays of strings.");let n=e.map(a=>String(a).trim()).filter(Boolean),o=t.map(a=>String(a).trim()).filter(Boolean),r=ur(n).filter(a=>a!=="*"),s=[];for(let a of o){let l=Ne(a);if(!l)throw new Error(`Invalid Telegram allow entry: ${a}`);s.push(l)}let i=S();i.allowFrom=r.sort(),i.telegramAllowFrom=[...new Set(s)].sort(),Be(i)}function xv(e){return typeof e=="string"?e:typeof e=="boolean"||typeof e=="number"?String(e):JSON.stringify(e)}async function Ph(e){let t=yv(),{meta:n}=e;Mh(e.port);let o=mv.createServer(async(r,s)=>{try{let i=new URL(r.url??"/",`http://${r.headers.host??"localhost"}`),a=i.pathname;if(r.method==="GET"&&a==="/"){let m=i.searchParams.get("token")?.trim();if(m&&m===n.token){let h=_l(n.secret);s.statusCode=302,s.setHeader("Location","/"),s.setHeader("Set-Cookie",Wl(h)),s.end();return}}if(a.startsWith("/api/")){let m=r.headers.cookie,h=xh(n.secret,m);if(r.method==="POST"&&a==="/api/session"){let f=await Hl(r),g=typeof f?.token=="string"?f.token.trim():"";if(!g||g!==n.token){s.statusCode=401,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:"Invalid token."}));return}let y=_l(n.secret);s.statusCode=200,s.setHeader("Content-Type",ue),s.setHeader("Set-Cookie",Wl(y)),s.end(JSON.stringify({ok:!0}));return}if(r.method==="GET"&&a==="/api/me"){s.statusCode=h?200:401,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:h}));return}if(!h){s.statusCode=401,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:"Unauthorized."}));return}if(r.method==="GET"&&a==="/api/status"){s.statusCode=200,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!0,...vv()}));return}if(r.method==="GET"&&a==="/api/config"){s.statusCode=200,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!0,config:jl(S())}));return}if(r.method==="PUT"&&a==="/api/config"){let f=await Hl(r);if(!f||typeof f!="object"){s.statusCode=400,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:"Expected JSON object."}));return}let g=f;for(let[y,b]of Object.entries(g))if(b!==void 0&&!(y==="allowFrom"||y==="telegramAllowFrom")){if(y==="telegramBotToken"){let k=typeof b=="string"?b.trim():"";if(!k||k==="(set)"||k.includes("\u2026"))continue;if(!gt(k))throw new Error("Invalid Telegram bot token format.");Ir("telegramBotToken",k);continue}if(!kv(y))throw new Error(`Unknown or unsupported config key: ${y}`);Ir(y,xv(b))}if("allowFrom"in g||"telegramAllowFrom"in g){let y=S();Sv("allowFrom"in g?g.allowFrom:y.allowFrom,"telegramAllowFrom"in g?g.telegramAllowFrom:y.telegramAllowFrom)}s.statusCode=200,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!0,config:jl(S())}));return}if(r.method==="POST"&&a==="/api/logout"){s.statusCode=200,s.setHeader("Content-Type",ue),s.setHeader("Set-Cookie",Ch()),s.end(JSON.stringify({ok:!0}));return}if(r.method==="POST"&&a==="/api/shutdown"){s.statusCode=200,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!0})),setImmediate(()=>{tr.requestCancel(),gv()});return}if(r.method==="POST"&&a==="/api/gateway/start"){if(pi()){s.statusCode=409,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:"Gateway already appears to be running (pidfile + live process). Stop it first if you need to restart."}));return}let f=ri();if(!f.ok){s.statusCode=400,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:f.message}));return}let g=Ue,y=ks(g);if(!y.ok){s.statusCode=500,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:y.message}));return}s.statusCode=200,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!0,pid:y.pid,logFile:g}));return}if(r.method==="POST"&&a==="/api/gateway/stop"){let f=vs();switch(f.outcome){case"no_pidfile":s.statusCode=400,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:"No gateway pidfile \u2014 background gateway does not appear to be running."}));return;case"invalid_pidfile":s.statusCode=400,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:"Invalid pidfile (removed)."}));return;case"stale_cleaned":s.statusCode=200,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!0,stale:!0,pid:f.pid,message:"Process was not running; stale pidfile removed."}));return;case"sent_signal":s.statusCode=200,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!0,pid:f.pid}));return;case"taskkill_ok":s.statusCode=200,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!0,pid:f.pid,taskkill:!0}));return;case"failed":s.statusCode=500,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:f.message}));return}}if(r.method==="GET"&&a==="/api/wa/link/events"){s.writeHead(200,{"Content-Type":"text/event-stream; charset=utf-8","Cache-Control":"no-store",Connection:"keep-alive"}),s.write(`: connected
390
-
391
- `);let f=tr.subscribe(g=>{s.write(`data: ${JSON.stringify(g)}
392
-
393
- `)});r.on("close",()=>{f()});return}if(r.method==="POST"&&a==="/api/wa/link/start"){let g=(await Hl(r))?.force===!0;try{tr.beginPairing({force:g})}catch(y){let b=String(y),k=b.includes("already in progress")||b.includes("Gateway appears")?409:400;s.statusCode=k,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:b}));return}s.statusCode=202,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!0}));return}if(r.method==="POST"&&a==="/api/wa/link/cancel"){tr.requestCancel(),s.statusCode=200,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!0}));return}s.statusCode=404,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:"Not found."}));return}let l=a==="/"?"index.html":a.replace(/^\/+/,""),d=bv(t,l),u=_e.join(t,"index.html");d&&ln.existsSync(d)&&ln.statSync(d).isFile()&&(u=d);let c=ln.readFileSync(u);s.statusCode=200,s.setHeader("Content-Type",wv(u)),s.setHeader("Cache-Control",_e.basename(u)==="index.html"?"no-store":"public, max-age=3600"),s.end(c)}catch(i){s.statusCode=500,s.setHeader("Content-Type",ue),s.end(JSON.stringify({ok:!1,error:String(i)}))}});await new Promise((r,s)=>{o.once("error",s),o.listen(e.port,e.host,()=>r())}),nr=o,$h({pid:pt.pid,port:e.port,host:e.host,startedAt:new Date().toISOString()}),o.on("close",()=>{er(),nr===o&&(nr=null)})}function Eh(e){let t=fv.networkInterfaces(),n=[];for(let o of Object.values(t))if(o)for(let r of o)r.family==="IPv4"&&!r.internal&&n.push(`http://${r.address}:${e}/`);return[...new Set(n)].sort()}import Pv from"node:readline";import Ih from"node:path";import Oe from"node:process";pe();Xe();G();import Cv from"node:net";import Rv from"node:fs";function Tv(){try{let e=Rv.readFileSync(Mn,"utf8"),t=JSON.parse(e);return typeof t.host!="string"||typeof t.port!="number"||typeof t.token!="string"||!Number.isFinite(t.port)?null:{host:t.host,port:t.port,token:t.token}}catch{return null}}async function or(e){let t=Tv();if(!t)return"No gateway control endpoint \u2014 is `omnish run` active? (control metadata missing.)";let n={...e,token:t.token},o=`${JSON.stringify(n)}
394
- `;return new Promise(r=>{let s=!1,i="";function a(d){s||(s=!0,r(d))}let l=Cv.connect({host:t.host,port:t.port},()=>{l.write(o)});l.setTimeout(6e5),l.on("data",d=>{i+=d.toString("utf8");let u=i.indexOf(`
395
- `);if(u>=0){let c=i.slice(0,u).trim();try{let m=JSON.parse(c);m.ok?a(null):a(m.error||"Unknown error from gateway control.")}catch{a("Invalid response from gateway control.")}l.destroy()}}),l.on("error",d=>{a(`Control connection failed: ${String(d)}`)}),l.on("timeout",()=>{l.destroy(),a("Gateway control timed out.")}),l.on("close",()=>{!s&&i.trim()===""&&a("Gateway closed the control connection without a response.")})})}Xe();function Gl(e){return e.channel==="all"||e.channel==="whatsapp-all"||e.channel==="telegram-all"}function $v(e){let t=e.trimStart();if(/^--text=/i.test(t)){let n=t.slice(t.indexOf("=")+1).trimEnd();return n.length>0?n:null}if(/^--text(\s+|$)/i.test(t)){let n=t.replace(/^--text\s*/i,"").trim();return n.length>0?n:null}if(/^-t=/i.test(t)){let n=t.slice(t.indexOf("=")+1).trimEnd();return n.length>0?n:null}if(/^-t(\s+|$)/i.test(t)){let n=t.replace(/^-t\s*/i,"").trim();return n.length>0?n:null}}function Mv(e){let t=e.toLowerCase();if(t==="*"||t==="all")return{channel:"all"};if(t==="wa"||t==="whatsapp"||t==="wa:all"||t==="whatsapp:all")return{channel:"whatsapp-all"};if(t==="tg"||t==="telegram"||t==="tg:all"||t==="telegram:all")return{channel:"telegram-all"};if(t.startsWith("tg:")||t.startsWith("telegram:")){let n=Ne(e);if(!n)return null;let o=Number(n);return Number.isFinite(o)?{channel:"telegram",chatId:o}:null}if(t.startsWith("wa:")){let n=e.slice(3).trim();if(!n)return null;let o=n.split(",").map(r=>te(r.trim())).filter(r=>!!r);return o.length===0?null:{channel:"whatsapp",e164s:o}}if(e.includes(",")){let n=e.split(",").map(o=>te(o.trim())).filter(o=>!!o);return n.length===0?null:{channel:"whatsapp",e164s:n}}if(e.startsWith("+")){let n=te(e);return n?{channel:"whatsapp",e164s:[n]}:null}return null}function Ah(e){let t=e.trim();if(!/^\/sendto(\s|$)/i.test(t))return null;let n=t.replace(/^\/sendto\s+/i,"").trim();if(!n)return null;let o=n.indexOf(" ");if(o===-1)return null;let r=n.slice(0,o).trim(),s=n.slice(o+1).trim();if(!s)return null;let i=Mv(r);if(!i)return null;let a=$v(s);if(a===null)return null;if(a!==void 0)return{...i,mode:"text",body:a};let l=Ds(s);if(!l)return null;let{selectorPart:d,caption:u}=l;return{...i,mode:"media",selectorPart:d,caption:u}}function ql(){return["/sendto wa|tg|* <selectors> [-- caption]","/sendto +E164 or +E164,+E164 <selectors> [-- caption]","/sendto <dest> --text <message> or --text=<msg> or -t <msg>","/sendto tg:<chat_id> <selectors> [-- caption] (compat: single Telegram id)","selectors: file1,file2 or *.mp4 or **/*.mp4 (from current session cwd); a path ./--text sends a file named --text","examples: /sendto wa ./promo.mp4 | /sendto * **/*.mp4 -- Daily clips","examples: /sendto +15551234567 --text Hello | /sendto tg:123 -t=Status OK","examples: /sendto +15550000001,+15550000002 intro.png,deck.pdf -- Launch","Also supports compat aliases: wa:all, whatsapp:all, tg:all, telegram:all.","Requires `omnish run` on this machine."].join(`
396
- `)}jo();st();G();var ho="wa:cli:interactive",Ev={onPlainTextLlmFallback(e,t){Yn(S(),e,t,async n=>{n.trim()&&console.log(D(Oe.stdout,n))})}};function Av(e){let t=e.trim();if(!t)return null;let n=t.toLowerCase();if(n.startsWith("tg:")||n.startsWith("telegram:")){let s=Ne(t);return s?`tg:${s}`:null}let o=n.startsWith("wa:")?t.slice(3):t,r=te(o);return r?`wa:${r}`:null}function Iv(e){let t=null,n=null;for(let o=0;o<e.length;){let r=e[o]??"";if(r==="--as"){let s=e[o+1];if(!s||s.startsWith("-"))return{opts:{senderKey:null,oneShot:null},error:"--as requires a sender (wa:+E164 or tg:<id>)."};let i=Av(s);if(!i)return{opts:{senderKey:null,oneShot:null},error:`Could not parse sender "${s}". Use +E164, wa:+E164, or tg:<user_id>.`};t=i,o+=2;continue}if(r==="-c"||r==="--command"){let s=e[o+1];if(typeof s!="string")return{opts:{senderKey:null,oneShot:null},error:`${r} requires a command string.`};n=s,o+=2;continue}return r==="--help"||r==="-h"?{opts:{senderKey:null,oneShot:null},error:"help"}:{opts:{senderKey:null,oneShot:null},error:`Unknown argument: ${r}`}}return{opts:{senderKey:t,oneShot:n},error:null}}function Jl(e){let t=Oe.cwd(),n=[`${me(e,"omnish i")} ${w(e,"[options]")}`,Q(e,"Interactive shell \u2014 same commands as WhatsApp/Telegram chat."),"",q(e,"Usage:"),` ${v(e,"omnish i [options]")}`,` ${v(e,"omnish interactive [options]")}`,"",q(e,"Options:"),...$t(e," ",[{left:"--as <sender>",right:"Sender key for cluster commands (wa:+E164 or tg:id). Default: synthetic wa:cli:interactive."},{left:"-c, --command <line>",right:"Run one line and exit (non-interactive)."},{left:"-h, --help",right:"Show this help."}],o=>w(e,o)),"",`${w(e,"Trust:")} ${v(e,"Full local access like your shell; not gated by inbox allowlist.")}`,`${w(e,"Jobs:")} ${v(e,"/bg and /jobs apply only to this REPL session (not the gateway process).")}`,`${w(e,"Files:")} ${v(e,"Use /sendto for files or --text for plain messages through the gateway; plain /send needs a chat peer.")}`,`${w(e,"Gateway:")} ${v(e,"/reload and /updates require omnish run; /sendto requires omnish run for WA/TG delivery.")}`,"",ql(),"",`${w(e,"cwd:")} ${v(e,`session starts at ${t} (change with !cd or ${S().commandPrefix}cd).`)}`];console.log(n.join(`
397
- `))}function Lh(e,t){let n=new Set,o=new Set;if(e.channel==="whatsapp-all"||e.channel==="all"){let r=new Set;for(let s of t.allowFrom){let i=te(String(s));i&&!r.has(i)&&(r.add(i),n.add(i))}}if(e.channel==="telegram-all"||e.channel==="all")for(let r of t.telegramAllowFrom){let s=Ne(String(r));if(!s)continue;let i=Number(s);Number.isFinite(i)&&o.add(i)}if(e.channel==="whatsapp")for(let r of e.e164s)n.add(r);return e.channel==="telegram"&&o.add(e.chatId),{waTargets:n,tgTargets:o}}function Oh(e){let t="No recipients matched the requested /sendto destination.";return Gl(e)?ae()?`${t} Check platform dashboard allowlists (or omnish platform status), or use explicit +E164 / tg:<id>.`:`${t} Set allowFrom / telegramAllowFrom in config.json, or use explicit +E164 / tg:<id>.`:t}async function Lv(e){let t=Gl(e)?await Qa():S();if(e.mode==="text"){let{waTargets:c,tgTargets:m}=Lh(e,t);if(c.size===0&&m.size===0)return{error:Oh(e)};let h=[];for(let f of c){let g=await or({op:"sendText",channel:"whatsapp",e164:f,text:e.body});g&&h.push(`[wa:${f}] ${g}`)}for(let f of m){let g=await or({op:"sendText",channel:"telegram",chatId:f,text:e.body});g&&h.push(`[tg:${f}] ${g}`)}return h.length>0?{error:h.join(`
398
- `)}:{error:null,kind:"text",recipientsSent:c.size+m.size}}let n=ie(ho).cwd,o=await Jo(n,e.selectorPart);if(o.length===0)return{error:`No files matched: ${e.selectorPart}`};let r=await zo(o);if(!r.ok)return{error:r.error};let s=o.map(c=>St(c,t.fileSendMaxBytes));for(let c of s)if("error"in c)return{error:c.error};let{waTargets:i,tgTargets:a}=Lh(e,t);if(i.size===0&&a.size===0)return{error:Oh(e)};let l=[];for(let c of i)for(let m of s){if("error"in m)continue;let h=await or({op:"sendMedia",channel:"whatsapp",e164:c,absPath:m.absPath,caption:e.caption});h&&l.push(`[wa:${c}] ${m.displayName}: ${h}`)}for(let c of a)for(let m of s){if("error"in m)continue;let h=await or({op:"sendMedia",channel:"telegram",chatId:c,absPath:m.absPath,caption:e.caption});h&&l.push(`[tg:${c}] ${m.displayName}: ${h}`)}if(l.length>0)return{error:l.join(`
399
- `)};let d=i.size+a.size,u=s.length;return{error:null,kind:"media",recipientsSent:d,filesSent:u,messagesSent:d*u}}async function Ov(e,t,n,o,r,s,i){let a=e.trim();if(!a)return;let l=Ah(a);if(l!==null||/^\/sendto(\s|$)/i.test(a)){if(l===null){console.log(T(Oe.stderr,`Invalid /sendto.
400
- `+ql()));return}let c=await Lv(l);if(c.error!==null)console.log(T(Oe.stderr,c.error));else if(c.kind==="text")console.log(D(Oe.stdout,`Sent text to ${c.recipientsSent} recipient(s).`));else{let m=`Sent ${c.filesSent} file(s) to ${c.recipientsSent} recipient(s) (${c.messagesSent} message(s)).`;console.log(D(Oe.stdout,m))}return}let d={peerKey:ho,text:e},u=await an(S(),t,n,o,r,d,s,void 0,i,!1,Ev);u!==null&&await Nv(u)}async function Nv(e){if(e===null)return;if(e.kind==="file"||e.kind==="files"){console.log(D(Oe.stdout,["This CLI session has no chat peer to attach to.","Push through the gateway instead, for example:"," /sendto wa:+15551234567 ./my.pdf"," /sendto tg:123456789 ~/photo.jpg -- optional caption","(Requires `omnish run` on this machine.)"].join(`
401
- `)));return}if(e.kind==="texts"){for(let n=0;n<e.bodies.length;n++){let o=Te(e.bodies[n],"whatsapp").text;o.trim()&&(n>0&&console.log(""),console.log(o))}return}let t=Te(e.body,"whatsapp").text;t.trim()&&console.log(t)}async function Nh(e){let t=Iv(e);if(t.error==="help"){Jl(Oe.stdout);return}if(t.error&&t.error!==null){console.error(T(Oe.stderr,t.error)),console.error(w(Oe.stderr,"Try: omnish i --help")),Oe.exitCode=1;return}let{senderKey:n,oneShot:o}=t.opts,r=n??ho;se(),Rr(ho,Oe.cwd());let s=new Ft,i=new Map,a=new Map,l=new Map,d=new en(()=>S(),async(h,f)=>{Oe.stdout.write(f),f.endsWith(`
402
- `)||Oe.stdout.write(`
403
- `)}),u=async h=>{try{await Ov(h,s,i,a,l,d,r)}catch(f){console.error(T(Oe.stderr,String(f)))}};if(o!==null){await u(o),d.dispose(),s.killAllRunning();return}let c=Pv.createInterface({input:Oe.stdin,output:Oe.stdout}),m=Ih.basename(ie(ho).cwd);c.setPrompt(`${m}> `),c.on("line",h=>{u(h).then(()=>{let f=Ih.basename(ie(ho).cwd);c.setPrompt(`${f}> `),c.prompt()})}),c.on("close",()=>{d.dispose(),s.killAllRunning(),Oe.stdout.write(`
404
- `)}),c.prompt()}function zl(){console.log(`omnish docs \u2014 search bundled documentation (offline)
405
+ `)}`}:{ok:!0}}Ve();q();Ve();import $i from"node:fs";import Mi from"node:path";pe();import{spawn as Zv}from"node:child_process";import Dh from"node:fs";import{stdout as eS}from"node:process";Ve();q();bn();var ec=["tunnelRelayUrl","platformToken","platformDeviceId"],Uh=[{label:"Platform (attached gateway + tunnel)",keys:["tunnelRelayUrl","platformToken","platformDeviceId"]},{label:"Tunnel",keys:["tunnelEnabled","tunnelMaxActive"]},{label:"Gateway",keys:["gatewayMode","commandPrefix","shell"]},{label:"Cluster",keys:["clusterEnabled","clusterRole","clusterLabel","clusterSenderBindings"]},{label:"Telegram",keys:["telegramBotToken"]},{label:"Apps",keys:["appsCols","appsRows","appsFlushMs","appsMinIntervalMs","appsMaxFlushBytes","appsMaxSessions","appsMaxSessionsTotal","appsMaxWaChars","appsLogTailLines","appsSubmitDelayMs","appsClearInput","appsClearInputDelayMs","appsClearInputSequence","appsSkipClearOnPasswordPrompt","appsPasswordPromptHint"]},{label:"Files",keys:["fileSendMaxBytes","fileReceiveMaxBytes","fileInboxSubdir","fileReceiveRootMode","fileReceiveRootPath"]},{label:"Recipes",keys:["recipesAllowDangerousBuiltins","recipesMaxTaskChars","recipesMacroDefaultCommand","recipesRunAttach"]},{label:"Sync / jobs",keys:["syncTimeoutMs","syncMaxBytes","jobLogTailLines"]},{label:"Service / updates",keys:["serviceInstallFromChat","updateCheckEnabled","updateCheckIntervalMs","updateCheckPackageName","updateInfoUrl"]},{label:"Chat LLM fallback",keys:["chatLlmFallbackEnabled","chatLlmShellCommand","chatLlmTimeoutMs","chatLlmMaxInputChars","chatLlmMaxOutputChars","chatLlmNeedsTty","chatLlmWorkDir"]}];function Ri(e){let t=[z(e,"omnish config"),w(e,"Manage ~/.omnish/config.json (env vars still override for platform credentials)."),"",z(e,"Usage:"),` ${v(e,"omnish config add <key> <value> [key value ...]")}`,` ${v(e,"omnish config show <key>[,<key>|*]")}`,` ${v(e,"omnish config edit")}`,` ${v(e,"omnish config edit <key> <value>")}`,` ${v(e,"omnish config delete <key>[,<key>]")}`,"",w(e,"Platform aliases: platform_url \u2192 tunnelRelayUrl, platform_token \u2192 platformToken,"),w(e," platform_device_id \u2192 platformDeviceId"),` ${v(e,"omnish config show platform")} ${w(e,"\u2014 attached-mode diagnostics (URL, token, devices)")}`,w(e," Full setup: omnish help platform"),w(e,"add sets/overwrites scalar keys (not array append). show * lists all keys."),""];console.log(t.join(`
406
+ `))}function Bh(e){return e.split(/[,\s]+/).map(t=>t.trim()).filter(Boolean)}function tc(e){if(e.length===0)return[];let t=e.join(" ");if(t.includes(",")){let n=[];for(let o of t.split(",")){let r=o.trim();if(!r)continue;let s=r.split(/\s+/);if(s.length<2)throw new Error(`Invalid pair "${r}" \u2014 expected "key value"`);n.push({key:s[0],value:s.slice(1).join(" ")})}return n}if(e.length===2)return[{key:e[0],value:e[1]}];if(e.length%2===0){let n=[];for(let o=0;o<e.length;o+=2)n.push({key:e[o],value:e[o+1]});return n}return[{key:e[0],value:e.slice(1).join(" ")}]}function Ci(e,t){if(e==="clusterSenderBindings")return JSON.stringify(t.clusterSenderBindings);let n=String(t[e]??"");return e==="platformToken"||e==="telegramBotToken"?vn(e,n):n}function Hh(e){return e==="tunnelRelayUrl"?{value:jo(),source:to()}:e==="platformToken"?{value:vn(e,Ea()),source:eo()}:e==="platformDeviceId"?{value:(Aa()??"")||"(empty)",source:Ia()}:null}function tS(e){if(e!=="platformToken"&&e!=="tunnelRelayUrl")return;let n=S().platformToken.trim()||It()?.token?.trim()||"";n&&vt({token:n,relayUrl:jo()})}async function nS(){let e=process.env.VISUAL?.trim()||process.env.EDITOR?.trim()||(process.platform==="win32"?"notepad":"nano"),t=Dh.readFileSync(_,"utf8");await new Promise((n,o)=>{let r=Zv(e,[_],{stdio:"inherit"});r.on("error",o),r.on("exit",s=>{s===0?n():o(new Error(`Editor exited with code ${s??1}`))})});try{S()}catch(n){throw Dh.writeFileSync(_,t,{mode:384}),new Error(`Invalid config after edit: ${n instanceof Error?n.message:String(n)}`)}}async function jh(e){let t=eS,n=process.stderr,o=(e[0]??"").trim().toLowerCase(),r=e.slice(1);if(!o||o==="help"||o==="--help"||o==="-h"){Ri(t);return}if(o==="add"||o==="edit"){try{if(o==="edit"&&r.length===0){await nS(),console.log(U(t,`Updated ${_}`));return}let s=tc(r);if(s.length===0){console.error(C(n,"Expected at least one key/value pair.")),process.exitCode=1;return}for(let{key:i,value:a}of s){let l=Gr(i);if(!l){console.error(C(n,`Unknown key "${i}". Try: ${ld().slice(0,8).join(", ")}\u2026 (omnish config show *)`)),process.exitCode=1;return}Qn(l,a),tS(l);let c=S(),d=l==="tunnelRelayUrl"?c.tunnelRelayUrl:ec.includes(l)?vn(l,String(c[l]??"")):Ci(l,c);console.log(U(t,`${i} \u2192 ${d}`))}console.log(U(t,_))}catch(s){console.error(C(n,s instanceof Error?s.message:String(s))),process.exitCode=1}return}if(o==="show"){let s=r.join(" ").trim()||"*";if(s==="*"){let l=S(),c=[z(t,"Config"),w(t,_),""];for(let m of Uh){c.push(z(t,m.label));for(let h of m.keys){let f=Ci(h,l),g=Hh(h);g&&ec.includes(h)?c.push(` ${v(t,h)}: ${f} ${w(t,`(effective: ${g.value}, source: ${g.source})`)}`):c.push(` ${v(t,h)}: ${f}`)}c.push("")}let d=new Set(Uh.flatMap(m=>m.keys)),u=ha.filter(m=>!d.has(m));if(u.length>0){c.push(z(t,"Other"));for(let m of u)c.push(` ${v(t,m)}: ${Ci(m,l)}`)}console.log(c.join(`
407
+ `));return}let i=Bh(s),a=S();for(let l of i){let c=Gr(l);if(!c){console.error(C(n,`Unknown key "${l}".`)),process.exitCode=1;return}let d=Ci(c,a),u=Hh(c);u&&ec.includes(c)?console.log(`${l}: ${d} (effective: ${u.value}, source: ${u.source})`):console.log(`${l}: ${d}`)}return}if(o==="delete"){let s=r.join(" ").trim();if(!s){console.error(C(n,"Expected at least one key to delete.")),process.exitCode=1;return}if(s==="*"){console.error(C(n,'Refusing "delete *". List keys explicitly.')),process.exitCode=1;return}try{for(let i of Bh(s)){let a=Gr(i);if(!a){console.error(C(n,`Unknown key "${i}".`)),process.exitCode=1;return}cd(a),console.log(U(t,`cleared ${i}`))}console.log(U(t,_))}catch(i){console.error(C(n,i instanceof Error?i.message:String(i))),process.exitCode=1}return}console.error(C(n,`Unknown subcommand "${o}".`)),Ri(t),process.exitCode=1}async function Gh(e){let t=re(),n=[];if(n.push(z(e,"platform")),!t){n.push(` ${w(e,"attached:")} ${ke(e,"no")} \u2014 set platform_url + platform_token (omnish config add) or env`);let a=to(),l=eo();return(a==="env"||l==="env")&&n.push(` ${w(e,"note:")} ${X(e,"partial env override (need URL + token)")}`),n}let o=to(),r=eo(),s=Ia(),i=o==="env"||r==="env"||s==="env"?` ${w(e,"(env overrides config)")}`:"";n.push(` ${w(e,"attached:")} ${v(e,"yes")}${i}`),n.push(` ${w(e,"url:")} ${v(e,t.platformUrl)} ${X(e,`[${o}]`)}`),n.push(` ${w(e,"token:")} ${v(e,vn("platformToken",t.token))} ${X(e,`[${r}]`)}`),t.deviceId&&n.push(` ${w(e,"device:")} ${v(e,t.deviceId)} ${X(e,`[${s}]`)}`);try{let{fetchPlatformAccount:a}=await Promise.resolve().then(()=>(tr(),lm)),l=await a(t);n.push(` ${w(e,"gatewayMode:")} ${v(e,l.gatewayMode)} ${X(e,"(platform)")}`);let c=u=>{let m=l.connectors[u];return m?m.linked?v(e,"linked"):X(e,m.status):ke(e,"idle")};n.push(` ${w(e,"whatsapp:")} ${c("whatsapp")}`),n.push(` ${w(e,"telegram:")} ${c("telegram")}`);let d=l.routing.onlineCount;n.push(` ${w(e,"routing:")} default=${l.routing.defaultDeviceId??X(e,"(none)")} online=${d}`),t.deviceId&&l.routing.defaultDeviceId&&t.deviceId!==l.routing.defaultDeviceId&&n.push(` ${w(e,"warn:")} ${ke(e,"platform_device_id \u2260 platform defaultDeviceId \u2014 run omnish run or set default on dashboard")}`),d===0&&n.push(` ${w(e,"warn:")} ${ke(e,"no device online on platform")}`)}catch{n.push(` ${w(e,"account:")} ${X(e,"(could not fetch /v1/me \u2014 check token and URL)")}`)}return n}tr();Ve();function qh(){let e=re();return e||(console.error(C(process.stderr,"Set platform_url + platform_token (omnish config add) or OMNISH_PLATFORM_URL + OMNISH_TOKEN.")),process.exitCode=1,null)}function oS(e){let t=e.trim();return t?t.startsWith("wa:")?{kind:"wa",value:t.slice(3).trim()}:t.startsWith("tg:")?{kind:"tg",value:t.slice(3).trim()}:/^\+?\d{8,}$/.test(t.replace(/\s/g,""))?{kind:"wa",value:t}:/^\d+$/.test(t)?{kind:"tg",value:t}:null:null}function Jh(e){return e.replace(/\D/g,"")}async function zh(){let e=qh();if(!e)return;let t=await In(e),n=t.connectors.whatsapp,o=t.connectors.telegram;console.log(U(process.stdout,"Platform account")),console.log(` gatewayMode: ${t.gatewayMode}`),console.log(` whatsapp: ${n?.linked?"linked":n?.status??"idle"} (allowlist: ${t.allowFrom.length})`);let r=o?.tokenConfigured?"token saved":"no token";console.log(` telegram: ${o?.linked?"linked":o?.status??"idle"} (${r}, allowlist: ${t.telegramAllowFrom.length})`),console.log(` defaultDeviceId: ${t.defaultDeviceId??"\u2014"}`),console.log(` online devices: ${t.routing.onlineCount}`),t.allowFrom.length&&console.log(` allowFrom: ${t.allowFrom.map(s=>`+${s}`).join(", ")}`),t.telegramAllowFrom.length&&console.log(` telegramAllowFrom: ${t.telegramAllowFrom.join(", ")}`)}async function Kh(e){let t=qh();if(!t)return;let n=(e[0]??"").toLowerCase();if(!n||n==="-h"||n==="--help"){console.log(["Usage:"," omnish platform allow list"," omnish platform allow add wa:+15551234567 tg:123456789"," omnish platform allow set --wa +1555,... --tg 123,...",""].join(`
408
+ `));return}if(n==="list"){let o=await In(t);console.log(U(process.stdout,"Platform allowlists")),console.log(` WhatsApp (${o.allowFrom.length}): ${o.allowFrom.length?o.allowFrom.map(r=>`+${r}`).join(", "):"(empty \u2014 any sender)"}`),console.log(` Telegram (${o.telegramAllowFrom.length}): ${o.telegramAllowFrom.length?o.telegramAllowFrom.join(", "):"(empty \u2014 any sender)"}`);return}if(n==="add"){let o=await In(t),r=[...o.allowFrom],s=[...o.telegramAllowFrom];for(let a of e.slice(1)){let l=oS(a);if(!l){console.error(C(process.stderr,`Unrecognized entry: ${a}`)),process.exitCode=1;return}if(l.kind==="wa"){let c=Jh(l.value);c&&!r.includes(c)&&r.push(c)}else{let c=l.value.replace(/\D/g,"");c&&!s.includes(c)&&s.push(c)}}let i=await Gs(t,{allowFrom:r,telegramAllowFrom:s});console.log(U(process.stdout,`Allowlists updated (${i.allowFrom.length} WhatsApp, ${i.telegramAllowFrom.length} Telegram).`));return}if(n==="set"){let o,r;for(let a=1;a<e.length;a++){let l=e[a];l==="--wa"&&e[a+1]?o=e[++a].split(",").map(c=>Jh(c.trim())).filter(Boolean):l==="--tg"&&e[a+1]&&(r=e[++a].split(",").map(c=>c.trim().replace(/^tg:/i,"").replace(/\D/g,"")).filter(Boolean))}if(o===void 0&&r===void 0){console.error(C(process.stderr,"Use --wa and/or --tg with comma-separated values.")),process.exitCode=1;return}let s={};o!==void 0&&(s.allowFrom=o),r!==void 0&&(s.telegramAllowFrom=r);let i=await Gs(t,s);console.log(U(process.stdout,`Allowlists set (${i.allowFrom.length} WhatsApp, ${i.telegramAllowFrom.length} Telegram).`));return}console.error(C(process.stderr,`Unknown allow subcommand: ${e[0]}`)),process.exitCode=1}async function Yh(e){let t="",n="",o="";for(let r=0;r<e.length;r++){let s=e[r];(s==="--url"||s==="-u")&&e[r+1]?t=e[++r].trim():(s==="--token"||s==="-t")&&e[r+1]?n=e[++r].trim():s==="--device-id"&&e[r+1]&&(o=e[++r].trim())}if(!t||!n){let r=tc(e);for(let s of r)s.key==="platform_url"&&(t=s.value),s.key==="platform_token"&&(n=s.value),s.key==="platform_device_id"&&(o=s.value)}if(!t||!n){console.error(C(process.stderr,"Usage: omnish platform login --url <url> --token <token> [--device-id <id>]")),process.exitCode=1;return}Qn("tunnelRelayUrl",t.replace(/\/$/,"")),Qn("platformToken",n),o&&Qn("platformDeviceId",o),console.log(U(process.stdout,"Platform credentials saved to config.json.")),console.log(" Run: omnish platform status && omnish run")}import rS from"qrcode-terminal";Ve();var sS=1e3,iS=120;function Qh(){let e=re();return e||(console.error(C(process.stderr,"Set platform_url + platform_token (omnish config add) or OMNISH_PLATFORM_URL + OMNISH_TOKEN.")),process.exitCode=1,null)}function oc(e){let t=e.replace(/\/$/,"");return/^https?:\/\//i.test(t)?t:`http://${t}`}async function Vh(e){let t=`${oc(e.platformUrl)}/v1/connectors/whatsapp/status`,n=await fetch(t,{headers:{Authorization:`Bearer ${e.token}`}}),o=await n.json().catch(()=>({}));if(!n.ok)throw new Error(o.error||`HTTP ${n.status}`);return o}async function aS(e){let t=await fetch(`${oc(e.platformUrl)}/v1/connectors/whatsapp/start`,{method:"POST",headers:{"content-type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify({})}),n=await t.json().catch(()=>({}));if(!t.ok)throw new Error(n.error||`HTTP ${t.status}`);return n}function Xh(e){let t=process.stdout,n=w(t,"\xB7".repeat(42));console.log(X(t,"Scan with WhatsApp \u2192 Linked devices")),console.log(n),rS.generate(e,{small:!0}),console.log(n)}var Ti="",nc=!1;function lS(e){let t=e.status??"";return t==="qr"||t==="connecting"||t==="reconnecting"||t==="pairing_restart"}async function cS(e){for(let t=0;t<iS;t++){let n=await Vh(e);if(n.status==="linked"||n.linked)return n;if(n.status==="pairing_restart"&&!nc&&(nc=!0,console.log(w(process.stdout,"Finishing WhatsApp link after scan (server restart) \u2014 wait a few seconds\u2026"))),n.qr&&n.qr!==Ti&&(Ti=n.qr,Xh(n.qr)),!lS(n))throw new Error(n.error||n.statusMessage||`Unexpected status: ${n.status}`);await new Promise(o=>setTimeout(o,sS))}throw new Error("Timed out waiting for WhatsApp to link (scan the QR within ~2 minutes).")}async function Zh(){let e=Qh();if(!e)return;let t=await Vh(e);if(t.status==="linked"||t.linked){console.log(U(process.stdout,"WhatsApp already linked on the platform.")),t.linkedPhoneE164&&(console.log(` Phone: +${t.linkedPhoneE164}`),console.log(` Hint: omnish platform allow add wa:+${t.linkedPhoneE164}`));return}console.log(U(process.stdout,"Starting WhatsApp pairing on the platform\u2026")),nc=!1,Ti="",t=await aS(e),t.qr&&(Xh(t.qr),Ti=t.qr),t=await cS(e),console.log(U(process.stdout,"WhatsApp linked on the platform.")),t.linkedPhoneE164&&(console.log(` Phone: +${t.linkedPhoneE164}`),console.log(` Next: omnish platform allow add wa:+${t.linkedPhoneE164}`),console.log(" Then: omnish run"))}async function ef(){let e=Qh();if(!e)return;let t=await fetch(`${oc(e.platformUrl)}/v1/connectors/whatsapp/unlink`,{method:"POST",headers:{"content-type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify({})}),n=await t.json().catch(()=>({}));if(!t.ok){console.error(C(process.stderr,n.error||`HTTP ${t.status}`)),process.exitCode=1;return}console.log(U(process.stdout,`WhatsApp unlinked (status: ${n.status??"idle"}).`))}import uS from"ws";function tf(e,t,n){return new Promise(o=>{let r=new uS(e,{headers:{Authorization:`Bearer ${t}`}}),s=setTimeout(()=>{r.terminate(),o({ok:!1,message:"timeout"})},n);r.once("open",()=>{clearTimeout(s),r.close(),o({ok:!0})}),r.once("unexpected-response",(i,a)=>{clearTimeout(s),o({ok:!1,status:a.statusCode,message:`HTTP ${a.statusCode} ${a.statusMessage}`})}),r.once("error",i=>{clearTimeout(s),o({ok:!1,message:String(i?.message??i)})})})}async function nf(e,t,n=12e3){let o=e.replace(/\/$/,""),r=bo(o),s=await tf(r,t,n),i,a,l="";for(let c of qs(o)){let d=await tf(c,t,n);if(d.ok)return i=new URL(c).pathname,{ok:!0,controlWsOk:s.ok,deviceWsOk:!0,deviceWsPath:i};a=d.status,l=d.message}return a===400?{ok:!1,controlWsOk:s.ok,deviceWsOk:!1,deviceWsStatus:400,error:"Device WebSocket returned HTTP 400 on /platform/device and could not use /control/device. Redeploy the latest relay image (adds /control/device), or route /platform/* to port 8788 in Caddy/EasyPanel."}:{ok:!1,controlWsOk:s.ok,deviceWsOk:!1,deviceWsStatus:a,error:`Device WebSocket failed: ${l}`}}var dS=400,pS=8*1024*1024,mS=2*1024*1024;function hS(e){let t=ce,n=!1,o=!1;for(let r=0;r<e.length;r++){let s=e[r];s==="-h"||s==="--help"?o=!0:s==="--force"?n=!0:(s==="--auth-dir"||s==="--authDir")&&e[r+1]&&(t=Mi.resolve(e[++r]))}return{authDir:t,force:n,help:o}}function fS(e){let t={},n=0,o=0;function r(s,i){let a;try{a=$i.readdirSync(i,{withFileTypes:!0})}catch{return}for(let l of a){let c=l.name;if(c==="."||c==="..")continue;let d=Mi.join(i,c),m=(s?`${s}/${c}`:c).split(Mi.sep).join("/");if(!l.isSymbolicLink()){if(l.isDirectory())r(m,d);else if(l.isFile()){let h=$i.readFileSync(d);if(h.length>mS)throw new Error(`file too large: ${m}`);if(n+=h.length,n>pS)throw new Error("total auth size exceeds limit (8 MiB)");if(o+=1,o>dS)throw new Error("too many files (max 400)");t[m]=h.toString("base64")}}}}return r("",e),t}function Pi(e){console.log([ye(e,"omnish platform"),w(e,"Attached mode: messengers on the hosted platform; shell on this machine (omnish run)."),"",z(e,"When to use"),w(e," Link WhatsApp/Telegram once on the platform dashboard. Run omnish run on laptops, servers, or containers without local Baileys."),"",z(e,"Prerequisites"),w(e," 1. Platform account token (signup/login on relay dashboard)."),w(e," 2. Messengers linked on the platform (dashboard QR or import-whatsapp)."),w(e," 3. allowFrom / telegramAllowFrom on dashboard Allowlists (Telegram: DM bot /id for your user id)."),"",z(e,"Configure"),` ${v(e,"omnish config add platform_url <url> platform_token <token>")}`,w(e," Optional: platform_device_id <id>"),w(e," Env overrides: OMNISH_PLATFORM_URL, OMNISH_TOKEN, OMNISH_DEVICE_ID"),w(e," (aliases: OMNISH_COMM_LAYER_URL, OMNISH_DEVICE_TOKEN, OMNISH_TUNNEL_*)"),"",z(e,"Workflow"),` ${v(e,"omnish platform login --url <url> --token <t>")} ${w(e,"\u2014 save credentials (or config add)")}`,` ${v(e,"omnish platform status")} ${w(e,"\u2014 account, connectors, allowlists")}`,` ${v(e,"omnish platform allow add tg:<id>")} ${w(e,"\u2014 or dashboard + bot /id")}`,` ${v(e,"omnish platform probe")} ${w(e,"\u2014 test WebSocket routing")}`,` ${v(e,"omnish run")} ${w(e,"\u2014 attach device; expect 'omnish attached to platform'")}`,"",z(e,"Subcommands"),v(e,"omnish platform login --url <url> --token <token> [--device-id <id>]"),w(e," Save platform URL and token to config.json."),"",v(e,"omnish platform status"),w(e," Show gatewayMode, connector status, allowlists, online devices."),"",v(e,"omnish platform allow list|add|set"),w(e," Manage platform allowlists (PUT /v1/me/allowlists)."),"",v(e,"omnish platform probe"),w(e," Test control-plane WebSocket routing (diagnose attached omnish run 400 errors)."),"",v(e,"omnish platform link-whatsapp"),w(e," Pair WhatsApp on the platform (terminal QR + poll until linked)."),"",v(e,"omnish platform unlink-whatsapp"),w(e," Remove platform WhatsApp session and auth files."),"",v(e,"omnish platform import-whatsapp [--auth-dir <path>] [--force]"),w(e," Upload local Baileys auth (after omnish link on this host). Use link-whatsapp or dashboard QR when possible."),w(e," Stop local omnish run before import to avoid WhatsApp session conflicts."),"",w(e,"Standalone omnish link on this host is not used for platform WhatsApp when attached."),w(e,"Docs: docs/guides/platform-reference.md (full API/CLI)"),w(e," docs/guides/platform-attached-mode.md (walkthrough)"),""].join(`
409
+ `))}async function gS(e){let{authDir:t,force:n,help:o}=hS(e);if(o){console.log(["Usage: omnish platform import-whatsapp [--auth-dir <path>] [--force]","","Requires OMNISH_PLATFORM_URL and OMNISH_TOKEN.","Default auth directory: ~/.omnish/auth (Baileys useMultiFileAuthState).","Stop `omnish run` on this machine before importing to avoid WhatsApp session conflicts.",""].join(`
410
+ `));return}let r=re();if(!r){console.error(C(process.stderr,"Set OMNISH_PLATFORM_URL (control plane URL) and OMNISH_TOKEN (from dashboard signup/login).")),process.exitCode=1;return}if(!$i.existsSync(t)){console.error(C(process.stderr,`Auth directory not found: ${t}`)),process.exitCode=1;return}ie();let s=Mi.join(t,"creds.json");if(!$i.existsSync(s)){console.error(C(process.stderr,`No creds.json in ${t} \u2014 link WhatsApp locally first (omnish link).`)),process.exitCode=1;return}let i;try{i=fS(t)}catch(h){console.error(C(process.stderr,String(h))),process.exitCode=1;return}if(Object.keys(i).length===0){console.error(C(process.stderr,"No files collected (empty auth directory?).")),process.exitCode=1;return}let a=r.platformUrl.replace(/\/$/,""),c=`${/^https?:\/\//i.test(a)?a:`http://${a}`}/v1/connectors/whatsapp/import`,d=await fetch(c,{method:"POST",headers:{"content-type":"application/json",Authorization:`Bearer ${r.token}`},body:JSON.stringify({files:i,force:n})}),u=await d.json().catch(()=>({}));if(!d.ok){console.error(C(process.stderr,u.error||`HTTP ${d.status}`)),process.exitCode=1;return}let m=u.qr?" QR emitted \u2014 open the platform dashboard if you need to scan.":"";console.log(U(process.stdout,`Uploaded Baileys auth. Platform connector status: ${u.status??"unknown"}.${m}`))}async function yS(){let e=process.stdout,t=process.stderr,n=re();if(!n){console.error(C(t,"Set platform_url + platform_token (omnish config add) or OMNISH_PLATFORM_URL + OMNISH_TOKEN.")),process.exitCode=1;return}console.log(`${ye(e,"Platform probe")} ${w(e,n.platformUrl)}`);let o=await nf(n.platformUrl,n.token);console.log(` ${w(e,"wss /control:")} ${o.controlWsOk?v(e,"ok"):ke(e,"fail")}`);for(let r of["/platform/device","/control/device"]){if(o.ok&&o.deviceWsPath&&o.deviceWsPath!==r){console.log(` ${w(e,`wss ${r}:`)} ${X(e,"skipped (fallback used)")}`);continue}if(o.ok&&o.deviceWsPath===r){console.log(` ${w(e,`wss ${r}:`)} ${v(e,"ok")}`);continue}!o.ok&&r==="/platform/device"&&console.log(` ${w(e,`wss ${r}:`)} ${ke(e,"fail")}${o.deviceWsStatus?` (HTTP ${o.deviceWsStatus})`:""}`),!o.ok&&r==="/control/device"&&console.log(` ${w(e,`wss ${r}:`)} ${ke(e,"fail")}`)}if(o.ok){o.deviceWsPath==="/control/device"&&console.log(w(e," Using /control/device fallback (/platform/* not on control port \u2014 redeploy Caddy when convenient).")),console.log(U(e,"Attached omnish run should be able to connect."));return}console.error(C(t,o.error??"Platform probe failed.")),o.controlWsOk&&!o.deviceWsOk&&console.error(C(t,"Fix: redeploy the latest tunnel-relay image (enables wss /control/device), update omnish CLI, then retry. Also route /platform/* to 8788 when you can (Caddyfile.standalone).")),process.exitCode=1}async function of(e){let t=(e[0]??"").toLowerCase();if(!t||t==="-h"||t==="--help"){Pi(process.stdout);return}if(t==="login"){await Yh(e.slice(1));return}if(t==="status"){await zh();return}if(t==="allow"){await Kh(e.slice(1));return}if(t==="probe"){await yS();return}if(t==="link-whatsapp"){await Zh();return}if(t==="unlink-whatsapp"){await ef();return}if(t==="import-whatsapp"){await gS(e.slice(1));return}console.error(C(process.stderr,`Unknown platform subcommand: ${e[0]}`)),Pi(process.stderr),process.exitCode=1}q();import af from"node:crypto";import Wn from"node:fs";import lf from"node:path";function wS(){return af.randomBytes(24).toString("hex")}function rf(){return af.randomBytes(32).toString("hex")}function sf(e){try{let t=Wn.readFileSync(e,"utf8"),n=JSON.parse(t);if(typeof n.token=="string"&&n.token.length>=16&&typeof n.secret=="string"&&n.secret.length>=16)return{token:n.token,secret:n.secret}}catch{}return null}function bS(){let e=sf(gt);if(e)return e;if(e=sf(To),e){H(lf.dirname(gt)),Wn.writeFileSync(gt,JSON.stringify(e,null,2)+`
411
+ `,{mode:384});try{Wn.unlinkSync(To)}catch{}return e}return null}function cf(e){H(lf.dirname(gt));let t=bS(),n=(e??"").trim();if(n){let r={token:n,secret:t?.secret??rf()};Wn.writeFileSync(gt,JSON.stringify(r,null,2)+`
412
+ `,{mode:384});try{Wn.existsSync(To)&&Wn.unlinkSync(To)}catch{}return r}if(t)return t;let o={token:wS(),secret:rf()};return Wn.writeFileSync(gt,JSON.stringify(o,null,2)+`
413
+ `,{mode:384}),o}pe();import fn from"node:fs";import RS from"node:http";import We from"node:path";import ft from"node:process";import{fileURLToPath as TS}from"node:url";import $S from"node:os";Ze();q();import rc from"node:crypto";var sc="omnish_cfg_sess",uf=7*24*60*60*1e3;function ic(e){let t=Date.now()+uf,n=Buffer.from(JSON.stringify({exp:t}),"utf8").toString("base64url"),o=rc.createHmac("sha256",e).update(n).digest("hex");return`${n}.${o}`}function df(e,t){let n=kS(t??"")[sc];if(!n||!n.includes("."))return!1;let o=n.lastIndexOf("."),r=n.slice(0,o),s=n.slice(o+1),i=rc.createHmac("sha256",e).update(r).digest("hex");try{let a=Buffer.from(s,"hex"),l=Buffer.from(i,"hex");if(a.length!==l.length||!rc.timingSafeEqual(a,l))return!1}catch{return!1}try{let a=JSON.parse(Buffer.from(r,"base64url").toString("utf8"));return!(typeof a.exp!="number"||a.exp<Date.now())}catch{return!1}}function ac(e){let t=Math.floor(uf/1e3);return`${sc}=${e}; HttpOnly; Path=/; SameSite=Lax; Max-Age=${t}`}function pf(){return`${sc}=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0`}function kS(e){let t={};for(let n of e.split(";")){let o=n.indexOf("=");if(o===-1)continue;let r=n.slice(0,o).trim(),s=n.slice(o+1).trim();r&&(t[r]=decodeURIComponent(s))}return t}q();import lc from"node:fs";import Ei from"node:process";function mf(e){try{return Ei.kill(e,0),!0}catch{return!1}}function hf(e){let t=Date.now()+e;for(;Date.now()<t;);}function vS(){try{let e=lc.readFileSync(gr,"utf8"),t=JSON.parse(e);if(!t||typeof t!="object")return null;let n=t,o=typeof n.pid=="number"?n.pid:Number(n.pid),r=typeof n.port=="number"?n.port:Number(n.port),s=typeof n.host=="string"?n.host:"",i=typeof n.startedAt=="string"?n.startedAt:"";return!Number.isFinite(o)||o<=0||!Number.isFinite(r)||r<=0||r>65535?null:{pid:o,port:r,host:s,startedAt:i}}catch{return null}}function ff(e){lc.writeFileSync(gr,`${JSON.stringify(e)}
414
+ `,{mode:384})}function pr(){try{lc.unlinkSync(gr)}catch{}}function gf(e){let t=vS();if(t&&t.port===e&&t.pid!==Ei.pid){if(!mf(t.pid)){pr();return}try{Ei.kill(t.pid,"SIGTERM")}catch{}if(hf(350),mf(t.pid)){try{Ei.kill(t.pid,"SIGKILL")}catch{}hf(100)}pr()}}pe();q();import cc from"node:fs";import SS from"node:process";function xS(e){try{return SS.kill(e,0),!0}catch{return!1}}function CS(){try{let e=cc.readFileSync(ge,"utf8").trim(),t=Number.parseInt(e,10);return Number.isFinite(t)&&t>0?t:null}catch{return null}}function Ai(){let e=CS();return e===null?!1:xS(e)}var uc=class{busy=!1;subscribers=new Set;abort=null;currentSock=null;subscribe(t){return this.subscribers.add(t),()=>{this.subscribers.delete(t)}}emit(t){for(let n of this.subscribers)try{n(t)}catch{}}requestCancel(){this.abort?.abort(),this.currentSock&&(fo(this.currentSock),this.currentSock=null)}beginPairing(t){if(this.busy)throw new Error("Pairing already in progress.");if(Ai())throw new Error("Gateway appears to be running (gateway.pid). Stop `omnish run` or your service before pairing from the browser.");if(!t.force&&lt())throw new Error("WhatsApp session already linked. Use \u201CReplace session\u201D or run `omnish link --force` from the CLI.");ie(),t.force&&(cc.rmSync(ce,{recursive:!0,force:!0}),cc.mkdirSync(ce,{recursive:!0,mode:448})),this.busy=!0,this.abort=new AbortController;let n=this.abort.signal;(async()=>{try{await nm({authDir:ce,verbose:!1,printQr:!1,signal:n,onQr:o=>this.emit({type:"qr",payload:o}),onRestart515:()=>this.emit({type:"restart"}),onSocketReady:o=>{this.currentSock=o},onSocketClosed:()=>{this.currentSock=null}}),this.emit({type:"open"}),this.emit({type:"done",ok:!0})}catch(o){let r=o instanceof Error?o.message:String(o);r==="Pairing cancelled."?this.emit({type:"error",message:"Cancelled."}):this.emit({type:"error",message:r}),this.emit({type:"done",ok:!1})}finally{this.busy=!1,this.abort=null,this.currentSock=null}})()}},mr=new uc;var he="application/json; charset=utf-8",hr=null;function MS(){pr();let e=hr;hr=null,e?e.close(()=>{ft.exit(0)}):ft.exit(0),setTimeout(()=>ft.exit(0),4e3).unref()}function PS(){let e=ft.env.OMNISH_CONFIG_UI_STATIC?.trim(),t=ft.env.OMNISH_UI_STATIC?.trim()||e;if(t&&fn.existsSync(We.join(t,"index.html")))return t;let n=We.dirname(TS(import.meta.url)),o=We.join(n,"ui");if(fn.existsSync(We.join(o,"index.html")))return o;let r=We.join(n,"..","..","dist","ui");if(fn.existsSync(We.join(r,"index.html")))return r;let s=We.join(ft.cwd(),"dist","ui");if(fn.existsSync(We.join(s,"index.html")))return s;throw new Error("omnish ui static files not found (expected dist/ui/index.html). Run `pnpm build`.")}function ES(e){let t=We.extname(e).toLowerCase();return{".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".ico":"image/x-icon",".woff2":"font/woff2",".json":"application/json; charset=utf-8"}[t]??"application/octet-stream"}function AS(e,t){let o=decodeURIComponent(t.split("?")[0]??"").replace(/^\/+/,""),r=We.normalize(We.join(e,o));return!r.startsWith(We.normalize(e+We.sep))&&r!==We.normalize(e)?null:r}async function dc(e,t=512e3){let n=[],o=0;for await(let s of e){if(o+=s.length,o>t)throw new Error("Body too large.");n.push(s)}if(n.length===0)return;let r=Buffer.concat(n).toString("utf8");return JSON.parse(r)}function IS(e){return tn.includes(e)}function pc(e){let t=(e.telegramBotToken??"").trim(),n=t.length===0?"":t.length<=8?"(set)":`${t.slice(0,4)}\u2026${t.slice(-4)}`;return{...e,telegramBotToken:n,telegramBotTokenConfigured:!!Me(e),telegramBotTokenEnvOverride:typeof ft.env.TELEGRAM_BOT_TOKEN=="string"&&ft.env.TELEGRAM_BOT_TOKEN.trim().length>0}}function LS(){let e=S();return{version:rt(),dataDir:D,configPath:_,waAuthDir:ce,whatsappLinked:lt(),gatewayPidHint:fn.existsSync(We.join(D,"gateway.pid")),gatewayRunning:Ai(),gatewayLogFile:Be,gatewayMode:e.gatewayMode,telegramBotTokenMasked:Me(e).length===0?"":pc(e).telegramBotToken,telegramBotTokenEnvOverride:typeof ft.env.TELEGRAM_BOT_TOKEN=="string"&&ft.env.TELEGRAM_BOT_TOKEN.trim().length>0}}function OS(e,t){if(!Array.isArray(e)||!Array.isArray(t))throw new Error("allowFrom and telegramAllowFrom must be arrays of strings.");let n=e.map(a=>String(a).trim()).filter(Boolean),o=t.map(a=>String(a).trim()).filter(Boolean),r=vr(n).filter(a=>a!=="*"),s=[];for(let a of o){let l=Ne(a);if(!l)throw new Error(`Invalid Telegram allow entry: ${a}`);s.push(l)}let i=S();i.allowFrom=r.sort(),i.telegramAllowFrom=[...new Set(s)].sort(),He(i)}function NS(e){return typeof e=="string"?e:typeof e=="boolean"||typeof e=="number"?String(e):JSON.stringify(e)}async function yf(e){let t=PS(),{meta:n}=e;gf(e.port);let o=RS.createServer(async(r,s)=>{try{let i=new URL(r.url??"/",`http://${r.headers.host??"localhost"}`),a=i.pathname;if(r.method==="GET"&&a==="/"){let m=i.searchParams.get("token")?.trim();if(m&&m===n.token){let h=ic(n.secret);s.statusCode=302,s.setHeader("Location","/"),s.setHeader("Set-Cookie",ac(h)),s.end();return}}if(a.startsWith("/api/")){let m=r.headers.cookie,h=df(n.secret,m);if(r.method==="POST"&&a==="/api/session"){let f=await dc(r),g=typeof f?.token=="string"?f.token.trim():"";if(!g||g!==n.token){s.statusCode=401,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:"Invalid token."}));return}let y=ic(n.secret);s.statusCode=200,s.setHeader("Content-Type",he),s.setHeader("Set-Cookie",ac(y)),s.end(JSON.stringify({ok:!0}));return}if(r.method==="GET"&&a==="/api/me"){s.statusCode=h?200:401,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:h}));return}if(!h){s.statusCode=401,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:"Unauthorized."}));return}if(r.method==="GET"&&a==="/api/status"){s.statusCode=200,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!0,...LS()}));return}if(r.method==="GET"&&a==="/api/config"){s.statusCode=200,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!0,config:pc(S())}));return}if(r.method==="PUT"&&a==="/api/config"){let f=await dc(r);if(!f||typeof f!="object"){s.statusCode=400,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:"Expected JSON object."}));return}let g=f;for(let[y,b]of Object.entries(g))if(b!==void 0&&!(y==="allowFrom"||y==="telegramAllowFrom")){if(y==="telegramBotToken"){let k=typeof b=="string"?b.trim():"";if(!k||k==="(set)"||k.includes("\u2026"))continue;if(!wt(k))throw new Error("Invalid Telegram bot token format.");jr("telegramBotToken",k);continue}if(!IS(y))throw new Error(`Unknown or unsupported config key: ${y}`);jr(y,NS(b))}if("allowFrom"in g||"telegramAllowFrom"in g){let y=S();OS("allowFrom"in g?g.allowFrom:y.allowFrom,"telegramAllowFrom"in g?g.telegramAllowFrom:y.telegramAllowFrom)}s.statusCode=200,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!0,config:pc(S())}));return}if(r.method==="POST"&&a==="/api/logout"){s.statusCode=200,s.setHeader("Content-Type",he),s.setHeader("Set-Cookie",pf()),s.end(JSON.stringify({ok:!0}));return}if(r.method==="POST"&&a==="/api/shutdown"){s.statusCode=200,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!0})),setImmediate(()=>{mr.requestCancel(),MS()});return}if(r.method==="POST"&&a==="/api/gateway/start"){if(Ai()){s.statusCode=409,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:"Gateway already appears to be running (pidfile + live process). Stop it first if you need to restart."}));return}let f=xi();if(!f.ok){s.statusCode=400,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:f.message}));return}let g=Be,y=Ds(g);if(!y.ok){s.statusCode=500,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:y.message}));return}s.statusCode=200,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!0,pid:y.pid,logFile:g}));return}if(r.method==="POST"&&a==="/api/gateway/stop"){let f=Us();switch(f.outcome){case"no_pidfile":s.statusCode=400,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:"No gateway pidfile \u2014 background gateway does not appear to be running."}));return;case"invalid_pidfile":s.statusCode=400,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:"Invalid pidfile (removed)."}));return;case"stale_cleaned":s.statusCode=200,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!0,stale:!0,pid:f.pid,message:"Process was not running; stale pidfile removed."}));return;case"sent_signal":s.statusCode=200,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!0,pid:f.pid}));return;case"taskkill_ok":s.statusCode=200,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!0,pid:f.pid,taskkill:!0}));return;case"failed":s.statusCode=500,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:f.message}));return}}if(r.method==="GET"&&a==="/api/wa/link/events"){s.writeHead(200,{"Content-Type":"text/event-stream; charset=utf-8","Cache-Control":"no-store",Connection:"keep-alive"}),s.write(`: connected
415
+
416
+ `);let f=mr.subscribe(g=>{s.write(`data: ${JSON.stringify(g)}
417
+
418
+ `)});r.on("close",()=>{f()});return}if(r.method==="POST"&&a==="/api/wa/link/start"){let g=(await dc(r))?.force===!0;try{mr.beginPairing({force:g})}catch(y){let b=String(y),k=b.includes("already in progress")||b.includes("Gateway appears")?409:400;s.statusCode=k,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:b}));return}s.statusCode=202,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!0}));return}if(r.method==="POST"&&a==="/api/wa/link/cancel"){mr.requestCancel(),s.statusCode=200,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!0}));return}s.statusCode=404,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:"Not found."}));return}let l=a==="/"?"index.html":a.replace(/^\/+/,""),c=AS(t,l),d=We.join(t,"index.html");c&&fn.existsSync(c)&&fn.statSync(c).isFile()&&(d=c);let u=fn.readFileSync(d);s.statusCode=200,s.setHeader("Content-Type",ES(d)),s.setHeader("Cache-Control",We.basename(d)==="index.html"?"no-store":"public, max-age=3600"),s.end(u)}catch(i){s.statusCode=500,s.setHeader("Content-Type",he),s.end(JSON.stringify({ok:!1,error:String(i)}))}});await new Promise((r,s)=>{o.once("error",s),o.listen(e.port,e.host,()=>r())}),hr=o,ff({pid:ft.pid,port:e.port,host:e.host,startedAt:new Date().toISOString()}),o.on("close",()=>{pr(),hr===o&&(hr=null)})}function wf(e){let t=$S.networkInterfaces(),n=[];for(let o of Object.values(t))if(o)for(let r of o)r.family==="IPv4"&&!r.internal&&n.push(`http://${r.address}:${e}/`);return[...new Set(n)].sort()}import _S from"node:readline";import kf from"node:path";import Ie from"node:process";pe();Ze();Ze();function mc(e){return e.channel==="all"||e.channel==="whatsapp-all"||e.channel==="telegram-all"}function FS(e){let t=e.trimStart();if(/^--text=/i.test(t)){let n=t.slice(t.indexOf("=")+1).trimEnd();return n.length>0?n:null}if(/^--text(\s+|$)/i.test(t)){let n=t.replace(/^--text\s*/i,"").trim();return n.length>0?n:null}if(/^-t=/i.test(t)){let n=t.slice(t.indexOf("=")+1).trimEnd();return n.length>0?n:null}if(/^-t(\s+|$)/i.test(t)){let n=t.replace(/^-t\s*/i,"").trim();return n.length>0?n:null}}function WS(e){let t=e.toLowerCase();if(t==="*"||t==="all")return{channel:"all"};if(t==="wa"||t==="whatsapp"||t==="wa:all"||t==="whatsapp:all")return{channel:"whatsapp-all"};if(t==="tg"||t==="telegram"||t==="tg:all"||t==="telegram:all")return{channel:"telegram-all"};if(t.startsWith("tg:")||t.startsWith("telegram:")){let n=Ne(e);if(!n)return null;let o=Number(n);return Number.isFinite(o)?{channel:"telegram",chatId:o}:null}if(t.startsWith("wa:")){let n=e.slice(3).trim();if(!n)return null;let o=n.split(",").map(r=>ne(r.trim())).filter(r=>!!r);return o.length===0?null:{channel:"whatsapp",e164s:o}}if(e.includes(",")){let n=e.split(",").map(o=>ne(o.trim())).filter(o=>!!o);return n.length===0?null:{channel:"whatsapp",e164s:n}}if(e.startsWith("+")){let n=ne(e);return n?{channel:"whatsapp",e164s:[n]}:null}return null}function bf(e){let t=e.trim();if(!/^\/sendto(\s|$)/i.test(t))return null;let n=t.replace(/^\/sendto\s+/i,"").trim();if(!n)return null;let o=n.indexOf(" ");if(o===-1)return null;let r=n.slice(0,o).trim(),s=n.slice(o+1).trim();if(!s)return null;let i=WS(r);if(!i)return null;let a=FS(s);if(a===null)return null;if(a!==void 0)return{...i,mode:"text",body:a};let l=oi(s);if(!l)return null;let{selectorPart:c,caption:d}=l;return{...i,mode:"media",selectorPart:c,caption:d}}function hc(){return["/sendto wa|tg|* <selectors> [-- caption]","/sendto +E164 or +E164,+E164 <selectors> [-- caption]","/sendto <dest> --text <message> or --text=<msg> or -t <msg>","/sendto tg:<chat_id> <selectors> [-- caption] (compat: single Telegram id)","selectors: file1,file2 or *.mp4 or **/*.mp4 (from current session cwd); a path ./--text sends a file named --text","examples: /sendto wa ./promo.mp4 | /sendto * **/*.mp4 -- Daily clips","examples: /sendto +15551234567 --text Hello | /sendto tg:123 -t=Status OK","examples: /sendto +15550000001,+15550000002 intro.png,deck.pdf -- Launch","Also supports compat aliases: wa:all, whatsapp:all, tg:all, telegram:all.","Requires `omnish run` on this machine."].join(`
419
+ `)}tr();Ve();q();var Ro="wa:cli:interactive",DS={onPlainTextLlmFallback(e,t){uo(S(),e,t,async n=>{n.trim()&&console.log(U(Ie.stdout,n))})}};function US(e){let t=e.trim();if(!t)return null;let n=t.toLowerCase();if(n.startsWith("tg:")||n.startsWith("telegram:")){let s=Ne(t);return s?`tg:${s}`:null}let o=n.startsWith("wa:")?t.slice(3):t,r=ne(o);return r?`wa:${r}`:null}function BS(e){let t=null,n=null;for(let o=0;o<e.length;){let r=e[o]??"";if(r==="--as"){let s=e[o+1];if(!s||s.startsWith("-"))return{opts:{senderKey:null,oneShot:null},error:"--as requires a sender (wa:+E164 or tg:<id>)."};let i=US(s);if(!i)return{opts:{senderKey:null,oneShot:null},error:`Could not parse sender "${s}". Use +E164, wa:+E164, or tg:<user_id>.`};t=i,o+=2;continue}if(r==="-c"||r==="--command"){let s=e[o+1];if(typeof s!="string")return{opts:{senderKey:null,oneShot:null},error:`${r} requires a command string.`};n=s,o+=2;continue}return r==="--help"||r==="-h"?{opts:{senderKey:null,oneShot:null},error:"help"}:{opts:{senderKey:null,oneShot:null},error:`Unknown argument: ${r}`}}return{opts:{senderKey:t,oneShot:n},error:null}}function fc(e){let t=Ie.cwd(),n=[`${ye(e,"omnish i")} ${w(e,"[options]")}`,X(e,"Interactive shell \u2014 same commands as WhatsApp/Telegram chat."),"",z(e,"Usage:"),` ${v(e,"omnish i [options]")}`,` ${v(e,"omnish interactive [options]")}`,"",z(e,"Options:"),...Et(e," ",[{left:"--as <sender>",right:"Sender key for cluster commands (wa:+E164 or tg:id). Default: synthetic wa:cli:interactive."},{left:"-c, --command <line>",right:"Run one line and exit (non-interactive)."},{left:"-h, --help",right:"Show this help."}],o=>w(e,o)),"",`${w(e,"Trust:")} ${v(e,"Full local access like your shell; not gated by inbox allowlist.")}`,`${w(e,"Jobs:")} ${v(e,"/bg and /jobs apply only to this REPL session (not the gateway process).")}`,`${w(e,"Files:")} ${v(e,"Use /sendto for files or --text for plain messages through the gateway; plain /send needs a chat peer.")}`,`${w(e,"Gateway:")} ${v(e,"/reload and /updates require omnish run; /sendto requires omnish run for WA/TG delivery.")}`,"",hc(),"",`${w(e,"cwd:")} ${v(e,`session starts at ${t} (change with !cd or ${S().commandPrefix}cd).`)}`];console.log(n.join(`
420
+ `))}function vf(e,t){let n=new Set,o=new Set;if(e.channel==="whatsapp-all"||e.channel==="all"){let r=new Set;for(let s of t.allowFrom){let i=ne(String(s));i&&!r.has(i)&&(r.add(i),n.add(i))}}if(e.channel==="telegram-all"||e.channel==="all")for(let r of t.telegramAllowFrom){let s=Ne(String(r));if(!s)continue;let i=Number(s);Number.isFinite(i)&&o.add(i)}if(e.channel==="whatsapp")for(let r of e.e164s)n.add(r);return e.channel==="telegram"&&o.add(e.chatId),{waTargets:n,tgTargets:o}}function Sf(e){let t="No recipients matched the requested /sendto destination.";return mc(e)?re()?`${t} Check platform dashboard allowlists (or omnish platform status), or use explicit +E164 / tg:<id>.`:`${t} Set allowFrom / telegramAllowFrom in config.json, or use explicit +E164 / tg:<id>.`:t}async function HS(e){let t=mc(e)?await wl():S();if(e.mode==="text"){let{waTargets:u,tgTargets:m}=vf(e,t);if(u.size===0&&m.size===0)return{error:Sf(e)};let h=[];for(let f of u){let g=await Sn({op:"sendText",channel:"whatsapp",e164:f,text:e.body});g&&h.push(`[wa:${f}] ${g}`)}for(let f of m){let g=await Sn({op:"sendText",channel:"telegram",chatId:f,text:e.body});g&&h.push(`[tg:${f}] ${g}`)}return h.length>0?{error:h.join(`
421
+ `)}:{error:null,kind:"text",recipientsSent:u.size+m.size}}let n=ae(Ro).cwd,o=await rr(n,e.selectorPart);if(o.length===0)return{error:`No files matched: ${e.selectorPart}`};let r=await sr(o);if(!r.ok)return{error:r.error};let s=o.map(u=>xt(u,t.fileSendMaxBytes));for(let u of s)if("error"in u)return{error:u.error};let{waTargets:i,tgTargets:a}=vf(e,t);if(i.size===0&&a.size===0)return{error:Sf(e)};let l=[];for(let u of i)for(let m of s){if("error"in m)continue;let h=await Sn({op:"sendMedia",channel:"whatsapp",e164:u,absPath:m.absPath,caption:e.caption});h&&l.push(`[wa:${u}] ${m.displayName}: ${h}`)}for(let u of a)for(let m of s){if("error"in m)continue;let h=await Sn({op:"sendMedia",channel:"telegram",chatId:u,absPath:m.absPath,caption:e.caption});h&&l.push(`[tg:${u}] ${m.displayName}: ${h}`)}if(l.length>0)return{error:l.join(`
422
+ `)};let c=i.size+a.size,d=s.length;return{error:null,kind:"media",recipientsSent:c,filesSent:d,messagesSent:c*d}}async function jS(e,t,n,o,r,s,i){let a=e.trim();if(!a)return;let l=bf(a);if(l!==null||/^\/sendto(\s|$)/i.test(a)){if(l===null){console.log(C(Ie.stderr,`Invalid /sendto.
423
+ `+hc()));return}let u=await HS(l);if(u.error!==null)console.log(C(Ie.stderr,u.error));else if(u.kind==="text")console.log(U(Ie.stdout,`Sent text to ${u.recipientsSent} recipient(s).`));else{let m=`Sent ${u.filesSent} file(s) to ${u.recipientsSent} recipient(s) (${u.messagesSent} message(s)).`;console.log(U(Ie.stdout,m))}return}let c={peerKey:Ro,text:e},d=await hn(S(),t,n,o,r,c,s,void 0,i,!1,DS);d!==null&&await GS(d)}async function GS(e){if(e===null)return;if(e.kind==="file"||e.kind==="files"){console.log(U(Ie.stdout,["This CLI session has no chat peer to attach to.","Push through the gateway instead, for example:"," /sendto wa:+15551234567 ./my.pdf"," /sendto tg:123456789 ~/photo.jpg -- optional caption","(Requires `omnish run` on this machine.)"].join(`
424
+ `)));return}if(e.kind==="bundle"){for(let n of e.texts??[]){let o=me(n,"whatsapp").text;o.trim()&&(console.log(o),console.log(""))}for(let n of e.files??[])console.log(U(Ie.stdout,`File: ${n.absPath}`));return}if(e.kind==="texts"){for(let n=0;n<e.bodies.length;n++){let o=me(e.bodies[n],"whatsapp").text;o.trim()&&(n>0&&console.log(""),console.log(o))}return}if(e.kind!=="text")return;let t=me(e.body,"whatsapp").text;t.trim()&&console.log(t)}async function xf(e){let t=BS(e);if(t.error==="help"){fc(Ie.stdout);return}if(t.error&&t.error!==null){console.error(C(Ie.stderr,t.error)),console.error(w(Ie.stderr,"Try: omnish i --help")),Ie.exitCode=1;return}let{senderKey:n,oneShot:o}=t.opts,r=n??Ro;ie(),Fr(Ro,Ie.cwd());let s=new Dt,i=new Map,a=new Map,l=new Map,c=new cn(()=>S(),async(h,f)=>{Ie.stdout.write(f),f.endsWith(`
425
+ `)||Ie.stdout.write(`
426
+ `)}),d=async h=>{try{await jS(h,s,i,a,l,c,r)}catch(f){console.error(C(Ie.stderr,String(f)))}};if(o!==null){await d(o),c.dispose(),s.killAllRunning();return}let u=_S.createInterface({input:Ie.stdin,output:Ie.stdout}),m=kf.basename(ae(Ro).cwd);u.setPrompt(`${m}> `),u.on("line",h=>{d(h).then(()=>{let f=kf.basename(ae(Ro).cwd);u.setPrompt(`${f}> `),u.prompt()})}),u.on("close",()=>{c.dispose(),s.killAllRunning(),Ie.stdout.write(`
427
+ `)}),u.prompt()}function gc(){console.log(`omnish docs \u2014 search bundled documentation (offline)
405
428
 
406
429
  omnish docs help
407
430
  omnish docs search <topic>
@@ -409,22 +432,22 @@ ${s.filter(l=>l.severity==="error").map(l=>{let d=[l.message];return l.detail&&d
409
432
  omnish docs show <path> e.g. docs/features/tunneling.md
410
433
 
411
434
  Chat: /docs search <topic> \xB7 /docs <n> \xB7 /docs follow <n>
412
- `)}function _h(e){let t=(e[0]??"help").toLowerCase(),n=e.slice(1).join(" ").trim();if(t==="help"||t==="-h"||t==="--help"){zl();return}if(t==="search"){if(!n){console.error("[omnish] Usage: omnish docs search <topic>"),process.exitCode=1;return}let r=Xs(n).map(Zs);if(Jm(r),r.length===0){console.log(`Search: ${n}
435
+ `)}function Rf(e){let t=(e[0]??"help").toLowerCase(),n=e.slice(1).join(" ").trim();if(t==="help"||t==="-h"||t==="--help"){gc();return}if(t==="search"){if(!n){console.error("[omnish] Usage: omnish docs search <topic>"),process.exitCode=1;return}let r=yi(n).map(wi);if(Ih(r),r.length===0){console.log(`Search: ${n}
413
436
  (no results)`);return}console.log(`Search: ${n}
414
437
  `),r.forEach((s,i)=>{let a=s.relatedCommands[0];console.log(`${i+1}. ${s.title}${a?` \u2014 ${a}`:""}`),console.log(` ${s.path}`),s.summary&&console.log(` ${s.summary.slice(0,120)}`)}),console.log(`
415
- Show: omnish docs show <n>`);return}if(t==="show"){if(!n){console.error("[omnish] Usage: omnish docs show <n> | <doc-path>"),process.exitCode=1;return}if(/^\d+$/.test(n)){let r=zm(n);if(!r){console.error("[omnish] No result #"+n+". Run omnish docs search first."),process.exitCode=1;return}let s=Xo(r.id);if(!s){console.error("[omnish] Entry missing from index."),process.exitCode=1;return}Fh(s,Number.parseInt(n,10));return}let o=ei(n);if(!o){console.error(`[omnish] No doc at path "${n}".`),process.exitCode=1;return}Fh(o);return}console.error(`[omnish] Unknown docs subcommand "${t}". Try: omnish docs help`),process.exitCode=1}function Fh(e,t){if(!e)return;let n=Vs(sn.repoUrl,e.path),o=t!==void 0?`${t}. ${e.title}`:e.title;if(console.log(o),console.log(e.path),console.log(n),console.log(""),console.log(ti(e)),console.log(""),e.relatedCommands.length){console.log("Try:");for(let r of e.relatedCommands.slice(0,8))console.log(` ${r}`)}}Fv.setDefaultResultOrder("ipv4first");function Uh(){let e=process.stdout,t=[`${Ce(e,"omnish run")} ${w(e,"[options]")}`,Q(e,"Listen for DMs and run shell commands from allowlisted chats."),"",q(e,"Usage:"),` ${v(e,"omnish run [options]")}`,"",q(e,"Options:"),...$t(e," ",[{left:"-d, --background",right:"Start the gateway detached; log to --log-file (default: <data>/logs/gateway.log)."},{left:"--log-file <path>",right:`Append stdout/stderr when background (default: ${Ue}).`},{left:"-vb, --verbose",right:"Baileys/gateway debug logs on stderr (legacy: OMNISH_VERBOSE=1)."},{left:"-h, --help",right:"Show this help."}],o=>w(e,o)),"",`${v(e,"Config reload:")} ${w(e,"while the gateway runs, edit config then send /reload or /restart from an allowlisted chat (no restart needed for many keys). /updates checks npm (and optional updateInfoUrl).")}`,""],n=ae();n?t.push(q(e,"Attached mode (platform credentials detected):"),` ${w(e,"url:")} ${v(e,n.platformUrl)} ${Q(e,`[${io()}]`)}`,` ${w(e,"token:")} ${v(e,hn("platformToken",n.token))} ${Q(e,`[${so()}]`)}`,w(e," Messengers run on the platform; allowlist is set on the dashboard. Run: omnish platform probe"),""):t.push(w(e,"Platform attached mode: omnish config add platform_url <url> platform_token <token>"),w(e," then omnish platform probe && omnish run \u2014 see omnish help platform"),""),console.log(t.join(`
416
- `))}function Bh(){let e=process.stdout,t=[{left:"omnish link [--force]",right:"WhatsApp: scan QR (Linked devices). --force wipes session first."},{left:"omnish link --tg <bot_token>",right:"Telegram: save token to config; gatewayMode telegram or both if WhatsApp is linked."}],n=t.map(i=>w(e,i.left)),o=Math.max(...n.map($r)),r=t.map((i,a)=>Ai(" ",o,n[a],v(e,i.right))),s=[`${Ce(e,"omnish link")} ${w(e,"[options]")}`,Q(e,"Connect WhatsApp (QR) or save a Telegram bot token."),"",q(e,"Usage:"),` ${v(e,"omnish link [--force]")}`,` ${v(e,"omnish link --tg <bot_token>")}`,"",q(e,"Modes:"),...r,` ${Q(e,"Do not combine --tg with --force.")}`,"",q(e,"Options:"),...$t(e," ",[{left:"-f, --force",right:"WhatsApp only: delete saved session before pairing."},{left:"-vb, --verbose",right:"Baileys debug logs on stderr during pairing (legacy: OMNISH_VERBOSE=1)."},{left:"-h, --help",right:"Show this help."}],i=>w(e,i)),"",`${v(e,"Next:")} ${me(e,"omnish allow tg:<your_user_id>")} ${w(e,"then")} ${me(e,"omnish run")}`,`${w(e,"Config:")} ${v(e,_)}`,""];ae()&&s.push(q(e,"Platform attached mode:"),w(e," omnish link on this host is for standalone only. Link WhatsApp on the platform dashboard or:"),` ${v(e,"omnish platform import-whatsapp")} ${w(e,"(after a local omnish link, with omnish run stopped)")}`,w(e," Telegram: set bot token on the platform dashboard, not --tg here."),""),console.log(s.join(`
417
- `))}function Wv(e){let t=!1,n=null;for(let o=0;o<e.length;o++){let r=e[o]??"";if(r==="--help"||r==="-h")return{kind:"help"};if(r==="--force"||r==="-f"){t=!0;continue}if(r==="-vb"||r==="--verbose"){wi(!0);continue}if(r==="--tg"||r==="--telegram"){let s=e[o+1];if(!s||s.startsWith("-"))return{kind:"error",message:"[omnish] --tg requires a bot token (from @BotFather)."};n=s,o++;continue}if(r.startsWith("--tg=")||r.startsWith("--telegram=")){let s=r.indexOf("="),i=r.slice(s+1).trim();if(!i)return{kind:"error",message:"[omnish] --tg= requires a non-empty bot token."};n=i;continue}return{kind:"error",message:`[omnish] unknown link argument: ${r}
418
- Try: omnish link --help`}}return n!==null?t?{kind:"error",message:"[omnish] --force applies to WhatsApp only; do not combine with --tg."}:{kind:"tg",token:n}:{kind:"wa",force:t}}function Yl(){let e=process.stdout,t=`${Ce(e,"omnish")} ${w(e,`v${ot()}`)}`,n=[{left:"link [--force] [--tg <token>]",right:"WhatsApp (QR) or Telegram bot token \u2014 omnish link --help"},{left:"run [options]",right:"Listen for DMs (WhatsApp and/or Telegram \u2014 see gatewayMode in config)"},{left:"stop",right:`Stop background gateway (pidfile: ${de})`},{left:"service <subcommand>",right:"Boot install hints, logs, systemd/LaunchAgent \u2014 omnish service help"},{left:"pull <subcommand>",right:"Media URL tools (yt-dlp, ffmpeg, Whisper) \u2014 omnish pull help"},{left:"logout",right:"Delete saved WhatsApp session"},{left:"allow +<E164> | tg:<id>",right:"Add allowlist entry"},{left:"deny +<E164> | tg:<id>",right:"Remove allowlist entry"},{left:"status [--check-updates]",right:"Channels, identity, allowlists, jobs, security, cluster (if enabled)"},{left:"commands",right:"Chat commands for allowlisted users (same as /help)"},{left:"security [--json]",right:"Configuration security report (JSON for scripts)"},{left:"cluster [status | use <sender> <label-or-id>]",right:"Per-sender machine bindings"},{left:"i | interactive [options]",right:"Local REPL (chat commands; /sendto needs omnish run)"},{left:"ui [options]",right:"Browser setup UI on LAN (token-gated) \u2014 omnish ui --help"},{left:"config <add|show|edit|delete>",right:"Manage config.json (platform URL/token, tunnel, gateway) \u2014 omnish config help"},{left:"platform <subcommand>",right:"Attached mode: configure, probe, import WA \u2014 omnish help platform"},{left:"tunnel <subcommand>",right:"Expose local HTTP/TCP via omnish relay \u2014 omnish tunnel help"},{left:"docs <subcommand>",right:"Search bundled guides offline \u2014 omnish docs help (same index as /docs in chat)"}],o=[{left:"-v, --version",right:"Print version and exit."},{left:"-h, --help",right:"Show this help (same as omnish help)."}],r=[t,Q(e,"Allowlisted inbox \u2192 your real shell. No AI."),"",q(e,"Usage:"),` ${v(e,"omnish [options] <command> [args...]")}`,"",q(e,"Options:"),...$t(e," ",o,s=>w(e,s)),"",q(e,"Commands:"),...$t(e," ",n,s=>w(e,s)),"",`${w(e,"Config:")} ${v(e,`${_} \u2014 gatewayMode: "whatsapp" | "telegram" | "both"`)}`,`${w(e,"Platform:")} ${v(e,"platform_url + platform_token \u2192 attached omnish run (omnish help platform)")}`,`${w(e,"Verbose:")} ${v(e,"omnish run --verbose (legacy: OMNISH_VERBOSE=1, WHATSVERBOSE=1)")}`,`${w(e,"Env:")} ${v(e,"TELEGRAM_BOT_TOKEN (optional override)")}`,`${w(e,"Data:")} ${v(e,"~/.omnish by default; ~/.whatslive reused if it already exists. OMNISH_HOME overrides.")}`,`${w(e,"See also:")} ${v(e,"https://omnish.dev")}`,""];console.log(r.join(`
419
- `))}function Dv(e){let t=(e??"").trim().toLowerCase(),n=process.stdout;if(!t){Yl();return}switch(t){case"link":Bh();return;case"run":Uh();return;case"service":jh();return;case"pull":ga();return;case"i":case"interactive":Jl(n);return;case"ui":Gh();return;case"config":ii(n);return;case"platform":ui(n);return;case"docs":zl();return;default:console.error(T(process.stderr,`No detailed help for "${e}". Try: omnish help`)),process.exitCode=1}}function Uv(e){let t=!1,n="",o=!1,r=!1;for(let i=0;i<e.length;i++){let a=e[i]??"";if(a==="-d"||a==="--background")t=!0;else if(a==="-vb"||a==="--verbose")r=!0;else if(a==="--log-file"||a==="--log"){let l=e[++i];l||(console.error(T(process.stderr,"--log-file requires a path.")),process.exit(1)),n=l}else if(a==="--help"||a==="-h")o=!0;else{let l=process.stderr;console.error(T(l,`unknown run option: ${a}`)),console.error(T(l,"Try: omnish run --help")),process.exit(1)}}let s=n.trim()!==""?Kl.isAbsolute(n)?n:Kl.resolve(process.cwd(),n):Ue;return{background:t,logFile:s,help:o,verbose:r}}function Bv(e,t){let n=ks(e,{verbose:t});n.ok||(console.error(T(process.stderr,n.message)),process.exit(1));let o=process.stdout;console.log(`${D(o,`gateway started in background (pid ${n.pid}).`)} ${w(o,`Log: ${e}`)}`)}function Hv(){let e=vs();switch(e.outcome){case"no_pidfile":console.error(T(process.stderr,`no pidfile at ${de} \u2014 is a background gateway running?`)),process.exitCode=1;return;case"invalid_pidfile":console.error(T(process.stderr,"invalid pidfile.")),process.exitCode=1;return;case"stale_cleaned":console.log(D(process.stdout,`process ${e.pid} is not running; removing stale pidfile.`));return;case"sent_signal":console.log(D(process.stdout,`sent SIGTERM to gateway (pid ${e.pid}).`));return;case"taskkill_ok":console.log(D(process.stdout,`stopped gateway (pid ${e.pid}) using taskkill.`));return;case"failed":console.error(T(process.stderr,e.message)),process.exitCode=1;return}}function Wh(){if(process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{cn.readFileSync(de,"utf8").trim()===String(process.pid)&&cn.unlinkSync(de)}catch{}}function jv(e){return e.length<=8?"(set)":`${e.slice(0,4)}\u2026${e.slice(-4)}`}function Gv(){if(!cn.existsSync(de))return"gateway process: not running (no pid file)";let e=cn.readFileSync(de,"utf8").trim(),t=Number(e);if(!Number.isFinite(t)||t<=0)return"gateway process: invalid pid file";try{return process.kill(t,0),`gateway process: running (pid ${t})`}catch{return`gateway process: not running (stale pid ${t} in pid file)`}}var Hh=120;function jh(){let e=process.stdout,t=[{left:"help",right:"This help."},{left:"instructions",right:"Copy-paste install steps for this machine."},{left:"status",right:"Data dir, pidfile, Node + entry script."},{left:"logs [n]",right:`Tail default gateway log (default 80 lines, max ${Hh}).`},{left:"install",right:"Write user systemd / LaunchAgent unit (needs serviceInstallFromChat=true)."},{left:"uninstall",right:"Remove that unit (same gate as install)."}],n=[`${Ce(e,"omnish service")} ${w(e,"<subcommand>")}`,Q(e,"Boot integration, crash restart policy, and logs (same ideas as /service in chat)."),"",q(e,"Usage:"),` ${v(e,"omnish service <subcommand>")}`,"",q(e,"Subcommands:"),...$t(e," ",t,o=>w(e,o)),"",q(e,"Restart and reload:"),` ${v(e,"Process")} ${w(e,"\u2014 Linux user unit: Restart=on-failure, RestartSec=5. macOS: KeepAlive. Full restart loads config from disk.")}`,` ${v(e,"Config live")} ${w(e,"\u2014 allowlisted chat while gateway runs: /reload or /restart")}`,"",`${w(e,"Docs:")} ${v(e,"docs/guides/background-and-boot.md")} ${Q(e,"\xB7")} https://omnish.dev`,""];console.log(n.join(`
420
- `))}function qv(e){let t=process.stdout,n=process.stderr,o=(e[0]??"help").toLowerCase();if(o==="help"||o==="--help"||o==="-h"){jh();return}if(o==="instructions"){let r=Ot();if(r.error){console.error(T(n,r.error)),process.exitCode=1;return}console.log(Nr(r));return}if(o==="status"){let r=Ot(),s=(()=>{try{return cn.existsSync(de)?`gateway.pid: ${cn.readFileSync(de,"utf8").trim()}`:"gateway.pid: (missing)"}catch(c){return`gateway.pid: (read error: ${String(c)})`}})(),i=process.env.OMNISH_BACKGROUND_GATEWAY==="1"?"This process: background gateway (OMNISH_BACKGROUND_GATEWAY=1).":"This process: CLI (not the gateway \u2014 run omnish service status on the host where the gateway runs for live pid info).",a=typeof process.env.OMNISH_HOME=="string"&&process.env.OMNISH_HOME.trim()?`OMNISH_HOME env: ${process.env.OMNISH_HOME.trim()}`:"OMNISH_HOME env: (not set \u2014 using default data dir)",l=r.error?r.error:`Node: ${r.nodePath}
421
- Script: ${r.scriptPath}`,u=S().serviceInstallFromChat?"Install from CLI/chat: enabled (omnish service install / /service install).":"Install from CLI/chat: off \u2014 set serviceInstallFromChat true in config (same trust as shell).";console.log(Ce(t,"omnish service status")),console.log(""),console.log(`${w(t,"platform:")} ${v(t,process.platform)}`),console.log(`${w(t,"session:")} ${v(t,i)}`),console.log(`${w(t,"env:")} ${v(t,a)}`),console.log(`${w(t,"data dir:")} ${v(t,W)}`),console.log(`${w(t,"pidfile:")} ${v(t,s)}`),console.log(`${w(t,"default log:")} ${v(t,Ue)}`),console.log(""),console.log(l),console.log(""),console.log(v(t,u));return}if(o==="logs"){let r=e.length>=2?Number.parseInt(e[1],10):80,s=Number.isFinite(r)&&r>0?Math.min(r,Hh):80,i=Gr(Ue,s);console.log(`${w(t,"file:")} ${v(t,Ue)}`),console.log(`${w(t,"lines:")} ${v(t,String(s))}`),console.log(""),console.log(i);return}if(o==="install"){if(!S().serviceInstallFromChat){console.error(T(n,"Install is disabled. Set serviceInstallFromChat to true in config (same trust as shell), then run again.")),process.exitCode=1;return}let s=Br();s.ok?console.log(D(t,s.detail)):(console.error(T(n,s.detail)),process.exitCode=1);return}if(o==="uninstall"){if(!S().serviceInstallFromChat){console.error(T(n,"Uninstall is disabled. Set serviceInstallFromChat to true in config or remove the unit file on the host manually.")),process.exitCode=1;return}let s=Hr();s.ok?console.log(D(t,s.detail)):(console.error(T(n,s.detail)),process.exitCode=1);return}console.error(T(n,`Unknown subcommand "${o}". Try: omnish service help`)),process.exitCode=1}function Jv(e){let t=e.trim();if(!t)return null;let n=t.toLowerCase();if(n.startsWith("tg:")||n.startsWith("telegram:")){let s=Ne(t);return s?`tg:${s}`:null}let o=n.startsWith("wa:")?t.slice(3):t,r=te(o);return r?`wa:${r}`:null}async function zv(){let e=null,t=ri();t.ok||(console.error(T(process.stderr,t.message)),process.exit(1));let n=ae();if(n){await Vm(n);return}let o=S(),r=o.gatewayMode,s=r==="whatsapp"||r==="both",i=r==="telegram"||r==="both",a=Me(o),l=At(o),d=process.stderr,u=Zc(l,"warn");if(u.length>0&&(console.warn(`${D(d,`Security (${u.length} finding(s)):`)}
422
- `),console.warn(Fi(u,d))),process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{cn.writeFileSync(de,`${process.pid}
423
- `,{mode:384})}catch(E){P.warn({err:String(E)},"could not write gateway pidfile")}let c=(E,j)=>{let K=S();if(!K.clusterEnabled)return E;let J=nt(),C=(K.clusterLabel??"").trim()||Dh.hostname(),x=null;if(j.startsWith("tg:"))x=j;else if(j){let oe=te(j);oe&&(x=`wa:${oe}`)}let Y=x?It(K,x):null;return Bu(E,{nodeId:J,label:C,role:K.clusterRole,activeNodeId:Y?.nodeId??""})},m=new Ft,h=new Map,f=new Map,g=new Map,y=null,b={stop:null,sendText:null,sendMedia:null},k=null,M=!1,R=async(E,j)=>{if(E.startsWith("wa:")){let K=E.slice(3);y&&await y.sendText(K,j)}else if(E.startsWith("tg:")){let K=Number(E.slice(3));b.sendText&&Number.isFinite(K)&&await b.sendText(K,p(j))}},L={onPlainTextLlmFallback(E,j){Yn(S(),E,j,K=>R(E,K))},sendToPeer:R},$=async(E,j)=>{if(E.startsWith("wa:")){let K=E.slice(3);y&&await y.sendMedia(K,j)}else if(E.startsWith("tg:")){let K=Number(E.slice(3));b.sendMedia&&Number.isFinite(K)&&await b.sendMedia(K,j)}},N=()=>new en(()=>S(),R),A=N();k=A;let z,Z={async reload(){try{P.info("gateway reload requested from chat"),await b.stop?.().catch(()=>{}),b.stop=null,b.sendText=null,b.sendMedia=null;let E=S(),j=E.gatewayMode==="telegram"||E.gatewayMode==="both",K=Me(E);if(j&&K){let x=await Wa(K,()=>S(),z,{decorate:c});b.sendText=x.sendText,b.sendMedia=x.sendMedia,b.stop=x.stop}let J=["Reload complete.",`gatewayMode: ${E.gatewayMode}`,j&&K?"Telegram bot is running with the current token.":"Telegram bot is stopped (enable telegram/both + token if you want it).","Allowlists, shell, app session limits, and timeouts are read from disk on each command."].join(`
424
- `),C=_s(Go());return{ok:!0,summary:C?`${J}
425
-
426
- Updates (last check): ${C}`:J}}catch(E){return{ok:!1,error:String(E)}}}};if(z=async(E,j)=>{let K=S();await Zo(K,m,h,f,g,E,A,Z,E.peerKey,L,El({sendTg:j},{surface:"telegram"}),{surface:"telegram"})},i){let E=await Wa(a,()=>S(),z,{decorate:c});b.sendText=E.sendText,b.sendMedia=E.sendMedia,b.stop=E.stop}xs({getCfg:()=>S(),getWaOutbound:()=>y,getTgSendMedia:()=>b.sendMedia,getTgSendText:()=>b.sendText});let ce=null;{let E=S();if(E.webhookEnabled){let j=E.webhookToken||_v.randomBytes(32).toString("hex");E.webhookToken||O({webhookToken:j}),ce=Cs({port:E.webhookPort,host:E.webhookHost,token:j},{sendToPeer:R,getDefaultPeerKey:()=>{let J=S();return J.allowFrom.length>0?`wa:${Bt(J.allowFrom[0])}`:J.telegramAllowFrom.length>0?`tg:${J.telegramAllowFrom[0]}`:null}}).stop}}e=Ws({getRunningVersion:ot,getConfig:S,log:P});let ve=ds({getConfig:S,sendToPeer:R,sendMediaToPeer:$}),H=Cr({getConfig:S,sendToPeer:R}),he=!i,ee=()=>{M=!0,ve(),H(),e?.(),e=null,ce?.(),Wh(),ro(),b.stop?.().catch(()=>{}),k?.dispose(),m.killAllRunning(),Cn().stopAll().catch(()=>{}),console.error(`
427
- ${T(process.stderr,"shutting down\u2026")}`),process.exit(0)};if(process.on("SIGINT",ee),process.on("SIGTERM",ee),s)for(;!M;){let E=!1,j;try{j=await fs({printQr:!1,verbose:An()}),await Sn(gs(j),3e5,"Gateway: timed out waiting for WhatsApp connection (5 min).")}catch(J){console.error(T(process.stderr,`connect failed: ${String(J)}`)),await new Promise(C=>setTimeout(C,5e3));continue}y=up(j,{decorate:c});let K=rp(j,async J=>{let C=S(),x=J.fromE164||te(J.fromJid)||"",Y=td(J),oe=`wa:${x}`;await Zo(C,m,h,f,g,Y,A,Z,oe,L,El({sendWaText:(ye,ne)=>y.sendText(ye,ne),sendWaMedia:(ye,ne)=>y.sendMedia(ye,ne)},{surface:"whatsapp",waJid:J.fromJid}),{surface:"whatsapp"})});if(await new Promise(J=>{let C=x=>{x.connection==="close"&&(Na(x.lastDisconnect)===vn.loggedOut&&(E=!0),j.ev.off("connection.update",C),J())};j.ev.on("connection.update",C)}),K(),he&&(A.dispose(),A=N(),k=A),eo(j),y=null,E&&(console.error(T(process.stderr,"session logged out. Run `omnish link` again.")),ve(),e?.(),e=null,Wh(),ro(),b.stop?.().catch(()=>{}),process.exit(1)),M)break;await new Promise(J=>setTimeout(J,3e3))}else for(;!M;)await new Promise(E=>setTimeout(E,500));ve(),e?.(),ro(),b.stop?.().catch(()=>{}),A.dispose()}function Kv(e){let t=!1,n="0.0.0.0",o=3789,r;for(let s=0;s<e.length;s++){let i=e[s];if(i==="--help"||i==="-h"){t=!0;continue}if(i==="--host"||i==="-H"){let l=e[++s];l||(console.error(T(process.stderr,"--host requires an address (e.g. 127.0.0.1).")),process.exit(1)),n=l;continue}if(i==="--port"||i==="-p"){let l=e[++s],d=Number.parseInt(l??"",10);Number.isFinite(d)||(console.error(T(process.stderr,"--port requires a number.")),process.exit(1)),o=d;continue}if(i==="--token"||i==="-t"){let l=e[++s];(!l||l.startsWith("-"))&&(console.error(T(process.stderr,"--token requires a secret string.")),process.exit(1)),r=l;continue}let a=process.stderr;console.error(T(a,`unknown ui argument: ${i}`)),console.error(T(a,"Try: omnish ui --help")),process.exit(1)}return{help:t,host:n,port:o,token:r}}function Gh(){let e=process.stdout,t=[`${Ce(e,"omnish ui")} ${w(e,"[options]")}`,Q(e,"Serve the browser setup panel on your LAN (token-gated). Same trust as editing config on disk."),"",q(e,"Usage:"),` ${v(e,"omnish ui [options]")}`,"",q(e,"Options:"),...$t(e," ",[{left:"--host <addr>",right:"Bind address (default 0.0.0.0 \u2014 reachable on LAN). Use 127.0.0.1 for loopback only."},{left:"--port <n>",right:"TCP port (default 3789)."},{left:"--token <secret>",right:`Set or rotate setup token (saved to ${ht}).`},{left:"-h, --help",right:"This help."}],n=>w(e,n)),"",`${w(e,"Warning:")} ${we(e,"Listening on all interfaces exposes a remote control panel on your network \u2014 use a trusted LAN.")}`,""];console.log(t.join(`
428
- `))}async function Yv(){let[,,e,...t]=process.argv;if(e==="--version"||e==="-v"||e==="-V"){let n=process.stdout;console.log(`${Ce(n,"omnish")} ${w(n,ot())}`);return}if(e==="--help"||e==="-h"){Yl();return}if(e==="help"){Dv(t[0]);return}switch(se(),e){case"link":{let n=Wv(t);if(n.kind==="help"){Bh();return}if(n.kind==="error"){let o=process.stderr,r=n.message.replace(/^\[omnish\]\s*/,"");console.error(T(o,r)),process.exitCode=1;return}if(n.kind==="tg"){let o=n.token.trim(),r=process.stderr,s=process.stdout;if(!gt(o)){console.error(T(r,"That does not look like a Telegram bot token (expect digits:secret from @BotFather).")),process.exitCode=1;return}Ht(o);let i=lt()?"both":"telegram";gr(i),console.log([`${me(s,"Telegram")} ${v(s,"bot token saved to")} ${w(s,_)}`,`${w(s,"gatewayMode:")} ${me(s,i)}`,"",v(s,"Next:"),` ${w(s,"1.")} ${v(s,"Find your numeric user id (e.g. t.me/userinfobot), then:")} ${me(s,"omnish allow tg:<id>")}`,` ${w(s,"2.")} ${me(s,"omnish run")}`,""].join(`
429
- `));return}await yp({verbose:An(),force:n.force});return}case"run":{let n=Uv(t);if(n.help){Uh();return}if(n.verbose&&wi(!0),n.background){Bv(n.logFile,n.verbose);return}await zv();return}case"stop":Hv();return;case"logout":{try{cn.rmSync(le,{recursive:!0,force:!0}),console.log(D(process.stdout,"Session removed. Run `omnish link` to pair again."))}catch(n){console.error(T(process.stderr,`logout failed: ${String(n)}`)),process.exitCode=1}return}case"allow":{let n=t[0],o=process.stdout,r=process.stderr;if(!n){console.error(T(r,"Usage: omnish allow +<E164> or omnish allow tg:<user_id>")),process.exitCode=1;return}try{let s=hr(n);console.log(`${w(o,"allowFrom:")} ${v(o,s.allowFrom.join(", ")||"(empty)")}`),console.log(`${w(o,"telegramAllowFrom:")} ${v(o,s.telegramAllowFrom.join(", ")||"(empty)")}`)}catch(s){console.error(T(r,String(s).replace(/^\[omnish\]\s*/,""))),process.exitCode=1}return}case"deny":{let n=t[0],o=process.stdout,r=process.stderr;if(!n){console.error(T(r,"Usage: omnish deny +<E164> or omnish deny tg:<user_id>")),process.exitCode=1;return}try{let s=fr(n);console.log(`${w(o,"allowFrom:")} ${v(o,s.allowFrom.join(", ")||"(empty)")}`),console.log(`${w(o,"telegramAllowFrom:")} ${v(o,s.telegramAllowFrom.join(", ")||"(empty)")}`)}catch(s){console.error(T(r,String(s).replace(/^\[omnish\]\s*/,""))),process.exitCode=1}return}case"i":case"interactive":{await Nh(t);return}case"status":{let n=process.stdout,o=S(),r=t.includes("--check-updates"),s=new Ft().list(),i=s.filter(f=>f.status==="running").length,a=Me(o),l=At(o),d=o.gatewayMode==="whatsapp"||o.gatewayMode==="both",u=o.gatewayMode==="telegram"||o.gatewayMode==="both",c=lt(),m=c?hp(le):null,h=[];if(h.push(`${Ce(n,"omnish")} ${w(n,ot())}`,`${w(n,"gatewayMode:")} ${v(n,o.gatewayMode)}`,`${w(n,"data dir:")} ${v(n,Kl.dirname(le))}`,"",`${w(n,"gateway process:")} ${v(n,Gv().replace(/^gateway process: /,""))}`,"",Yt(n),me(n,"whatsapp"),` ${w(n,"in use:")} ${d?v(n,"yes"):we(n,"no (gatewayMode is telegram-only)")}`),d){let f=c?v(n,`linked (${le})`):we(n,"missing \u2014 run omnish link");h.push(` ${w(n,"session:")} ${f}`),c&&m&&h.push(` ${w(n,"linked as:")} ${v(n,m)}`),c&&!m&&h.push(` ${w(n,"linked as:")} ${Q(n,"(not in creds yet \u2014 try again after omnish link completes)")}`)}if(h.push(` ${q(n,"Allowed")}`),o.allowFrom.length===0)h.push(` ${Q(n,"(none)")}`);else for(let f of o.allowFrom)h.push(` ${w(n,"whatsapp:")} ${v(n,f)}`);if(h.push("",Yt(n),me(n,"telegram")),h.push(` ${w(n,"in use:")} ${u?v(n,"yes"):we(n,"no (gatewayMode is whatsapp-only)")}`),u){let f=a?v(n,jv(a)):we(n,"(none) \u2014 omnish link --tg <token> or TELEGRAM_BOT_TOKEN");h.push(` ${w(n,"bot token:")} ${f}`)}if(h.push(` ${q(n,"Allowed")}`),o.telegramAllowFrom.length===0)h.push(` ${Q(n,"(none)")}`);else for(let f of o.telegramAllowFrom)h.push(` ${w(n,"telegram:")} ${v(n,f)}`);if(h.push("",Yt(n),...await oh(n)),h.push("",Yt(n),`${me(n,"jobs")} ${w(n,`(recent): ${s.length} total, ${i} running`)}`,nu(l,n)),console.log(h.join(`
430
- `)),r){let f=await qo(ot(),o),g=Co(f);console.log(""),console.log(Yt(n)),console.log(Te(g,"whatsapp").text)}if(o.clusterEnabled){let f=nt(),g=be(),y=Object.keys(g.senderBindings).length,b=Object.keys(o.clusterSenderBindings??{}).length;console.log(""),console.log(Yt(n)),console.log(me(n,"cluster")),console.log(` ${w(n,"\xB7")} ${v(n,`enabled \xB7 label ${o.clusterLabel||Dh.hostname()} \xB7 bindings ${y} chat / ${b} default`)}`),console.log(` ${w(n,"node:")} ${v(n,`${f.slice(0,8)}\u2026`)}`)}return}case"commands":{let n=process.stdout,o=S();console.log(Qc(_n(o),n)),console.log(""),console.log(Yt(n)),console.log(v(n,"Keys editable from chat via /config set (same trust as shell):")),console.log(w(n,Vt.join(", "))),console.log(`${w(n,"See also:")} ${v(n,_)}`);return}case"cluster":{let n=process.stdout,o=process.stderr,r=(t[0]??"status").toLowerCase();if(r==="status"){let s=S(),i=nt();if(console.log(`${w(n,"clusterEnabled:")} ${v(n,String(s.clusterEnabled))}`),console.log(`${w(n,"clusterLabel:")} ${v(n,s.clusterLabel||"(hostname)")}`),console.log(`${w(n,"clusterRole:")} ${v(n,`${s.clusterRole} (informational; no longer used to gate traffic)`)}`),console.log(`${w(n,"node id:")} ${me(n,i)}`),s.clusterEnabled){let a=be(),l=sa(s,null);console.log(""),console.log(v(n,l.wa.replace(/\*([^*]+)\*/g,"$1").replace(/`([^`]+)`/g,"$1"))),console.log(""),console.log(ra(a,s,null));let d=Object.keys(a.senderBindings).length,u=Object.entries(s.clusterSenderBindings??{});if(d>0){console.log(""),console.log(me(n,"Chat bindings (cluster-local.json)"));for(let[c,m]of Object.entries(a.senderBindings))console.log(` ${w(n,c)} ${Q(n,"->")} ${v(n,`${m.nodeId} (${m.source}, since ${m.sinceIso})`)}`)}if(u.length>0){console.log(""),console.log(me(n,"Config defaults (clusterSenderBindings)"));for(let[c,m]of u)console.log(` ${w(n,c)} ${Q(n,"->")} ${v(n,m)}`)}}else console.log(""),console.log(we(n,"(cluster disabled \u2014 /config set clusterEnabled true to enable, then /c use <label-or-id> from each sender)"));return}if(r==="use"||r==="bind"){let s=t[1],i=t.slice(2).join(" ").trim();if(!s||!i){console.error(T(o,"Usage: omnish cluster use <senderE164|tg:id> <label-or-id>")),process.exitCode=1;return}let a=Jv(s);if(!a){console.error(T(o,`Could not parse sender "${s}". Use +E164 (WhatsApp) or tg:<user_id> (Telegram).`)),process.exitCode=1;return}let{state:l,resolved:d}=ed(a,i);if(!d.ok){if(d.reason==="ambiguous-label"){let c=(d.matches??[]).map(m=>`${m.nodeId}(${m.label})`).join(", ");console.error(T(o,`Label "${i}" matches multiple machines: ${c}. Use the 8-character id.`))}else console.error(T(o,`No machine matches "${i}". Run /c status from the chat first to populate the roster, or pass an 8-character node id.`));process.exitCode=1;return}console.log(`${me(n,"cluster:")} ${v(n,`${a} -> ${d.peer.nodeId} (${d.peer.label}).`)}`);let u=S();console.log(""),console.log(ra(l,u,a));return}if(r==="here"){console.error(T(o,"omnish cluster here is no longer available. Use: omnish cluster use <senderE164|tg:id> <label-or-id>")),console.error(T(o,"Or send /c here from the controller's chat on the machine you want to bind.")),process.exitCode=1;return}console.error(T(o,"Usage: omnish cluster [status | use <sender> <label-or-id>]")),process.exitCode=1;return}case"security":{let n=S(),o=At(n),r=t.includes("--json");console.log(r?eu(o):Fi(o,process.stdout)),xo(o)&&(process.exitCode=1);return}case"service":{qv(t);return}case"pull":{await ud(t);return}case"pull-exec":{await dd(t);return}case"config":{await nh(t);return}case"tunnel":{await Dp(t);return}case"platform":{await gh(t);return}case"docs":{_h(t);return}case"ui":{let n=Kv(t);if(n.help){Gh();return}if(!Number.isFinite(n.port)||n.port<1||n.port>65535){console.error(T(process.stderr,"port must be between 1 and 65535.")),process.exitCode=1;return}let o=vh(n.token);await Ph({host:n.host,port:n.port,meta:o});let r=process.stdout,s=Eh(n.port);console.log(""),console.log(`${Ce(r,"ui")} ${w(r,"listening")}`),console.log(`${w(r,"bind:")} ${v(r,`${n.host}:${n.port}`)}`),console.log(`${w(r,"setup token:")} ${v(r,o.token)}`),console.log(`${w(r,"token file:")} ${Q(r,ht)}`),console.log(""),console.log(we(r,"Anyone on your network who can reach this port needs the token \u2014 do not expose to untrusted Wi\u2011Fi.")),console.log(""),console.log(`${w(r,"Open:")}`),console.log(` ${v(r,`http://127.0.0.1:${n.port}/`)}`);for(let i of s)console.log(` ${v(r,i)}`);console.log(""),console.log(`${w(r,"Quick link (same Wi\u2011Fi):")} ${v(r,`http://127.0.0.1:${n.port}/?token=${encodeURIComponent(o.token)}`)}`),console.log("");return}default:Yl(),e&&(process.exitCode=1)}}Yv().catch(e=>{console.error(T(process.stderr,String(e))),process.exit(1)});
438
+ Show: omnish docs show <n>`);return}if(t==="show"){if(!n){console.error("[omnish] Usage: omnish docs show <n> | <doc-path>"),process.exitCode=1;return}if(/^\d+$/.test(n)){let r=Lh(n);if(!r){console.error("[omnish] No result #"+n+". Run omnish docs search first."),process.exitCode=1;return}let s=ur(r.id);if(!s){console.error("[omnish] Entry missing from index."),process.exitCode=1;return}Cf(s,Number.parseInt(n,10));return}let o=bi(n);if(!o){console.error(`[omnish] No doc at path "${n}".`),process.exitCode=1;return}Cf(o);return}console.error(`[omnish] Unknown docs subcommand "${t}". Try: omnish docs help`),process.exitCode=1}function Cf(e,t){if(!e)return;let n=gi(mn.repoUrl,e.path),o=t!==void 0?`${t}. ${e.title}`:e.title;if(console.log(o),console.log(e.path),console.log(n),console.log(""),console.log(ki(e)),console.log(""),e.relatedCommands.length){console.log("Try:");for(let r of e.relatedCommands.slice(0,8))console.log(` ${r}`)}}JS.setDefaultResultOrder("ipv4first");function Mf(){let e=process.stdout,t=[`${Re(e,"omnish run")} ${w(e,"[options]")}`,X(e,"Listen for DMs and run shell commands from allowlisted chats."),"",z(e,"Usage:"),` ${v(e,"omnish run [options]")}`,"",z(e,"Options:"),...Et(e," ",[{left:"-d, --background",right:"Start the gateway detached; log to --log-file (default: <data>/logs/gateway.log)."},{left:"--log-file <path>",right:`Append stdout/stderr when background (default: ${Be}).`},{left:"-vb, --verbose",right:"Baileys/gateway debug logs on stderr (legacy: OMNISH_VERBOSE=1)."},{left:"-h, --help",right:"Show this help."}],o=>w(e,o)),"",`${v(e,"Config reload:")} ${w(e,"while the gateway runs, edit config then send /reload or /restart from an allowlisted chat (no restart needed for many keys). /updates checks npm (and optional updateInfoUrl).")}`,""],n=re();n?t.push(z(e,"Attached mode (platform credentials detected):"),` ${w(e,"url:")} ${v(e,n.platformUrl)} ${X(e,`[${to()}]`)}`,` ${w(e,"token:")} ${v(e,vn("platformToken",n.token))} ${X(e,`[${eo()}]`)}`,w(e," Messengers run on the platform; allowlist is set on the dashboard. Run: omnish platform probe"),""):t.push(w(e,"Platform attached mode: omnish config add platform_url <url> platform_token <token>"),w(e," then omnish platform probe && omnish run \u2014 see omnish help platform"),""),console.log(t.join(`
439
+ `))}function Pf(){let e=process.stdout,t=[{left:"omnish link [--force]",right:"WhatsApp: scan QR (Linked devices). --force wipes session first."},{left:"omnish link --tg <bot_token>",right:"Telegram: save token to config; gatewayMode telegram or both if WhatsApp is linked."}],n=t.map(i=>w(e,i.left)),o=Math.max(...n.map(_r)),r=t.map((i,a)=>Qi(" ",o,n[a],v(e,i.right))),s=[`${Re(e,"omnish link")} ${w(e,"[options]")}`,X(e,"Connect WhatsApp (QR) or save a Telegram bot token."),"",z(e,"Usage:"),` ${v(e,"omnish link [--force]")}`,` ${v(e,"omnish link --tg <bot_token>")}`,"",z(e,"Modes:"),...r,` ${X(e,"Do not combine --tg with --force.")}`,"",z(e,"Options:"),...Et(e," ",[{left:"-f, --force",right:"WhatsApp only: delete saved session before pairing."},{left:"-vb, --verbose",right:"Baileys debug logs on stderr during pairing (legacy: OMNISH_VERBOSE=1)."},{left:"-h, --help",right:"Show this help."}],i=>w(e,i)),"",`${v(e,"Next:")} ${ye(e,"omnish allow tg:<your_user_id>")} ${w(e,"then")} ${ye(e,"omnish run")}`,`${w(e,"Config:")} ${v(e,_)}`,""];re()&&s.push(z(e,"Platform attached mode:"),w(e," omnish link on this host is for standalone only. Link WhatsApp on the platform dashboard or:"),` ${v(e,"omnish platform import-whatsapp")} ${w(e,"(after a local omnish link, with omnish run stopped)")}`,w(e," Telegram: set bot token on the platform dashboard, not --tg here."),""),console.log(s.join(`
440
+ `))}function zS(e){let t=!1,n=null;for(let o=0;o<e.length;o++){let r=e[o]??"";if(r==="--help"||r==="-h")return{kind:"help"};if(r==="--force"||r==="-f"){t=!0;continue}if(r==="-vb"||r==="--verbose"){Wi(!0);continue}if(r==="--tg"||r==="--telegram"){let s=e[o+1];if(!s||s.startsWith("-"))return{kind:"error",message:"[omnish] --tg requires a bot token (from @BotFather)."};n=s,o++;continue}if(r.startsWith("--tg=")||r.startsWith("--telegram=")){let s=r.indexOf("="),i=r.slice(s+1).trim();if(!i)return{kind:"error",message:"[omnish] --tg= requires a non-empty bot token."};n=i;continue}return{kind:"error",message:`[omnish] unknown link argument: ${r}
441
+ Try: omnish link --help`}}return n!==null?t?{kind:"error",message:"[omnish] --force applies to WhatsApp only; do not combine with --tg."}:{kind:"tg",token:n}:{kind:"wa",force:t}}function wc(){let e=process.stdout,t=`${Re(e,"omnish")} ${w(e,`v${rt()}`)}`,n=[{left:"link [--force] [--tg <token>]",right:"WhatsApp (QR) or Telegram bot token \u2014 omnish link --help"},{left:"run [options]",right:"Listen for DMs (WhatsApp and/or Telegram \u2014 see gatewayMode in config)"},{left:"stop",right:`Stop background gateway (pidfile: ${ge})`},{left:"service <subcommand>",right:"Boot install hints, logs, systemd/LaunchAgent \u2014 omnish service help"},{left:"pull <subcommand>",right:"Media URL tools (yt-dlp, ffmpeg, Whisper) \u2014 omnish pull help"},{left:"logout",right:"Delete saved WhatsApp session"},{left:"allow +<E164> | tg:<id>",right:"Add allowlist entry"},{left:"deny +<E164> | tg:<id>",right:"Remove allowlist entry"},{left:"status [--check-updates]",right:"Channels, identity, allowlists, jobs, security, cluster (if enabled)"},{left:"commands",right:"Chat commands for allowlisted users (same as /help)"},{left:"security [--json]",right:"Configuration security report (JSON for scripts)"},{left:"cluster [status | use <sender> <label-or-id>]",right:"Per-sender machine bindings"},{left:"i | interactive [options]",right:"Local REPL (chat commands; /sendto needs omnish run)"},{left:"ui [options]",right:"Browser setup UI on LAN (token-gated) \u2014 omnish ui --help"},{left:"config <add|show|edit|delete>",right:"Manage config.json (platform URL/token, tunnel, gateway) \u2014 omnish config help"},{left:"platform <subcommand>",right:"Attached mode: configure, probe, import WA \u2014 omnish help platform"},{left:"tunnel <subcommand>",right:"Expose local HTTP/TCP via omnish relay \u2014 omnish tunnel help"},{left:"docs <subcommand>",right:"Search bundled guides offline \u2014 omnish docs help (same index as /docs in chat)"}],o=[{left:"-v, --version",right:"Print version and exit."},{left:"-h, --help",right:"Show this help (same as omnish help)."}],r=[t,X(e,"Allowlisted inbox \u2192 your real shell. No AI."),"",z(e,"Usage:"),` ${v(e,"omnish [options] <command> [args...]")}`,"",z(e,"Options:"),...Et(e," ",o,s=>w(e,s)),"",z(e,"Commands:"),...Et(e," ",n,s=>w(e,s)),"",`${w(e,"Config:")} ${v(e,`${_} \u2014 gatewayMode: "whatsapp" | "telegram" | "both"`)}`,`${w(e,"Platform:")} ${v(e,"platform_url + platform_token \u2192 attached omnish run (omnish help platform)")}`,`${w(e,"Verbose:")} ${v(e,"omnish run --verbose (legacy: OMNISH_VERBOSE=1, WHATSVERBOSE=1)")}`,`${w(e,"Env:")} ${v(e,"TELEGRAM_BOT_TOKEN (optional override)")}`,`${w(e,"Data:")} ${v(e,"~/.omnish by default; ~/.whatslive reused if it already exists. OMNISH_HOME overrides.")}`,`${w(e,"See also:")} ${v(e,"https://omnish.dev")}`,""];console.log(r.join(`
442
+ `))}function KS(e){let t=(e??"").trim().toLowerCase(),n=process.stdout;if(!t){wc();return}switch(t){case"link":Pf();return;case"run":Mf();return;case"service":Af();return;case"pull":Ha();return;case"i":case"interactive":fc(n);return;case"ui":If();return;case"config":Ri(n);return;case"platform":Pi(n);return;case"docs":gc();return;default:console.error(C(process.stderr,`No detailed help for "${e}". Try: omnish help`)),process.exitCode=1}}function YS(e){let t=!1,n="",o=!1,r=!1;for(let i=0;i<e.length;i++){let a=e[i]??"";if(a==="-d"||a==="--background")t=!0;else if(a==="-vb"||a==="--verbose")r=!0;else if(a==="--log-file"||a==="--log"){let l=e[++i];l||(console.error(C(process.stderr,"--log-file requires a path.")),process.exit(1)),n=l}else if(a==="--help"||a==="-h")o=!0;else{let l=process.stderr;console.error(C(l,`unknown run option: ${a}`)),console.error(C(l,"Try: omnish run --help")),process.exit(1)}}let s=n.trim()!==""?yc.isAbsolute(n)?n:yc.resolve(process.cwd(),n):Be;return{background:t,logFile:s,help:o,verbose:r}}function QS(e,t){let n=Ds(e,{verbose:t});n.ok||(console.error(C(process.stderr,n.message)),process.exit(1));let o=process.stdout;console.log(`${U(o,`gateway started in background (pid ${n.pid}).`)} ${w(o,`Log: ${e}`)}`)}function VS(){let e=Us();switch(e.outcome){case"no_pidfile":console.error(C(process.stderr,`no pidfile at ${ge} \u2014 is a background gateway running?`)),process.exitCode=1;return;case"invalid_pidfile":console.error(C(process.stderr,"invalid pidfile.")),process.exitCode=1;return;case"stale_cleaned":console.log(U(process.stdout,`process ${e.pid} is not running; removing stale pidfile.`));return;case"sent_signal":console.log(U(process.stdout,`sent SIGTERM to gateway (pid ${e.pid}).`));return;case"taskkill_ok":console.log(U(process.stdout,`stopped gateway (pid ${e.pid}) using taskkill.`));return;case"failed":console.error(C(process.stderr,e.message)),process.exitCode=1;return}}function Tf(){if(process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{gn.readFileSync(ge,"utf8").trim()===String(process.pid)&&gn.unlinkSync(ge)}catch{}}function XS(e){return e.length<=8?"(set)":`${e.slice(0,4)}\u2026${e.slice(-4)}`}function ZS(){if(!gn.existsSync(ge))return"gateway process: not running (no pid file)";let e=gn.readFileSync(ge,"utf8").trim(),t=Number(e);if(!Number.isFinite(t)||t<=0)return"gateway process: invalid pid file";try{return process.kill(t,0),`gateway process: running (pid ${t})`}catch{return`gateway process: not running (stale pid ${t} in pid file)`}}var Ef=120;function Af(){let e=process.stdout,t=[{left:"help",right:"This help."},{left:"instructions",right:"Copy-paste install steps for this machine."},{left:"status",right:"Data dir, pidfile, Node + entry script."},{left:"logs [n]",right:`Tail default gateway log (default 80 lines, max ${Ef}).`},{left:"install",right:"Write user systemd / LaunchAgent unit (needs serviceInstallFromChat=true)."},{left:"uninstall",right:"Remove that unit (same gate as install)."}],n=[`${Re(e,"omnish service")} ${w(e,"<subcommand>")}`,X(e,"Boot integration, crash restart policy, and logs (same ideas as /service in chat)."),"",z(e,"Usage:"),` ${v(e,"omnish service <subcommand>")}`,"",z(e,"Subcommands:"),...Et(e," ",t,o=>w(e,o)),"",z(e,"Restart and reload:"),` ${v(e,"Process")} ${w(e,"\u2014 Linux user unit: Restart=on-failure, RestartSec=5. macOS: KeepAlive. Full restart loads config from disk.")}`,` ${v(e,"Config live")} ${w(e,"\u2014 allowlisted chat while gateway runs: /reload or /restart")}`,"",`${w(e,"Docs:")} ${v(e,"docs/guides/background-and-boot.md")} ${X(e,"\xB7")} https://omnish.dev`,""];console.log(n.join(`
443
+ `))}function ex(e){let t=process.stdout,n=process.stderr,o=(e[0]??"help").toLowerCase();if(o==="help"||o==="--help"||o==="-h"){Af();return}if(o==="instructions"){let r=Wt();if(r.error){console.error(C(n,r.error)),process.exitCode=1;return}console.log(qr(r));return}if(o==="status"){let r=Wt(),s=(()=>{try{return gn.existsSync(ge)?`gateway.pid: ${gn.readFileSync(ge,"utf8").trim()}`:"gateway.pid: (missing)"}catch(u){return`gateway.pid: (read error: ${String(u)})`}})(),i=process.env.OMNISH_BACKGROUND_GATEWAY==="1"?"This process: background gateway (OMNISH_BACKGROUND_GATEWAY=1).":"This process: CLI (not the gateway \u2014 run omnish service status on the host where the gateway runs for live pid info).",a=typeof process.env.OMNISH_HOME=="string"&&process.env.OMNISH_HOME.trim()?`OMNISH_HOME env: ${process.env.OMNISH_HOME.trim()}`:"OMNISH_HOME env: (not set \u2014 using default data dir)",l=r.error?r.error:`Node: ${r.nodePath}
444
+ Script: ${r.scriptPath}`,d=S().serviceInstallFromChat?"Install from CLI/chat: enabled (omnish service install / /service install).":"Install from CLI/chat: off \u2014 set serviceInstallFromChat true in config (same trust as shell).";console.log(Re(t,"omnish service status")),console.log(""),console.log(`${w(t,"platform:")} ${v(t,process.platform)}`),console.log(`${w(t,"session:")} ${v(t,i)}`),console.log(`${w(t,"env:")} ${v(t,a)}`),console.log(`${w(t,"data dir:")} ${v(t,D)}`),console.log(`${w(t,"pidfile:")} ${v(t,s)}`),console.log(`${w(t,"default log:")} ${v(t,Be)}`),console.log(""),console.log(l),console.log(""),console.log(v(t,d));return}if(o==="logs"){let r=e.length>=2?Number.parseInt(e[1],10):80,s=Number.isFinite(r)&&r>0?Math.min(r,Ef):80,i=cs(Be,s);console.log(`${w(t,"file:")} ${v(t,Be)}`),console.log(`${w(t,"lines:")} ${v(t,String(s))}`),console.log(""),console.log(i);return}if(o==="install"){if(!S().serviceInstallFromChat){console.error(C(n,"Install is disabled. Set serviceInstallFromChat to true in config (same trust as shell), then run again.")),process.exitCode=1;return}let s=is();s.ok?console.log(U(t,s.detail)):(console.error(C(n,s.detail)),process.exitCode=1);return}if(o==="uninstall"){if(!S().serviceInstallFromChat){console.error(C(n,"Uninstall is disabled. Set serviceInstallFromChat to true in config or remove the unit file on the host manually.")),process.exitCode=1;return}let s=as();s.ok?console.log(U(t,s.detail)):(console.error(C(n,s.detail)),process.exitCode=1);return}console.error(C(n,`Unknown subcommand "${o}". Try: omnish service help`)),process.exitCode=1}function tx(e){let t=e.trim();if(!t)return null;let n=t.toLowerCase();if(n.startsWith("tg:")||n.startsWith("telegram:")){let s=Ne(t);return s?`tg:${s}`:null}let o=n.startsWith("wa:")?t.slice(3):t,r=ne(o);return r?`wa:${r}`:null}async function nx(){let e=null,t=xi();t.ok||(console.error(C(process.stderr,t.message)),process.exit(1));let n=re();if(n){await _h(n);return}let o=S(),r=o.gatewayMode,s=r==="whatsapp"||r==="both",i=r==="telegram"||r==="both",a=Me(o),l=Ot(o),c=process.stderr,d=Su(l,"warn");if(d.length>0&&(console.warn(`${U(c,`Security (${d.length} finding(s)):`)}
445
+ `),console.warn(ta(d,c))),process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{gn.writeFileSync(ge,`${process.pid}
446
+ `,{mode:384})}catch(P){M.warn({err:String(P)},"could not write gateway pidfile")}let u=(P,G)=>{let Q=S();if(!Q.clusterEnabled)return P;let J=ot(),le=(Q.clusterLabel??"").trim()||$f.hostname(),K=null;if(G.startsWith("tg:"))K=G;else if(G){let $=ne(G);$&&(K=`wa:${$}`)}let _e=K?Nt(Q,K):null;return ud(P,{nodeId:J,label:le,role:Q.clusterRole,activeNodeId:_e?.nodeId??""})},m=new Map,h=new Map,f=new Map,g=null,y={stop:null,sendText:null,sendMedia:null},b=null,k=!1,R=async(P,G)=>{if(P.startsWith("wa:")){let Q=P.slice(3);g&&await g.sendText(Q,G)}else if(P.startsWith("tg:")){let Q=Number(P.slice(3));y.sendText&&Number.isFinite(Q)&&await y.sendText(Q,p(G))}},T=new Dt({onJobExit(P){P.notifyPeerKey&&R(P.notifyPeerKey,Is(P))}}),L={onPlainTextLlmFallback(P,G){uo(S(),P,G,Q=>R(P,Q))},sendToPeer:R},x=async(P,G)=>{if(P.startsWith("wa:")){let Q=P.slice(3);g&&await g.sendMedia(Q,G)}else if(P.startsWith("tg:")){let Q=Number(P.slice(3));y.sendMedia&&Number.isFinite(Q)&&await y.sendMedia(Q,G)}},O=()=>new cn(()=>S(),R),A=O();b=A;let Y,te={async reload(){try{M.info("gateway reload requested from chat"),await y.stop?.().catch(()=>{}),y.stop=null,y.sendText=null,y.sendMedia=null;let P=S(),G=P.gatewayMode==="telegram"||P.gatewayMode==="both",Q=Me(P);if(G&&Q){let K=await ul(Q,()=>S(),Y,{decorate:u});y.sendText=K.sendText,y.sendMedia=K.sendMedia,y.stop=K.stop}let J=["Reload complete.",`gatewayMode: ${P.gatewayMode}`,G&&Q?"Telegram bot is running with the current token.":"Telegram bot is stopped (enable telegram/both + token if you want it).","Allowlists, shell, app session limits, and timeouts are read from disk on each command."].join(`
447
+ `),le=ti(nr());return{ok:!0,summary:le?`${J}
448
+
449
+ Updates (last check): ${le}`:J}}catch(P){return{ok:!1,error:String(P)}}}};if(Y=async(P,G)=>{let Q=S();await dr(Q,T,m,h,f,P,A,te,P.peerKey,L,Zl({sendTg:G},{surface:"telegram"}),{surface:"telegram"})},i){let P=await ul(a,()=>S(),Y,{decorate:u});y.sendText=P.sendText,y.sendMedia=P.sendMedia,y.stop=P.stop}Bs({getCfg:()=>S(),getWaOutbound:()=>g,getTgSendMedia:()=>y.sendMedia,getTgSendText:()=>y.sendText});let ue=null;{let P=S();if(P.webhookEnabled){let G=P.webhookToken||qS.randomBytes(32).toString("hex");P.webhookToken||N({webhookToken:G}),ue=Hs({port:P.webhookPort,host:P.webhookHost,token:G},{sendToPeer:R,getDefaultPeerKey:()=>{let J=S();return J.allowFrom.length>0?`wa:${Jt(J.allowFrom[0])}`:J.telegramAllowFrom.length>0?`tg:${J.telegramAllowFrom[0]}`:null}}).stop}}e=ni({getRunningVersion:rt,getConfig:S,log:M});let xe=Ps({getConfig:S,sendToPeer:R,sendMediaToPeer:x}),W=Nr({getConfig:S,sendToPeer:R}),fe=!i,V=()=>{k=!0,xe(),W(),e?.(),e=null,ue?.(),Tf(),wo(),y.stop?.().catch(()=>{}),b?.dispose(),T.killAllRunning(),Ln().stopAll().catch(()=>{}),console.error(`
450
+ ${C(process.stderr,"shutting down\u2026")}`),process.exit(0)};if(process.on("SIGINT",V),process.on("SIGTERM",V),s)for(;!k;){let P=!1,G;try{G=await Os({printQr:!1,verbose:Hn()}),await An(Ns(G),3e5,"Gateway: timed out waiting for WhatsApp connection (5 min).")}catch(J){console.error(C(process.stderr,`connect failed: ${String(J)}`)),await new Promise(le=>setTimeout(le,5e3));continue}g=Qp(G,{decorate:u});let Q=Gp(G,async J=>{let le=S(),K=J.fromE164||ne(J.fromJid)||"",_e=Cd(J),$=`wa:${K}`;await dr(le,T,m,h,f,_e,A,te,$,L,Zl({sendWaText:(E,j)=>g.sendText(E,j),sendWaMedia:(E,j)=>g.sendMedia(E,j)},{surface:"whatsapp",waJid:J.fromJid}),{surface:"whatsapp"})});if(await new Promise(J=>{let le=K=>{K.connection==="close"&&(al(K.lastDisconnect)===En.loggedOut&&(P=!0),G.ev.off("connection.update",le),J())};G.ev.on("connection.update",le)}),Q(),fe&&(A.dispose(),A=O(),b=A),fo(G),g=null,P&&(console.error(C(process.stderr,"session logged out. Run `omnish link` again.")),xe(),e?.(),e=null,Tf(),wo(),y.stop?.().catch(()=>{}),process.exit(1)),k)break;await new Promise(J=>setTimeout(J,3e3))}else for(;!k;)await new Promise(P=>setTimeout(P,500));xe(),e?.(),wo(),y.stop?.().catch(()=>{}),A.dispose()}function ox(e){let t=!1,n="0.0.0.0",o=3789,r;for(let s=0;s<e.length;s++){let i=e[s];if(i==="--help"||i==="-h"){t=!0;continue}if(i==="--host"||i==="-H"){let l=e[++s];l||(console.error(C(process.stderr,"--host requires an address (e.g. 127.0.0.1).")),process.exit(1)),n=l;continue}if(i==="--port"||i==="-p"){let l=e[++s],c=Number.parseInt(l??"",10);Number.isFinite(c)||(console.error(C(process.stderr,"--port requires a number.")),process.exit(1)),o=c;continue}if(i==="--token"||i==="-t"){let l=e[++s];(!l||l.startsWith("-"))&&(console.error(C(process.stderr,"--token requires a secret string.")),process.exit(1)),r=l;continue}let a=process.stderr;console.error(C(a,`unknown ui argument: ${i}`)),console.error(C(a,"Try: omnish ui --help")),process.exit(1)}return{help:t,host:n,port:o,token:r}}function If(){let e=process.stdout,t=[`${Re(e,"omnish ui")} ${w(e,"[options]")}`,X(e,"Serve the browser setup panel on your LAN (token-gated). Same trust as editing config on disk."),"",z(e,"Usage:"),` ${v(e,"omnish ui [options]")}`,"",z(e,"Options:"),...Et(e," ",[{left:"--host <addr>",right:"Bind address (default 0.0.0.0 \u2014 reachable on LAN). Use 127.0.0.1 for loopback only."},{left:"--port <n>",right:"TCP port (default 3789)."},{left:"--token <secret>",right:`Set or rotate setup token (saved to ${gt}).`},{left:"-h, --help",right:"This help."}],n=>w(e,n)),"",`${w(e,"Warning:")} ${ke(e,"Listening on all interfaces exposes a remote control panel on your network \u2014 use a trusted LAN.")}`,""];console.log(t.join(`
451
+ `))}async function rx(){let[,,e,...t]=process.argv;if(e==="--version"||e==="-v"||e==="-V"){let n=process.stdout;console.log(`${Re(n,"omnish")} ${w(n,rt())}`);return}if(e==="--help"||e==="-h"){wc();return}if(e==="help"){KS(t[0]);return}switch(ie(),e){case"link":{let n=zS(t);if(n.kind==="help"){Pf();return}if(n.kind==="error"){let o=process.stderr,r=n.message.replace(/^\[omnish\]\s*/,"");console.error(C(o,r)),process.exitCode=1;return}if(n.kind==="tg"){let o=n.token.trim(),r=process.stderr,s=process.stdout;if(!wt(o)){console.error(C(r,"That does not look like a Telegram bot token (expect digits:secret from @BotFather).")),process.exitCode=1;return}qt(o);let i=lt()?"both":"telegram";$r(i),console.log([`${ye(s,"Telegram")} ${v(s,"bot token saved to")} ${w(s,_)}`,`${w(s,"gatewayMode:")} ${ye(s,i)}`,"",v(s,"Next:"),` ${w(s,"1.")} ${v(s,"Find your numeric user id (e.g. t.me/userinfobot), then:")} ${ye(s,"omnish allow tg:<id>")}`,` ${w(s,"2.")} ${ye(s,"omnish run")}`,""].join(`
452
+ `));return}await om({verbose:Hn(),force:n.force});return}case"run":{let n=YS(t);if(n.help){Mf();return}if(n.verbose&&Wi(!0),n.background){QS(n.logFile,n.verbose);return}await nx();return}case"stop":VS();return;case"logout":{try{gn.rmSync(ce,{recursive:!0,force:!0}),console.log(U(process.stdout,"Session removed. Run `omnish link` to pair again."))}catch(n){console.error(C(process.stderr,`logout failed: ${String(n)}`)),process.exitCode=1}return}case"allow":{let n=t[0],o=process.stdout,r=process.stderr;if(!n){console.error(C(r,"Usage: omnish allow +<E164> or omnish allow tg:<user_id>")),process.exitCode=1;return}try{let s=Rr(n);console.log(`${w(o,"allowFrom:")} ${v(o,s.allowFrom.join(", ")||"(empty)")}`),console.log(`${w(o,"telegramAllowFrom:")} ${v(o,s.telegramAllowFrom.join(", ")||"(empty)")}`)}catch(s){console.error(C(r,String(s).replace(/^\[omnish\]\s*/,""))),process.exitCode=1}return}case"deny":{let n=t[0],o=process.stdout,r=process.stderr;if(!n){console.error(C(r,"Usage: omnish deny +<E164> or omnish deny tg:<user_id>")),process.exitCode=1;return}try{let s=Tr(n);console.log(`${w(o,"allowFrom:")} ${v(o,s.allowFrom.join(", ")||"(empty)")}`),console.log(`${w(o,"telegramAllowFrom:")} ${v(o,s.telegramAllowFrom.join(", ")||"(empty)")}`)}catch(s){console.error(C(r,String(s).replace(/^\[omnish\]\s*/,""))),process.exitCode=1}return}case"i":case"interactive":{await xf(t);return}case"status":{let n=process.stdout,o=S(),r=t.includes("--check-updates"),s=new Dt().list(),i=s.filter(f=>f.status==="running").length,a=Me(o),l=Ot(o),c=o.gatewayMode==="whatsapp"||o.gatewayMode==="both",d=o.gatewayMode==="telegram"||o.gatewayMode==="both",u=lt(),m=u?em(ce):null,h=[];if(h.push(`${Re(n,"omnish")} ${w(n,rt())}`,`${w(n,"gatewayMode:")} ${v(n,o.gatewayMode)}`,`${w(n,"data dir:")} ${v(n,yc.dirname(ce))}`,"",`${w(n,"gateway process:")} ${v(n,ZS().replace(/^gateway process: /,""))}`,"",Zt(n),ye(n,"whatsapp"),` ${w(n,"in use:")} ${c?v(n,"yes"):ke(n,"no (gatewayMode is telegram-only)")}`),c){let f=u?v(n,`linked (${ce})`):ke(n,"missing \u2014 run omnish link");h.push(` ${w(n,"session:")} ${f}`),u&&m&&h.push(` ${w(n,"linked as:")} ${v(n,m)}`),u&&!m&&h.push(` ${w(n,"linked as:")} ${X(n,"(not in creds yet \u2014 try again after omnish link completes)")}`)}if(h.push(` ${z(n,"Allowed")}`),o.allowFrom.length===0)h.push(` ${X(n,"(none)")}`);else for(let f of o.allowFrom)h.push(` ${w(n,"whatsapp:")} ${v(n,f)}`);if(h.push("",Zt(n),ye(n,"telegram")),h.push(` ${w(n,"in use:")} ${d?v(n,"yes"):ke(n,"no (gatewayMode is whatsapp-only)")}`),d){let f=a?v(n,XS(a)):ke(n,"(none) \u2014 omnish link --tg <token> or TELEGRAM_BOT_TOKEN");h.push(` ${w(n,"bot token:")} ${f}`)}if(h.push(` ${z(n,"Allowed")}`),o.telegramAllowFrom.length===0)h.push(` ${X(n,"(none)")}`);else for(let f of o.telegramAllowFrom)h.push(` ${w(n,"telegram:")} ${v(n,f)}`);if(h.push("",Zt(n),...await Gh(n)),h.push("",Zt(n),`${ye(n,"jobs")} ${w(n,`(recent): ${s.length} total, ${i} running`)}`,Ru(l,n)),console.log(h.join(`
453
+ `)),r){let f=await or(rt(),o),g=No(f);console.log(""),console.log(Zt(n)),console.log(me(g,"whatsapp").text)}if(o.clusterEnabled){let f=ot(),g=ve(),y=Object.keys(g.senderBindings).length,b=Object.keys(o.clusterSenderBindings??{}).length;console.log(""),console.log(Zt(n)),console.log(ye(n,"cluster")),console.log(` ${w(n,"\xB7")} ${v(n,`enabled \xB7 label ${o.clusterLabel||$f.hostname()} \xB7 bindings ${y} chat / ${b} default`)}`),console.log(` ${w(n,"node:")} ${v(n,`${f.slice(0,8)}\u2026`)}`)}return}case"commands":{let n=process.stdout,o=S();console.log(bu(Kn(o),n)),console.log(""),console.log(Zt(n)),console.log(v(n,"Keys editable from chat via /config set (same trust as shell):")),console.log(w(n,tn.join(", "))),console.log(`${w(n,"See also:")} ${v(n,_)}`);return}case"cluster":{let n=process.stdout,o=process.stderr,r=(t[0]??"status").toLowerCase();if(r==="status"){let s=S(),i=ot();if(console.log(`${w(n,"clusterEnabled:")} ${v(n,String(s.clusterEnabled))}`),console.log(`${w(n,"clusterLabel:")} ${v(n,s.clusterLabel||"(hostname)")}`),console.log(`${w(n,"clusterRole:")} ${v(n,`${s.clusterRole} (informational; no longer used to gate traffic)`)}`),console.log(`${w(n,"node id:")} ${ye(n,i)}`),s.clusterEnabled){let a=ve(),l=Ca(s,null);console.log(""),console.log(v(n,l.wa.replace(/\*([^*]+)\*/g,"$1").replace(/`([^`]+)`/g,"$1"))),console.log(""),console.log(xa(a,s,null));let c=Object.keys(a.senderBindings).length,d=Object.entries(s.clusterSenderBindings??{});if(c>0){console.log(""),console.log(ye(n,"Chat bindings (cluster-local.json)"));for(let[u,m]of Object.entries(a.senderBindings))console.log(` ${w(n,u)} ${X(n,"->")} ${v(n,`${m.nodeId} (${m.source}, since ${m.sinceIso})`)}`)}if(d.length>0){console.log(""),console.log(ye(n,"Config defaults (clusterSenderBindings)"));for(let[u,m]of d)console.log(` ${w(n,u)} ${X(n,"->")} ${v(n,m)}`)}}else console.log(""),console.log(ke(n,"(cluster disabled \u2014 /config set clusterEnabled true to enable, then /c use <label-or-id> from each sender)"));return}if(r==="use"||r==="bind"){let s=t[1],i=t.slice(2).join(" ").trim();if(!s||!i){console.error(C(o,"Usage: omnish cluster use <senderE164|tg:id> <label-or-id>")),process.exitCode=1;return}let a=tx(s);if(!a){console.error(C(o,`Could not parse sender "${s}". Use +E164 (WhatsApp) or tg:<user_id> (Telegram).`)),process.exitCode=1;return}let{state:l,resolved:c}=xd(a,i);if(!c.ok){if(c.reason==="ambiguous-label"){let u=(c.matches??[]).map(m=>`${m.nodeId}(${m.label})`).join(", ");console.error(C(o,`Label "${i}" matches multiple machines: ${u}. Use the 8-character id.`))}else console.error(C(o,`No machine matches "${i}". Run /c status from the chat first to populate the roster, or pass an 8-character node id.`));process.exitCode=1;return}console.log(`${ye(n,"cluster:")} ${v(n,`${a} -> ${c.peer.nodeId} (${c.peer.label}).`)}`);let d=S();console.log(""),console.log(xa(l,d,a));return}if(r==="here"){console.error(C(o,"omnish cluster here is no longer available. Use: omnish cluster use <senderE164|tg:id> <label-or-id>")),console.error(C(o,"Or send /c here from the controller's chat on the machine you want to bind.")),process.exitCode=1;return}console.error(C(o,"Usage: omnish cluster [status | use <sender> <label-or-id>]")),process.exitCode=1;return}case"security":{let n=S(),o=Ot(n),r=t.includes("--json");console.log(r?xu(o):ta(o,process.stdout)),Oo(o)&&(process.exitCode=1);return}case"service":{ex(t);return}case"pull":{await Vd(t);return}case"media-exec":{await Zd(t);return}case"pull-exec":{await Xd(t);return}case"config":{await jh(t);return}case"tunnel":{await km(t);return}case"platform":{await of(t);return}case"docs":{Rf(t);return}case"ui":{let n=ox(t);if(n.help){If();return}if(!Number.isFinite(n.port)||n.port<1||n.port>65535){console.error(C(process.stderr,"port must be between 1 and 65535.")),process.exitCode=1;return}let o=cf(n.token);await yf({host:n.host,port:n.port,meta:o});let r=process.stdout,s=wf(n.port);console.log(""),console.log(`${Re(r,"ui")} ${w(r,"listening")}`),console.log(`${w(r,"bind:")} ${v(r,`${n.host}:${n.port}`)}`),console.log(`${w(r,"setup token:")} ${v(r,o.token)}`),console.log(`${w(r,"token file:")} ${X(r,gt)}`),console.log(""),console.log(ke(r,"Anyone on your network who can reach this port needs the token \u2014 do not expose to untrusted Wi\u2011Fi.")),console.log(""),console.log(`${w(r,"Open:")}`),console.log(` ${v(r,`http://127.0.0.1:${n.port}/`)}`);for(let i of s)console.log(` ${v(r,i)}`);console.log(""),console.log(`${w(r,"Quick link (same Wi\u2011Fi):")} ${v(r,`http://127.0.0.1:${n.port}/?token=${encodeURIComponent(o.token)}`)}`),console.log("");return}default:wc(),e&&(process.exitCode=1)}}rx().catch(e=>{console.error(C(process.stderr,String(e))),process.exit(1)});