omnish 1.5.0 → 1.5.2
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/CHANGELOG.md +30 -0
- package/config.example.json +7 -8
- package/dist/index.js +303 -259
- package/package.json +24 -14
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
|
|
4
|
-
`,{mode:384})}function
|
|
5
|
-
`,{mode:384}),
|
|
6
|
-
`,{mode:384})}function
|
|
2
|
+
var Vf=Object.defineProperty;var Kt=(e,t)=>()=>(e&&(t=e(e=0)),t);var Xf=(e,t)=>{for(var n in t)Vf(e,n,{get:t[n],enumerable:!0})};import Zf from"node:crypto";import Hi from"node:fs";import eg from"node:os";import re from"node:path";function tg(){let e=process.env.OMNISH_HOME?.trim();if(e)return re.resolve(e);let t=eg.homedir(),n=re.join(t,".omnish"),o=re.join(t,".whatslive");try{if(Hi.existsSync(n))return n;if(Hi.existsSync(o))return o}catch{}return n}function No(e){let t=Zf.createHash("sha1").update(e,"utf8").digest("hex").slice(0,8);return re.join(Rc,t)}function B(e){Hi.mkdirSync(e,{recursive:!0,mode:448})}function se(){B(D),B(le),B(ut),B(Rc),B(Tc),B(kt),B(At),B(jn),B(Gi)}var D,le,ut,Rc,Tc,je,me,Bn,Lo,bt,Rr,Tr,W,$r,Pr,Mr,kt,Bi,ji,$c,At,Oo,Er,jn,dt,Gi,G=Kt(()=>{"use strict";D=tg(),le=re.join(D,"auth"),ut=re.join(D,"jobs"),Rc=re.join(D,"apps"),Tc=re.join(D,"logs"),je=re.join(Tc,"gateway.log"),me=re.join(D,"gateway.pid"),Bn=re.join(D,"gateway-control.json"),Lo=re.join(D,"config-ui.json"),bt=re.join(D,"ui.json"),Rr=re.join(D,"tunnel-auth.json"),Tr=re.join(D,"ui-server.json"),W=re.join(D,"config.json"),$r=re.join(D,"shortcuts.json"),Pr=re.join(D,"recipes.json"),Mr=re.join(D,"recipes-user.json"),kt=re.join(D,"cowork"),Bi=re.join(kt,"tasks.json"),ji=re.join(kt,"pending-runs.json"),$c=re.join(kt,"completions.sqlite"),At=re.join(D,"watch"),Oo=re.join(At,"rules.json"),Er=re.join(At,"events.sqlite"),jn=re.join(D,"bin"),dt=re.join(D,"venvs","whisper"),Gi=re.join(D,"media","pull")});function Ic(e){let t=e.trim();for(;;){let n=t;if(t=t.replace(/^whatsapp:/i,"").trim(),t===n)return t}}function ng(e){let t=Ic(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 og(e){let t=e.match(Mc);if(t)return t[1]??null;let n=e.match(Ec);if(n)return n[1]??null;let o=e.match(Ac);return o?o[1]??null:null}function Pc(e){let t=e.replace(/\D/g,"");return t?`+${t}`:""}function Yt(e){return`${e.replace(/\D/g,"")}@s.whatsapp.net`}function ne(e){let t=Ic(e);if(!t||ng(t))return null;if(Mc.test(t)||Ec.test(t)||Ac.test(t)){let o=og(t);if(!o)return null;let r=Pc(o);return r.length>1?r:null}if(t.includes("@"))return null;let n=Pc(t);return n.length>1?n:null}function Ar(e){return e.map(t=>String(t).trim()).filter(t=>!!t).map(t=>t==="*"?t:ne(t)).filter(t=>!!t)}function Ir(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 Fe(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 Lr(e){let t=new Set;for(let n of e){let o=Fe(String(n));o&&t.add(o)}return t}function Ji(e){let t=e.trim();if(!t)return null;let n=t.toLowerCase();if(n.startsWith("tg:")||n.startsWith("telegram:")){let r=Fe(t);return r?{kind:"tg",id:r}:null}let o=ne(t);return o?{kind:"wa",normalized:o}:null}function Lc(e,t){let n=ne(t);return n?e.has(n):!1}var Mc,Ec,Ac,ot=Kt(()=>{"use strict";Mc=/^(\d+)(?::\d+)?@s\.whatsapp\.net$/i,Ec=/^(\d+)@c\.us$/i,Ac=/^(\d+)@lid$/i});import Or from"node:fs";function rg(e){return typeof e.mediaSendFiles=="boolean"?e.mediaSendFiles:typeof e.pullAutoSend=="boolean"?e.pullAutoSend:A.mediaSendFiles}function sg(e){return typeof e.mediaUrlAutoDl=="boolean"?e.mediaUrlAutoDl:typeof e.pullUrlAutoDetect=="boolean"?e.pullUrlAutoDetect:A.mediaUrlAutoDl}function ig(e){return typeof e.mediaInstallFromChat=="boolean"?e.mediaInstallFromChat:typeof e.pullInstallFromChat=="boolean"?e.pullInstallFromChat:A.mediaInstallFromChat}function ag(e){return typeof e.mediaOutputDir=="string"?e.mediaOutputDir.trim().slice(0,4096):typeof e.pullOutputDir=="string"?e.pullOutputDir.trim().slice(0,4096):A.mediaOutputDir}function lg(e){let t=typeof e.mediaMaxBytes=="number"?e.mediaMaxBytes:typeof e.pullMaxBytes=="number"?e.pullMaxBytes:A.mediaMaxBytes;return!Number.isFinite(t)||t<0?A.mediaMaxBytes:t===0?0:Math.min(2e9,Math.floor(t))}function cg(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:A.mediaWhisperModel;return String(t).trim().slice(0,64)}function ug(e){return e==="downloads"||e==="omnishData"||e==="sessionCwd"||e==="processCwd"||e==="fixed"?e:A.fileReceiveRootMode}function dg(e){return e==="primary"?"primary":"secondary"}function pg(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=Fe(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 Fo(e){let t=typeof e.appsFlushMs=="number"&&e.appsFlushMs>=0?e.appsFlushMs:A.appsFlushMs,n=e.gatewayMode==="telegram"||e.gatewayMode==="both"||e.gatewayMode==="whatsapp"?e.gatewayMode:A.gatewayMode,o=typeof e.telegramBotToken=="string"?e.telegramBotToken:A.telegramBotToken,r=Array.isArray(e.telegramAllowFrom)?[...new Set(e.telegramAllowFrom.map(s=>Fe(String(s))).filter(s=>!!s))].sort():A.telegramAllowFrom;return{...A,...e,gatewayMode:n,telegramBotToken:o,telegramAllowFrom:r,allowFrom:Array.isArray(e.allowFrom)?Ar(e.allowFrom.map(String)).filter(s=>s!=="*"):A.allowFrom,commandPrefix:(()=>{let s=typeof e.commandPrefix=="string"&&e.commandPrefix.length>0?e.commandPrefix:A.commandPrefix;return s==="! "?"!":s})(),syncTimeoutMs:typeof e.syncTimeoutMs=="number"&&e.syncTimeoutMs>0?e.syncTimeoutMs:A.syncTimeoutMs,syncMaxBytes:typeof e.syncMaxBytes=="number"&&e.syncMaxBytes>0?e.syncMaxBytes:A.syncMaxBytes,jobLogTailLines:typeof e.jobLogTailLines=="number"&&e.jobLogTailLines>0?e.jobLogTailLines:A.jobLogTailLines,shell:typeof e.shell=="string"&&e.shell.length>0?e.shell:A.shell,appsCols:typeof e.appsCols=="number"&&e.appsCols>0&&e.appsCols<=500?Math.floor(e.appsCols):A.appsCols,appsRows:typeof e.appsRows=="number"&&e.appsRows>0&&e.appsRows<=200?Math.floor(e.appsRows):A.appsRows,appsFlushMs:t,appsMinIntervalMs:typeof e.appsMinIntervalMs=="number"&&e.appsMinIntervalMs>=0?e.appsMinIntervalMs:A.appsMinIntervalMs,appsMaxFlushBytes:typeof e.appsMaxFlushBytes=="number"&&e.appsMaxFlushBytes>256?Math.floor(e.appsMaxFlushBytes):A.appsMaxFlushBytes,appsMaxSessions:typeof e.appsMaxSessions=="number"&&e.appsMaxSessions>0?Math.min(50,Math.floor(e.appsMaxSessions)):A.appsMaxSessions,appsMaxSessionsTotal:typeof e.appsMaxSessionsTotal=="number"&&e.appsMaxSessionsTotal>0?Math.min(200,Math.floor(e.appsMaxSessionsTotal)):A.appsMaxSessionsTotal,appsMaxWaChars:typeof e.appsMaxWaChars=="number"&&e.appsMaxWaChars>256?Math.floor(e.appsMaxWaChars):A.appsMaxWaChars,appsLogTailLines:typeof e.appsLogTailLines=="number"&&e.appsLogTailLines>0?Math.min(500,Math.floor(e.appsLogTailLines)):A.appsLogTailLines,appsSubmitDelayMs:typeof e.appsSubmitDelayMs=="number"&&e.appsSubmitDelayMs>=0?Math.min(500,Math.floor(e.appsSubmitDelayMs)):A.appsSubmitDelayMs,appsClearInput:typeof e.appsClearInput=="boolean"?e.appsClearInput:A.appsClearInput,appsClearInputDelayMs:typeof e.appsClearInputDelayMs=="number"&&e.appsClearInputDelayMs>=0?Math.min(200,Math.floor(e.appsClearInputDelayMs)):A.appsClearInputDelayMs,appsClearInputSequence:typeof e.appsClearInputSequence=="string"&&e.appsClearInputSequence.length>0?e.appsClearInputSequence.slice(0,200):A.appsClearInputSequence,appsSkipClearOnPasswordPrompt:typeof e.appsSkipClearOnPasswordPrompt=="boolean"?e.appsSkipClearOnPasswordPrompt:A.appsSkipClearOnPasswordPrompt,appsPasswordPromptHint:typeof e.appsPasswordPromptHint=="boolean"?e.appsPasswordPromptHint:A.appsPasswordPromptHint,fileSendMaxBytes:typeof e.fileSendMaxBytes=="number"&&e.fileSendMaxBytes>=0?e.fileSendMaxBytes===0?0:Math.min(2e9,Math.floor(e.fileSendMaxBytes)):A.fileSendMaxBytes,fileReceiveMaxBytes:typeof e.fileReceiveMaxBytes=="number"&&e.fileReceiveMaxBytes>=0?e.fileReceiveMaxBytes===0?0:Math.min(2e9,Math.floor(e.fileReceiveMaxBytes)):A.fileReceiveMaxBytes,fileInboxSubdir:typeof e.fileInboxSubdir=="string"&&e.fileInboxSubdir.length>0&&e.fileInboxSubdir.replace(/[/\\]/g,"").slice(0,80)||A.fileInboxSubdir,fileReceiveRootMode:ug(e.fileReceiveRootMode),fileReceiveRootPath:typeof e.fileReceiveRootPath=="string"?e.fileReceiveRootPath.trim().slice(0,4096):A.fileReceiveRootPath,recipesAllowDangerousBuiltins:typeof e.recipesAllowDangerousBuiltins=="boolean"?e.recipesAllowDangerousBuiltins:A.recipesAllowDangerousBuiltins,recipesMaxTaskChars:typeof e.recipesMaxTaskChars=="number"&&e.recipesMaxTaskChars>=0?e.recipesMaxTaskChars===0?0:Math.min(Number.MAX_SAFE_INTEGER,Math.floor(e.recipesMaxTaskChars)):A.recipesMaxTaskChars,recipesMacroDefaultCommand:typeof e.recipesMacroDefaultCommand=="string"&&e.recipesMacroDefaultCommand.trim().length>0?e.recipesMacroDefaultCommand.trim().slice(0,4096):A.recipesMacroDefaultCommand,recipesRunAttach:typeof e.recipesRunAttach=="boolean"?e.recipesRunAttach:A.recipesRunAttach,clusterEnabled:typeof e.clusterEnabled=="boolean"?e.clusterEnabled:A.clusterEnabled,clusterLabel:typeof e.clusterLabel=="string"?e.clusterLabel.trim().slice(0,128):A.clusterLabel,clusterRole:dg(e.clusterRole),clusterSenderBindings:pg(e.clusterSenderBindings),serviceInstallFromChat:typeof e.serviceInstallFromChat=="boolean"?e.serviceInstallFromChat:A.serviceInstallFromChat,updateCheckEnabled:typeof e.updateCheckEnabled=="boolean"?e.updateCheckEnabled:A.updateCheckEnabled,updateCheckIntervalMs:typeof e.updateCheckIntervalMs=="number"&&e.updateCheckIntervalMs>0?Math.min(6048e5,Math.max(36e5,Math.floor(e.updateCheckIntervalMs))):A.updateCheckIntervalMs,updateCheckPackageName:typeof e.updateCheckPackageName=="string"&&e.updateCheckPackageName.trim().length>0?e.updateCheckPackageName.trim().slice(0,214):A.updateCheckPackageName,updateInfoUrl:typeof e.updateInfoUrl=="string"?e.updateInfoUrl.trim().slice(0,2048):A.updateInfoUrl,chatLlmFallbackEnabled:typeof e.chatLlmFallbackEnabled=="boolean"?e.chatLlmFallbackEnabled:A.chatLlmFallbackEnabled,chatLlmShellCommand:typeof e.chatLlmShellCommand=="string"?e.chatLlmShellCommand.trim().slice(0,8192):A.chatLlmShellCommand,chatLlmTimeoutMs:typeof e.chatLlmTimeoutMs=="number"&&e.chatLlmTimeoutMs>0?Math.min(9e5,Math.floor(e.chatLlmTimeoutMs)):A.chatLlmTimeoutMs,chatLlmMaxInputChars:typeof e.chatLlmMaxInputChars=="number"&&e.chatLlmMaxInputChars>0?Math.min(5e5,Math.floor(e.chatLlmMaxInputChars)):A.chatLlmMaxInputChars,chatLlmMaxOutputChars:typeof e.chatLlmMaxOutputChars=="number"&&e.chatLlmMaxOutputChars>0?Math.min(2e6,Math.floor(e.chatLlmMaxOutputChars)):A.chatLlmMaxOutputChars,chatLlmNeedsTty:typeof e.chatLlmNeedsTty=="boolean"?e.chatLlmNeedsTty:A.chatLlmNeedsTty,chatLlmWorkDir:typeof e.chatLlmWorkDir=="string"?e.chatLlmWorkDir.trim().slice(0,4096):A.chatLlmWorkDir,tunnelEnabled:typeof e.tunnelEnabled=="boolean"?e.tunnelEnabled:A.tunnelEnabled,tunnelRelayUrl:typeof e.tunnelRelayUrl=="string"&&e.tunnelRelayUrl.trim().length>0?e.tunnelRelayUrl.trim().slice(0,2048):A.tunnelRelayUrl,tunnelMaxActive:typeof e.tunnelMaxActive=="number"&&e.tunnelMaxActive>0?Math.min(50,Math.floor(e.tunnelMaxActive)):A.tunnelMaxActive,platformToken:typeof e.platformToken=="string"?e.platformToken.trim():A.platformToken,platformDeviceId:typeof e.platformDeviceId=="string"?e.platformDeviceId.trim().slice(0,128):A.platformDeviceId,webhookEnabled:typeof e.webhookEnabled=="boolean"?e.webhookEnabled:A.webhookEnabled,webhookPort:typeof e.webhookPort=="number"&&e.webhookPort>=0?Math.min(65535,Math.floor(e.webhookPort)):A.webhookPort,webhookHost:typeof e.webhookHost=="string"&&e.webhookHost.trim().length>0?e.webhookHost.trim():A.webhookHost,webhookToken:typeof e.webhookToken=="string"?e.webhookToken.trim():A.webhookToken,watchEnabled:typeof e.watchEnabled=="boolean"?e.watchEnabled:A.watchEnabled,watchDebounceMs:typeof e.watchDebounceMs=="number"&&Number.isFinite(e.watchDebounceMs)?Math.max(500,Math.min(6e4,Math.floor(e.watchDebounceMs))):A.watchDebounceMs,watchMaxEventsPerMinute:typeof e.watchMaxEventsPerMinute=="number"&&Number.isFinite(e.watchMaxEventsPerMinute)?Math.max(1,Math.min(120,Math.floor(e.watchMaxEventsPerMinute))):A.watchMaxEventsPerMinute,watchAutoRestore:typeof e.watchAutoRestore=="boolean"?e.watchAutoRestore:A.watchAutoRestore,mediaSendFiles:rg(e),mediaUrlAutoDl:sg(e),mediaInstallFromChat:ig(e),mediaOutputDir:ag(e),mediaMaxBytes:lg(e),mediaWhisperModel:cg(e),progressUpdates:typeof e.progressUpdates=="boolean"?e.progressUpdates:A.progressUpdates,pullYtDlpPath:typeof e.pullYtDlpPath=="string"?e.pullYtDlpPath.trim().slice(0,4096):A.pullYtDlpPath,pullFfmpegPath:typeof e.pullFfmpegPath=="string"?e.pullFfmpegPath.trim().slice(0,4096):A.pullFfmpegPath,pullWhisperPath:typeof e.pullWhisperPath=="string"?e.pullWhisperPath.trim().slice(0,4096):A.pullWhisperPath}}function N(e){let t=S(),n=Fo({...t,...e});return Ge(n),n}function S(){if(se(),!Or.existsSync(W)){let e=Fo({});return Ge(e),e}try{let e=Or.readFileSync(W,"utf8"),t=JSON.parse(e);return Fo(t)}catch{return Fo({})}}function Ge(e){se();let t=Fo(e);Or.writeFileSync(W,JSON.stringify(t,null,2)+`
|
|
3
|
+
`,{mode:384})}function Nr(e){let t=Ji(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 Ge(n),n}function Fr(e){let t=Ji(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=>Fe(o)!==t.id),Ge(n),n}function Pe(e){let t=typeof process.env.TELEGRAM_BOT_TOKEN=="string"?process.env.TELEGRAM_BOT_TOKEN.trim():"";return t||(e.telegramBotToken??"").trim()}function vt(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(),Ge(t),S()}function Gn(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,Ge(t),S()}function Wr(e){let t=S();return t.clusterEnabled=e,Ge(t),S()}function pt(){try{return Or.readdirSync(le).length>0}catch{return!1}}var A,ue=Kt(()=>{"use strict";G();ot();A={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:!0,mediaInstallFromChat:!1,mediaOutputDir:"",mediaMaxBytes:0,mediaWhisperModel:"small",progressUpdates:!0,pullYtDlpPath:"",pullFfmpegPath:"",pullWhisperPath:""}});import mg from"pino";function Jn(){return process.env.OMNISH_VERBOSE==="1"||process.env.WHATSVERBOSE==="1"}function Oc(){return Jn()?"info":"silent"}function qi(e){e?process.env.OMNISH_VERBOSE="1":delete process.env.OMNISH_VERBOSE,P.level=Oc()}function Nc(){return P.child({module:"baileys"})}var P,xe=Kt(()=>{"use strict";P=mg({level:Oc(),base:{app:"omnish"}})});import ia from"node:fs";function Nt(){try{let e=ia.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 Ct(e){se(),ia.writeFileSync(Rr,JSON.stringify({token:e.token.trim(),...e.relayUrl?{relayUrl:e.relayUrl.trim()}:{}},null,2)+`
|
|
4
|
+
`,{mode:384})}function Zr(){try{ia.unlinkSync(Rr)}catch{}}function dy(){return process.env.OMNISH_TOKEN?.trim()||process.env.OMNISH_TUNNEL_TOKEN?.trim()||process.env.OMNISH_DEVICE_TOKEN?.trim()||""}function Rt(){let e=dy();if(e)return e;let t=S().platformToken.trim();return t||(Nt()?.token??"")}function aa(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=Nt()?.relayUrl?.trim();return o?o.replace(/\/$/,""):e.replace(/\/$/,"")}function mt(e){return aa(e)}var xn=Kt(()=>{"use strict";ue();G()});var Ee,Cn=Kt(()=>{"use strict";Ee="https://tunnel.omnish.dev"});function ln(e){return(e??"").trim()}function zy(){return ln(process.env.OMNISH_PLATFORM_URL)||ln(process.env.OMNISH_COMM_LAYER_URL)||ln(process.env.OMNISH_TUNNEL_RELAY)}function Ky(){return ln(process.env.OMNISH_TOKEN)||ln(process.env.OMNISH_DEVICE_TOKEN)||ln(process.env.OMNISH_TUNNEL_TOKEN)}function _a(){return Rt()}function ro(){return Ky()?"env":S().platformToken.trim()?"config":Nt()?.token?"file":"default"}function Vo(){let e=S();return aa(e.tunnelRelayUrl||Ee)}function so(){return zy()?"env":S().tunnelRelayUrl.trim()?"config":Nt()?.relayUrl?.trim()?"file":"default"}function Wa(){let e=ln(process.env.OMNISH_DEVICE_ID);if(e)return e;let t=S().platformDeviceId.trim();if(t)return t}function Da(){return ln(process.env.OMNISH_DEVICE_ID)?"env":S().platformDeviceId.trim()?"config":"default"}function oe(){let e=Vo(),t=_a();if(!e||!t)return null;let n=Wa();return{platformUrl:e.replace(/\/$/,""),token:t,deviceId:n}}var tt=Kt(()=>{"use strict";ue();xn();Cn()});var vm={};Xf(vm,{fetchPlatformAccount:()=>On,getAttachedConfig:()=>Rl,getAttachedPlatformSnapshot:()=>Xk,loadConfigForSendtoBroadcast:()=>Tl,mergeAttachedPlatformPolicy:()=>Cl,parsePlatformMeResponse:()=>bm,setAttachedPlatformSnapshot:()=>Sl,setPlatformDefaultDevice:()=>$l,snapshotFromRegisteredAccount:()=>km,syncAttachedPlatformPolicy:()=>ti,updatePlatformAllowlists:()=>ei});function xl(e){let t=e.replace(/\/$/,"");return/^https?:\/\//i.test(t)?t:`http://${t}`}function Qk(e){return e==="telegram"||e==="both"||e==="whatsapp"?e:"whatsapp"}function Vk(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 bm(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:Qk(e.gatewayMode),connectors:Vk(e.connectors),defaultDeviceId:r,routing:{defaultDeviceId:r,onlineDeviceIds:o,onlineCount:typeof n?.onlineCount=="number"?n.onlineCount:o.length}}}function km(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 Cl(e,t){return{...e,allowFrom:[...t.allowFrom],telegramAllowFrom:[...t.telegramAllowFrom],gatewayMode:t.gatewayMode}}function Sl(e){Zs=e}function Xk(){return Zs}function Rl(){let e=S();return Zs?Cl(e,Zs):e}async function Tl(){let e=S(),t=oe();if(!t)return e;try{return Cl(e,await On(t))}catch(n){return P.warn({err:String(n)},"sendto: could not fetch platform allowlists; using local config.json"),e}}async function On(e){let t=await fetch(`${xl(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 bm(n)}async function ei(e,t){let n=await fetch(`${xl(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 $l(e,t){let n=await fetch(`${xl(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 ti(e,t){try{let n=await On(e);return Sl(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=km(t);return Sl(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 Zs,cr=Kt(()=>{"use strict";ue();xe();tt();Zs=null});ue();import Cx from"node:dns";import Rx from"node:crypto";import kn from"node:fs";import xc from"node:path";import Gf from"node:os";ue();xe();import Zg from"node:os";import ey from"node:fs";G();import Fc from"node:crypto";import _o from"node:fs";import zi from"node:os";import qn from"node:path";var hg=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/;function Yi(e){let t=e.trim().toLowerCase();return hg.test(t)?{ok:!0,name:t}:{ok:!1,error:"Name must be alphanumeric with _ or -, max 32 chars."}}function Ze(e,t){let n=e.trim();return n===""||n==="."?qn.resolve(t):n==="~"?zi.homedir():n.startsWith("~/")||n.startsWith("~\\")?qn.join(zi.homedir(),n.slice(2)):qn.isAbsolute(n)?n:qn.resolve(t,n)}function Dr(e){return qn.join(zi.homedir(),"Cowork",e)}function fg(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:Dr(o),l=typeof t.enabled=="boolean"?t.enabled:!0,c=typeof t.notify=="string"?t.notify.toLowerCase():"self",u=c==="wa"||c==="whatsapp"?"wa":c==="tg"||c==="telegram"?"tg":c==="all"?"all":c==="none"?"none":"self",d={kind:"ondemand"};if(t.schedule&&typeof t.schedule=="object"){let k=t.schedule,T=typeof k.kind=="string"?k.kind:"";if(T==="ondemand")d={kind:"ondemand"};else if(T==="daily"){let $=Number(k.hour),L=Number(k.minute);Number.isFinite($)&&Number.isFinite(L)&&(d={kind:"daily",hour:$,minute:L})}else if(T==="weekdays"){let $=Number(k.hour),L=Number(k.minute);Number.isFinite($)&&Number.isFinite(L)&&(d={kind:"weekdays",hour:$,minute:L})}else if(T==="hourly"){let $=Number(k.minute);Number.isFinite($)&&Number.isInteger($)&&$>=0&&$<=59&&(d={kind:"hourly",minute:$})}else if(T==="weekly"){let $=Number(k.hour),L=Number(k.minute),x=Number(k.weekday);Number.isFinite($)&&Number.isFinite(L)&&Number.isInteger(x)&&(d={kind:"weekly",weekday:x,hour:$,minute:L})}else if(T==="heartbeat"){let $=Number(k.intervalMs),L=Number(k.graceMs);Number.isFinite($)&&$>0&&Number.isFinite(L)&&L>0&&(d={kind:"heartbeat",intervalMs:$,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:d,enabled:l,notify:u,notifyWhen:y==="failure"?"failure":y==="state-change"?"state-change":"always",attachLog:f,attachFiles:g,lastCompletedSlotMs:m,createdAtMs:h}}function Me(){try{let e=_o.readFileSync(Bi,"utf8"),t=JSON.parse(e);if(!t||!Array.isArray(t.tasks))return[];let n=[];for(let o of t.tasks){let r=fg(o);r&&n.push(r)}return n}catch{return[]}}function De(e){B(kt);let t={tasks:e},n=qn.join(kt,`.tasks.${process.pid}.${Fc.randomBytes(4).toString("hex")}.tmp`);_o.writeFileSync(n,JSON.stringify(t,null,2)+`
|
|
5
|
+
`,{mode:384}),_o.renameSync(n,Bi)}function rt(e,t,n){let o=t.trim().toLowerCase();return e.find(r=>r.name===o&&r.ownerPeerKey===n)}function Wo(){return Fc.randomBytes(4).toString("hex")}function _c(e){B(kt),_o.writeFileSync(ji,JSON.stringify(e,null,2)+`
|
|
6
|
+
`,{mode:384})}function Wc(e){let t=Ki();t.push(e),_c(t)}function Ki(){try{let e=_o.readFileSync(ji,"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 Dc(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 _c(o),{batch:n,remainingAfter:o.length}}G();G();import gg from"better-sqlite3";var Uc=1,Hc=200,zn=null;function yg(){B(At);let e=new gg(Er);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,159 @@ 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)<
|
|
25
|
+
`);let t=e.prepare("SELECT value FROM watch_meta WHERE key = 'schema_version'").get();return(t?Number(t.value):0)<Uc&&e.prepare("INSERT OR REPLACE INTO watch_meta (key, value) VALUES ('schema_version', ?)").run(String(Uc)),e}function Do(){return zn||(zn=yg()),zn}function Ur(){Do()}function Bc(){if(zn){try{zn.close()}catch{}zn=null}}function jc(e){let t=Do();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>Hc&&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-
|
|
28
|
-
`,o=`${
|
|
29
|
-
`)){let y=g.trim();y&&t(y)}}finally{
|
|
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=
|
|
31
|
-
`,{mode:384})}var
|
|
32
|
-
`).replace(/^\n+/,"").trimEnd()}
|
|
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
|
|
34
|
-
`).trimEnd()}function
|
|
35
|
-
`}function
|
|
36
|
-
`).replace(/^\n+/,"").trimEnd()}function
|
|
37
|
-
`).replace(/^\n+/,"").trimEnd()}function
|
|
27
|
+
)`).run(n.c-Hc)}function Gc(e,t){let n=Do(),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 Jc(e){return Do().prepare("SELECT state_key FROM watch_rule_state WHERE rule_id = ?").get(e)?.state_key??null}function qc(e,t,n){Do().prepare("INSERT OR REPLACE INTO watch_rule_state (rule_id, state_key, updated_at_ms) VALUES (?, ?, ?)").run(e,t,n)}import Vc from"node:path";import It from"node:path";function wg(e,t){let n=It.normalize(e);for(let o of t){let r=It.normalize(o);if(n===r||n.startsWith(r+It.sep))return!0}return!1}function bg(e,t,n){let o=It.relative(t,e);if(o.startsWith("..")||It.isAbsolute(o))return!1;let r=o.split(It.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 Hr(e,t){if(t.excludePaths.length&&wg(e,t.excludePaths))return!0;let n=t.path;if(n&&t.excludeGlobs.length){for(let o of t.excludeGlobs)if(bg(e,n,o))return!0}return!1}function zc(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=It.relative(e.path,n);!o.startsWith("..")&&!It.isAbsolute(o)&&t.push(o.split(It.sep).join("/")+"/**")}catch{}return t}ot();function Kn(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:${Yt(s)}`)}if(e==="tg"||e==="all")for(let r of n.telegramAllowFrom){let s=Fe(String(r));s&&o.add(`tg:${s}`)}return[...o]}function Kc(e,t,n){return Kn(e,t,n)}xe();G();import kg from"node:crypto";import Qi from"node:fs";var vg=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,Sg=1,Vi=20;function Br(e){let t=e.trim().toLowerCase();return vg.test(t)?{ok:!0,name:t}:{ok:!1,error:"Name must be alphanumeric with _ or -, max 32 chars."}}function jr(){return kg.randomBytes(8).toString("hex")}function xg(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 Cg(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",u=c==="failure"||c==="state-change"?c:"always",d=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:xg(t.events),units:d,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 Rg(e){let t=JSON.parse(e);return!t||!Array.isArray(t.rules)?[]:t.rules.map(Cg).filter(n=>n!==null)}function Tg(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),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 Ue(){try{let e=Qi.readFileSync(Oo,"utf8"),t=Rg(e),{rules:n,changed:o}=Tg(t);return o&&St(n),n}catch{return[]}}function $g(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 Uo(){let e=Ue();return{rules:e,summary:$g(e)}}function St(e){B(At);let n=`${JSON.stringify({version:Sg,rules:e},null,2)}
|
|
28
|
+
`,o=`${Oo}.tmp`;Qi.writeFileSync(o,n,{mode:384}),Qi.renameSync(o,Oo)}function Xi(){return Oo}function vn(e,t){let n=t.trim().toLowerCase();return e.find(o=>o.name===n)}function Yc(e,t){return e.find(n=>n.id===t)}function Yn(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 Qc(e,t){let n=t.trim().toLowerCase();return e.filter(o=>o.name!==n)}import Pg from"node:os";import xt from"node:path";var Mg=new Set([".env","id_rsa","id_ed25519","id_ecdsa","known_hosts","credentials.json","secrets.json",".netrc","shadow","passwd"]);function st(e){let t=xt.normalize(e),n=t.toLowerCase(),o=Pg.homedir().toLowerCase();if(n.includes(`${xt.sep}.ssh${xt.sep}`)||n.endsWith(`${xt.sep}.ssh`)||n.includes(`${xt.sep}browser${xt.sep}`)&&n.includes("profile")||o&&(n===`${o}/.gnupg`||n.startsWith(`${o}/.gnupg${xt.sep}`))||n.includes(`${xt.sep}keychains${xt.sep}`))return!0;let r=xt.basename(t);return!!(Mg.has(r)||r.startsWith(".env.")||r.endsWith(".pem")||r.endsWith(".key"))}var Eg=["node_modules",".git",".svn",".hg","__pycache__",".cache",".next","dist","build"],Ag=[".tmp",".swp",".swx","~",".part"];function Xc(e,t){let n=new Map,o=new Map;function r(u,d){let m=u.split(Vc.sep);for(let f of m)if(Eg.includes(f))return!0;let h=Vc.basename(u);for(let f of Ag)if(h.endsWith(f))return!0;return!!Hr(u,d)}function s(u){let d=Yc(Ue(),u);return!d||!d.enabled||d.paused?null:d}function i(u){let d=Date.now(),m=o.get(u);return!m||d-m.windowStart>=6e4?(o.set(u,{windowStart:d,count:1}),!1):m.count>=t.maxEventsPerMinute?!0:(m.count+=1,!1)}async function a(u,d){let m=e.getConfig();if(!m.watchEnabled)return;let h=s(u.id);if(!h||d.kind==="fs"&&d.meta?.path&&(st(d.meta.path)||r(d.meta.path,h)))return;if(jc(d),(h.notifyWhen??"always")==="state-change"){if(Jc(h.id)===d.stateKey)return;qc(h.id,d.stateKey,d.tsMs)}let g=Kc(h.notify,h.ownerPeerKey,m);if(g.length===0)return;let y=`[watch:${h.name}] ${d.summary}`;await Promise.all(g.map(b=>e.sendToPeer(b,y).catch(()=>{})))}function l(u){let d=n.get(u);if(!d)return;n.delete(u);let m=s(d.ruleId);m&&(i(d.ruleId)||a(m,d.event))}function c(u){for(let[d,m]of n)m.ruleId===u&&(clearTimeout(m.timer),n.delete(d))}return{ingest(u,d){if(!u.enabled||u.paused||d.meta?.path&&(st(d.meta.path)||r(d.meta.path,u)))return;let m=`${u.id}:${d.stateKey||d.summary}`,h=n.get(m);h&&clearTimeout(h.timer);let f=setTimeout(()=>l(m),t.debounceMs);f.unref?.(),n.set(m,{timer:f,event:d,ruleId:u.id})},cancelForRule:c,dispose(){for(let u of n.values())clearTimeout(u.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")))}xe();import eu from"node:fs";import Ig from"node:path";import{subscribe as Lg}from"@parcel/watcher";var Og=["**/node_modules/**","**/.git/**","**/.svn/**","**/.hg/**","**/__pycache__/**","**/.cache/**"];function Ng(e){return e==="create"?"create":e==="delete"?"delete":e==="update"?"update":e}function Fg(e,t){try{let n=eu.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 tu(e,t){let n=e.path;if(!n||!eu.existsSync(n))return{stop:async()=>{},status:()=>"error: path missing or not found"};if(st(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 Lg(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 c=Ng(l.type);if(!Zc(c,o))continue;let u=Ig.resolve(l.path);if(!u.startsWith(n)||st(u)||Hr(u,e))continue;let d=Fg(c,u);t({ruleId:e.id,ruleName:e.name,kind:"fs",stateKey:`${c}:${u}`,summary:d,tsMs:Date.now(),meta:{path:u,type:c}})}},{ignore:[...Og,...zc(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 Gg from"node:os";xe();import Vt from"node:fs";function _g(e,t,n){let o=n?.pollMs??2e3,r=0,s=!1,i=null,a=null,l="starting";function c(){if(!s)try{let u=Vt.statSync(e);if(u.size<r&&(r=0),u.size<=r)return;let d=Vt.openSync(e,"r");try{let m=u.size-r,h=Buffer.alloc(m);Vt.readSync(d,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{Vt.closeSync(d)}l="ok"}catch(u){l=`error: ${String(u)}`}}try{Vt.existsSync(e)&&(r=Vt.statSync(e).size),i=Vt.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 Gr(e,t){for(let n of e)try{if(Vt.existsSync(n))return _g(n,t)}catch(o){P.debug({path:n,err:String(o)},"watch log-tail skip path")}return null}var Wg=["/var/log/install.log"];function Dg(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 nu(e,t){return Gr(Wg,n=>{let o=Dg(n);o&&t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:o.slice(0,200),summary:o,tsMs:Date.now()})})}var Ug=["/var/log/dpkg.log","/var/log/apt/history.log"];function Hg(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 Bg(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 ou(e,t){return Gr(Ug,n=>{let o=Hg(n)??Bg(n);o&&t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:o,summary:o,tsMs:Date.now()})})}import{spawn as jg}from"node:child_process";function ru(e,t){let n=!1,o="ok",r=0,s=()=>{if(n)return;let a=jg("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 u=JSON.parse(l),d=Array.isArray(u)?u:[u];for(let m of d.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",c=>{o=`error: ${String(c)}`,i()})},i=()=>{n||setTimeout(s,3e4).unref?.()};return s(),{stop(){n=!0},status:()=>o}}function su(e,t){let n=Gg.platform();if(n==="win32"){let r=ru(e,t);return{stop:()=>r.stop(),status:r.status}}if(n==="darwin"){let r=nu(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=ou(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 Xg from"node:os";import{spawnSync as iu}from"node:child_process";var Jg=3e4;function qg(e){let t=new Map;for(let n of e){let o=iu("launchctl",["print",`system/${n}`],{encoding:"utf8",timeout:1e4});if(o.status!==0){let s=process.getuid?.()??501,i=iu("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 au(e,t){let n=e.units,o=!1,r="ok",s=new Map,i=()=>{if(o)return;let a=qg(n);if(a.size===0)r="error: no launchd labels configured";else{r="ok";for(let[l,c]of a){let u=s.get(l);if(u!==void 0&&u!==c){let d=`svc: ${l} ${u} \u2192 ${c}`;t({ruleId:e.id,ruleName:e.name,kind:"svc",stateKey:`${l}:${c}`,summary:d,tsMs:Date.now(),meta:{unit:l,state:c}})}}s=a}o||setTimeout(i,Jg).unref?.()};return i(),{stop(){o=!0},status:()=>r}}import{spawnSync as lu}from"node:child_process";var zg=3e4;function Kg(e){let t=new Map;if(e.length===0)return t;let n=["show",...e,"--property=ActiveState,SubState,UnitFileState","--no-pager"],o=lu("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=lu("systemctl",["is-active",i],{encoding:"utf8",timeout:5e3}),l=(a.stdout??a.stderr??"unknown").trim();t.set(i,l)}return t}function cu(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=Kg(n);if(a.size===0)r="error: systemctl unavailable or units not found";else{r="ok";for(let[l,c]of a){let u=s.get(l);if(u!==void 0&&u!==c){let d=`svc: ${l} ${u} \u2192 ${c}`;t({ruleId:e.id,ruleName:e.name,kind:"svc",stateKey:`${l}:${c}`,summary:d,tsMs:Date.now(),meta:{unit:l,state:c}})}}s=a}o||setTimeout(i,zg).unref?.()};return i(),{stop(){o=!0},status:()=>r}}import{spawnSync as Yg}from"node:child_process";var Qg=3e4;function Vg(e){let t=new Map;for(let n of e){let o=Yg("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 uu(e,t){let n=e.units,o=!1,r="ok",s=new Map,i=()=>{if(o)return;let a=Vg(n);if(a.size===0)r="error: no service names configured";else{r="ok";for(let[l,c]of a){let u=s.get(l);if(u!==void 0&&u!==c){let d=`svc: ${l} ${u} \u2192 ${c}`;t({ruleId:e.id,ruleName:e.name,kind:"svc",stateKey:`${l}:${c}`,summary:d,tsMs:Date.now(),meta:{unit:l,state:c}})}}s=a}o||setTimeout(i,Qg).unref?.()};return i(),{stop(){o=!0},status:()=>r}}function du(e,t){let n=Xg.platform();return n==="win32"?uu(e,t):n==="darwin"?au(e,t):cu(e,t)}var it=null,Xt=null;function pu(e){let t=e.getConfig();return Xc(e,{debounceMs:Math.max(500,t.watchDebounceMs??2e3),maxEventsPerMinute:Math.max(1,t.watchMaxEventsPerMinute??30)})}function ty(e){return e.watchEnabled&&e.watchAutoRestore}var Zi=class{deps;pipeline;runtimes=new Map;stopped=!1;constructor(t){this.deps=t,this.pipeline=pu(t)}onEvent=(t,n)=>{this.pipeline.ingest(t,n)};cancelPendingForRule(t){this.pipeline.cancelForRule(t)}async reload(){if(this.pipeline.dispose(),this.pipeline=pu(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=Ze(t.path,Zg.homedir()),r={...t,path:o};if(st(o)){t.adapterStatus="denied: sensitive path",this.persistRuleStatus(t);return}if(!ey.existsSync(o)){t.adapterStatus="error: path not found",this.persistRuleStatus(t);return}let s=await tu(r,i=>this.onEvent(r,i));n={stop:()=>s.stop(),status:s.status}}else if(t.kind==="pkg")n=su(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=du(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=Ue(),o=n.findIndex(r=>r.id===t.id);o>=0&&(n[o]={...n[o],adapterStatus:t.adapterStatus},St(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(),Bc()}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 mu(){return it}function ea(e){it?.cancelPendingForRule(e)}function ny(){B(At),Ur()}function ta(){Xt&&(it||(it=new Zi(Xt)),it.reload())}function Jr(e){Xt=e,ny();let t=e.getConfig(),{summary:n}=Uo();return n.total>0&&P.info({rules:n.total,active:n.active,paused:n.paused,disabled:n.disabled},"watch rules loaded from disk"),ty(t)&&ta(),()=>{it?.stop(),it=null,Xt=null}}async function hu(){return Xt?Xt.getConfig().watchEnabled?(ta(),it?.getRunningCount()??0):(it?.stop(),it=null,-1):-1}function Ke(){if(!Xt)return;if(!Xt.getConfig().watchEnabled){it?.stop(),it=null;return}ta()}G();ue();G();import sy from"node:os";import Kr from"node:path";G();import na from"node:fs";import gu from"node:os";import Zt from"node:path";var yu=Zt.join(D,"sessions.json"),Qn=new Map;function wu(){return{cwd:Zt.resolve(gu.homedir())}}function oy(e){return e.startsWith("wa:")||e.startsWith("tg:")?e:`wa:${e}`}function ry(){try{let e=na.readFileSync(yu,"utf8"),t=JSON.parse(e);for(let[n,o]of Object.entries(t)){if(!o||typeof o!="object")continue;let r=oy(n),i={cwd:typeof o.cwd=="string"&&o.cwd.length>0?Zt.resolve(o.cwd):wu().cwd};o.fileReceiveRoot==="sessionCwd"&&(i.fileReceiveRoot="sessionCwd"),Qn.set(r,i)}}catch{}}function bu(){B(D);let e={};for(let[t,n]of Qn){let o={cwd:n.cwd};n.fileReceiveRoot==="sessionCwd"&&(o.fileReceiveRoot="sessionCwd"),e[t]=o}na.writeFileSync(yu,JSON.stringify(e,null,2)+`
|
|
31
|
+
`,{mode:384})}var fu=!1;function oa(){fu||(ry(),fu=!0)}function ie(e){oa();let t=Qn.get(e);return t||(t=wu(),Qn.set(e,t)),t}function qr(e,t){oa();let n=Zt.resolve(t),o=ie(e);o.cwd=n,Qn.set(e,o),bu()}function zr(e){return ie(e).fileReceiveRoot==="sessionCwd"?"sessionCwd":"default"}function ra(e,t){oa();let n=ie(e);t==="default"?delete n.fileReceiveRoot:n.fileReceiveRoot="sessionCwd",Qn.set(e,n),bu()}function ku(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 vu(e,t){if(t.kind==="home")return Zt.resolve(gu.homedir());let n=t.value.replace(/^['"]|['"]$/g,"");return Zt.isAbsolute(n)?Zt.normalize(n):Zt.resolve(e,n)}function Su(e){try{return na.statSync(e).isDirectory()?{ok:!0}:{ok:!1,error:`Not a directory: ${e}`}}catch(t){return{ok:!1,error:String(t)}}}var iy="Omnish";function Yr(){return Kr.join(sy.homedir(),"Downloads",iy)}function en(e,t){let n=ie(t);if(n.fileReceiveRoot==="sessionCwd")return n.cwd;switch(e.fileReceiveRootMode){case"downloads":return Yr();case"omnishData":return Kr.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(!Kr.isAbsolute(o))throw new Error('fileReceiveRootPath must be an absolute path when fileReceiveRootMode is "fixed".');return Kr.resolve(o)}default:return Yr()}}G();import Ft from"node:fs";import Cu from"node:path";import rn from"node:process";var Sn="\x1B";function ay(e){return e.replace(/\u001B\[[\d;]*m/g,"")}function Qr(e){return ay(e).length}function sa(e,t,n,o,r=2){let s=Math.max(0,t-Qr(n));return`${e}${n}${" ".repeat(s)}${" ".repeat(r)}${o}`}function Lt(e,t,n,o){if(n.length===0)return[];let r=n.map(i=>o(i.left)),s=Math.max(...r.map(Qr));return n.map((i,a)=>sa(t,s,r[a],v(e,i.right)))}var tn={primary:"#b4ff24",foreground:"#eef2f4",muted:"#8a9199",border:"#2a3139",error:"#ef4444",warn:"#a0e614",warnAlt:"#8cce04"};function ly(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 nn(e){let t=ly(e);return t?`${Sn}[38;2;${t.r};${t.g};${t.b}m`:""}function Vr(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 Ot(e,t,n){return!t||!Vr(e)?n:`${t}${n}${Sn}[0m`}function z(e,t){return Ot(e,`${Sn}[1m`,t)}function cy(e,t){return Vr(e)?`${nn(tn.foreground)}${Sn}[1m${t}${Sn}[0m`:t}function X(e,t){return Ot(e,`${Sn}[2m`,t)}function Ce(e,t){return Ot(e,`${nn(tn.primary)}${Sn}[1m`,t)}function he(e,t){return Ot(e,nn(tn.primary),t)}function v(e,t){return Ot(e,nn(tn.foreground),t)}function w(e,t){return Ot(e,nn(tn.muted),t)}function uy(e,t){return Ot(e,nn(tn.border),t)}function Xr(e,t){return Ot(e,nn(tn.error),t)}function we(e,t){return Ot(e,nn(tn.warn),t)}function on(e,t=60,n="\u2500"){let o=n.repeat(Math.max(1,t));return uy(e,o)}function U(e,t){return Vr(e)?`${`${w(e,"[")}${he(e,"omnish")}${w(e,"]")}`} ${t}`:`[omnish] ${t}`}function C(e,t){return Vr(e)?`${`${Xr(e,"[omnish]")}`} ${t}`:`[omnish] ${t}`}function xu(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("",cy(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()}ue();G();xn();Cn();function es(e){return(e&4)!==0}function la(e){return(e&2)!==0}var Ru={error:3,warn:2,info:1};function Tu(e,t){let n=Ru[t];return e.filter(o=>Ru[o.severity]>=n)}function _t(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 ${W} and delete wildcard entries.`,fixHint:`Edit ${W} 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 ${W} 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 ${W} 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 ${W} unless you trust every allowlisted identity.`}),!Cu.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 ${W}.`});else try{Ft.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 ${W} 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 ${W} points to a real file.`})}if(e.fileReceiveRootMode==="fixed"){let a=e.fileReceiveRootPath.trim();a?Cu.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 ${W}.`}):n.push({severity:"error",code:"receive-fixed-empty",message:'fileReceiveRootMode is "fixed" but fileReceiveRootPath is empty.',fixHint:`Set fileReceiveRootPath to an absolute directory in ${W}, or change fileReceiveRootMode.`})}if(rn.platform!=="win32"){try{if(Ft.existsSync(W)){let c=Ft.statSync(W);(es(c.mode)||la(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 ${W}`,fixHint:`chmod 600 ${W}`})}}catch{}(typeof rn.getuid=="function"?rn.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(Ft.existsSync(D)){let c=Ft.statSync(D);(es(c.mode)||la(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&&Ft.existsSync(ut)){let c=Ft.statSync(ut);(es(c.mode)||la(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 ${ut}`,fixHint:`chmod 700 ${ut}`})}}catch{}try{if(Ft.existsSync(le)){let c=Ft.statSync(le);es(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 ${le}`,fixHint:`chmod 700 ${le}`})}}catch{}}return(typeof rn.env.TELEGRAM_BOT_TOKEN=="string"?rn.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&&!Pe(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 ${W} 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."}),(rn.env.OMNISH_TOKEN?.trim()||rn.env.OMNISH_TUNNEL_TOKEN?.trim()||rn.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."}),Rt()||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 Ho(e){return e.some(t=>t.severity==="error")}function py(e,t){switch(t){case"error":return Xr(e,"[ERROR]");case"warn":return we(e,"[WARN]");case"info":return w(e,"[INFO]")}}function ca(e,t,n,o){if(o.length!==0){t.push(z(e,`${n} (${o.length}):`));for(let r of o)t.push(` ${py(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 ua(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 ca(t,i,"Errors",n),ca(t,i,"Warnings",o),ca(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 da(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 $u(e){return`${JSON.stringify({findings:e,summary:da(e)},null,2)}
|
|
35
|
+
`}function Pu(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 Mu(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(Xr(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 ge(e,t){return{wa:e,tg:t,tgHtml:!0}}function de(e,t){return t==="whatsapp"?{text:e.wa}:e.tgHtml?{text:e.tg,parseModeHtml:!0}:{text:e.tg}}function ee(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function Re(e){return e.replace(/[*_~`]/g,t=>({"*":"\u2217",_:"\uFF3F","~":"\u02DC","`":"\u2032"})[t]??t)}function my(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 hy(e){let t=[];for(let n of e)switch(n.kind){case"title":t.push("",`<b>${ee(n.text)}</b>`,"");break;case"sub":t.push("",`<i>${ee(n.text)}</i>`,"");break;case"gap":t.push("");break;case"p":t.push(ee(n.text));break;case"bullet":t.push(`\u2022 ${ee(n.text)}`);break}return t.join(`
|
|
37
|
+
`).replace(/^\n+/,"").trimEnd()}function Q(e){return{wa:my(e),tg:hy(e),tgHtml:!0}}function Vn(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 auto file, video, or page\u2192markdown (background; /dl help)"},{kind:"bullet",text:"/dlf <url> \u2014 force file \xB7 /dlv <url> \u2014 force video (yt-dlp)"},{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 Bo(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 auto: file (HTTP), video (yt-dlp ~1000 sites), or HTML\u2192markdown"},{kind:"bullet",text:"/dlf <url> \u2014 force HTTP file \xB7 /dlv <url> \u2014 force yt-dlp video"},{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 auto /dl (file, video, or markdown)":"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 Eu(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 Au(){return[{kind:"title",text:"Chat config"},{kind:"p",text:`Same trust as shell \u2014 allowlisted senders can change many keys saved to ${W}.`},{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 pa(){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: ${W}`}]}function Iu(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(W)}`);let n=["<b>Gateway status</b>","",`<b>${ee(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> ${ee(e.updateBrief)}`,""),n.push("<i>Change:</i> /gateway both \xB7 /gw tg","<i>/updates</i> \u2014 npm + optional notice URL",ee(`Config: ${W}`)),ge(t.join(`
|
|
38
38
|
`),n.join(`
|
|
39
|
-
`))}function
|
|
39
|
+
`))}function jo(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>${ee(e.runningVersion)}</code>`,`<b>Checked (UTC)</b> ${ee(e.checkedAtIso)}`,`<b>npm package</b> <code>${ee(e.registryPackage)}</code>`];return e.registryError?n.push("",`<b>Registry</b> ${ee(e.registryError)}`):e.registryLatest&&n.push("",e.updateAvailable?`<b>Newer on npm</b> <code>${ee(e.registryLatest)}</code>`:`<b>npm latest</b> <code>${ee(e.registryLatest)}</code> (current or newer here)`),e.infoError?n.push("",`<b>Notice URL</b> ${ee(e.infoError)}`):e.infoMessage&&(n.push("",`<b>Notice</b> ${ee(e.infoMessage)}`),e.infoLink&&n.push(ee(e.infoLink))),n.push("","<i>Config keys:</i> updateCheckEnabled, updateCheckIntervalMs, updateCheckPackageName, updateInfoUrl"),ge(t.join(`
|
|
40
40
|
`),n.join(`
|
|
41
|
-
`))}function
|
|
41
|
+
`))}function Lu(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 ${W}`},{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 Ou(e){let t=!!Pe(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 ${W}, 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 ${W}; 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 Nu(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>${ee(o.label)}</b> (${o.items.length})`,ee(r),"")}return ge(t.join(`
|
|
42
42
|
`).trimEnd(),n.join(`
|
|
43
|
-
`).trimEnd())}function
|
|
43
|
+
`).trimEnd())}function ma(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>${ee(r.label)}</b> (${r.items.length})`,ee(s),"")}return ge(n.join(`
|
|
44
44
|
`).trimEnd(),o.join(`
|
|
45
|
-
`).trimEnd())}function
|
|
46
|
-
`),n=["<b>Token saved</b>","","telegramBotToken written to config (not echoed).",
|
|
45
|
+
`).trimEnd())}function Fu(e){let t=["*Token saved*","","telegramBotToken written to config (not echoed).",`Config: ${Re(W)}`,...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).",ee(`Config: ${W}`),...e.map(o=>`<i>${ee(o)}</i>`),"","Send /reload so the gateway picks it up."].join(`
|
|
47
47
|
`)+`
|
|
48
|
-
`;return
|
|
48
|
+
`;return ge(t.trimEnd(),n.trimEnd())}function _u(){return Q([{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 Wu(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
|
-
${
|
|
54
|
+
${ee(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
58
|
"${Re(e)}"
|
|
59
|
-
${Re(
|
|
59
|
+
${Re(W)}${o}`,i=`<b>gatewayMode saved</b>
|
|
60
60
|
|
|
61
|
-
<code>${
|
|
62
|
-
${
|
|
63
|
-
`),a=["<b>Shortcuts</b>",r,"",...e.map(l=>{let u=`${s?l.scope==="chat"?"[chat] ":"[global] ":""}${l.name}`;return`\u2022 <code>${
|
|
64
|
-
`);return
|
|
61
|
+
<code>${ee(e)}</code>
|
|
62
|
+
${ee(W)}${r}`;return ge(s,i)}function Du(){return Q([{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 Uu(){return Q([{kind:"title",text:"/deny \u2014 remove from allowlist"},{kind:"p",text:"Same forms as /allow: +E164 or tg:id"}])}function Hu(){return Q([{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 ha(){return Q([{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 fa(){return Q([{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 (${W}) 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 ga(){return Q([{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 Bu(e,t){let n=zr(t),o=ie(t),r="";try{r=en(e,t)}catch(s){r=`(${String(s)})`}return Q(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 ju(){return Q([{kind:"title",text:"Unknown /wa command"},{kind:"p",text:"Send /wa help for setup."}])}function Gu(){return Q([{kind:"title",text:"Unknown /tg command"},{kind:"p",text:"Send /tg help or /tg token <botfather_token>."}])}function Ju(){return Q([{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 qu(e){return Q([{kind:"title",text:"Unknown command"},{kind:"p",text:`Try /help or ${e.commandPrefix}<command>.`},{kind:"gap"},...Vn(e)])}function fy(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 zu(e,t){let n=t?fy(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"},...Vn(e)),Q(o)}function Ku(){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"},...pa()];return Q(e)}function ya(){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 Yu(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 u=`${s?l.scope==="chat"?"[chat] ":"[global] ":""}${l.name}`;return`\u2022 <code>${ee(u)}</code> \u2192 ${ee(l.body)}`})].join(`
|
|
64
|
+
`);return ge(i,a)}function Go(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
|
|
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 Vu(e){return p(`Unknown shortcut: ${e}`)}function Xu(e,t){return p(`Unknown shortcut "${e}" in ${t==="global"?"shared shortcuts":"this chat"}.`)}function wa(e,t,n){let o=n?`
|
|
67
67
|
|
|
68
68
|
${n}`:"";return p(`${e}
|
|
69
|
-
\u2192 ${t}${o}`)}function
|
|
69
|
+
\u2192 ${t}${o}`)}function Zu(){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: "+Pr},{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 gy(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 ed(e){let t=["*Recipes*","`/run <name> <task>`",""],n=["<b>Recipes</b>","<code>/run <name> <task></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 ${ee(r.description)}`:"",c=s&&r.dangerous?ee(" [dangerous flags]"):"";n.push(`\u2022 ${ee(r.name)} \u2014 ${ee(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(ee("(no recipes \u2014 add host file or /run add)"))),ge(t.join(`
|
|
70
70
|
`).trimEnd(),n.join(`
|
|
71
|
-
`).trimEnd())}function
|
|
72
|
-
`))}function
|
|
73
|
-
`))}function
|
|
71
|
+
`).trimEnd())}function ba(e,t){let n=e.taskEnv??"OMNISH_TASK",o=[`Recipe: ${e.name}`,`Source: ${gy(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 Jo(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 td(e,t="chat"){return p(`Recipe removed (${t==="global"?"gateway-shared storage":"this chat"}): ${e}`)}function nd(e,t){return p(`Unknown recipe "${e}" in ${t==="global"?"gateway-shared storage":"this chat storage"}.`)}function od(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 ${ee(i.description)}`:"";s.push(`\u2022 ${ee(i.name)} \u2014 ${ee(i.label??i.name)}${l}`)}return ge(r.join(`
|
|
74
74
|
`).trimEnd(),s.join(`
|
|
75
|
-
`).trimEnd())}function
|
|
76
|
-
/run list`)}function
|
|
77
|
-
`)}function
|
|
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
|
|
75
|
+
`).trimEnd())}function ka(e){return p(`Unknown recipe: ${e}
|
|
76
|
+
/run list`)}function va(){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 yy(e){let{errors:t,warns:n,infos:o}=da(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:Re(`[${c}] ${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 rd(e){return Q(yy(e))}function sd(){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 id(){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 Te(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function Xn(e){let t=e.trim();return t?t.length<=8?"(set)":`${t.slice(0,4)}\u2026${t.slice(-4)}`:"(empty)"}function ad(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 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}var ld=new Set(["downloads","omnishData","sessionCwd","processCwd","fixed"]),sn=["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 cd(e){return sn.includes(e)}function wy(e){let t=Pe(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: ${Xn(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: ${Xn(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||"(Downloads/Omnish or session cwd via /receive here)"}`,`mediaMaxBytes: ${e.mediaMaxBytes}`,`mediaWhisperModel: ${e.mediaWhisperModel}`,`progressUpdates: ${e.progressUpdates}`,"",`File: ${W}`].join(`
|
|
77
|
+
`)}function by(e){let t=Pe(e);return["<b>Config</b> (secrets masked)","",`<b>gatewayMode</b> ${Te(e.gatewayMode)}`,`<b>commandPrefix</b> ${Te(e.commandPrefix)}`,`<b>shell</b> ${Te(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: ${Te(e.clusterRole)}`,`clusterLabel: ${Te(e.clusterLabel||"(hostname)")}`,`clusterSenderBindings: ${Object.keys(e.clusterSenderBindings??{}).length} entries`,"","<b>Telegram</b>",`telegramBotToken: ${Te(Xn(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>",`${Te(e.fileReceiveRootMode)} \xB7 inbox ${Te(e.fileInboxSubdir)}`,"","<b>Service (chat)</b>",`serviceInstallFromChat: ${e.serviceInstallFromChat}`,"","<b>Updates (optional)</b>",`updateCheckEnabled: ${e.updateCheckEnabled} \xB7 interval ms: ${e.updateCheckIntervalMs}`,`package: <code>${Te(e.updateCheckPackageName)}</code> \xB7 info URL: ${e.updateInfoUrl?"set":"empty"}`,"","<b>Tunneling (chat /tunnel)</b>",`tunnelEnabled: ${e.tunnelEnabled} \xB7 relay <code>${Te(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: ${Te(e.chatLlmWorkDir||"(empty)")}`,"","<b>Webhook receiver</b>",`enabled: ${e.webhookEnabled} \xB7 ${Te(e.webhookHost)}:${e.webhookPort} \xB7 token: ${Te(Xn(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>${Te(W)}</code>`].join(`
|
|
78
|
+
`)}function ud(e,t){if(!cd(t))return null;let n=t;if(n==="telegramBotToken")return`telegramBotToken: ${Xn(Pe(e))}`;if(n==="webhookToken")return`webhookToken: ${Xn(e.webhookToken)}`;let o=e[n];return`${t}: ${typeof o=="object"?JSON.stringify(o):String(o)}`}function ky(e,t){let n=ud(e,t);if(!n)return null;let o=n.split(": ");return o.length<2?Te(n):`<b>${Te(o[0])}</b> ${Te(o.slice(1).join(": "))}`}function ts(e,t){let n=ad(t),o=!1,r=!1,s=!1;if(e==="telegramBotToken"){if(!vt(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=Gn(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=Oe(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[u,d]of Object.entries(l)){if(typeof d!="string"||!d.trim())throw new Error(`clusterSenderBindings.${u}: value must be a non-empty string`);c[u]=d.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=ad(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=Oe(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=Oe(n);if(a===null)throw new Error("appsSkipClearOnPasswordPrompt: true or false");i.appsSkipClearOnPasswordPrompt=a;break}case"appsPasswordPromptHint":{let a=Oe(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(!ld.has(a))throw new Error(`fileReceiveRootMode: ${[...ld].join(" | ")}`);i.fileReceiveRootMode=a;break}case"fileReceiveRootPath":i.fileReceiveRootPath=n.trim().slice(0,4096);break;case"recipesRunAttach":{let a=Oe(n);if(a===null)throw new Error("recipesRunAttach: true or false");i.recipesRunAttach=a;break}case"recipesAllowDangerousBuiltins":{let a=Oe(n);if(a===null)throw new Error("recipesAllowDangerousBuiltins: true or false");i.recipesAllowDangerousBuiltins=a;break}case"serviceInstallFromChat":{let a=Oe(n);if(a===null)throw new Error("serviceInstallFromChat: true or false");i.serviceInstallFromChat=a;break}case"updateCheckEnabled":{let a=Oe(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=Oe(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=Oe(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=Oe(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=Oe(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=Oe(n);if(a===null)throw new Error("watchEnabled: true or false");return N({watchEnabled:a}),Ke(),{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}),Ke(),{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}),Ke(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"watchAutoRestore":{let a=Oe(n);if(a===null)throw new Error("watchAutoRestore: true or false");return N({watchAutoRestore:a}),Ke(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaSendFiles":{let a=Oe(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=Oe(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=Oe(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=Oe(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 Ge(i),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}async function dd(e,t){let n=e.trim(),o=n.toLowerCase();if(!n||o==="help")return Q(Au());if(o==="keys"||o==="help keys")return p(["*Configurable keys* (/config set)","",sn.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 ge(wy(i),by(i))}let r=n.match(/^get\s+(\S+)\s*$/i);if(r){let i=r[1],a=S(),l=ud(a,i);if(!l)return p(`Unknown key "${i}". /config keys`);let c=ky(a,i);return ge(`${l}
|
|
80
80
|
|
|
81
|
-
${
|
|
81
|
+
${W}`,`${c}<br/><br/><code>${Te(W)}</code>`)}let s=n.match(/^set\s+(\S+)\s+([\s\S]+)$/i);if(s){let i=s[1],a=s[2]??"";if(!cd(i))return p(`Unknown key "${i}". /config keys`);try{let{reloadSuggested:l,shellWarning:c,tokenSaved:u}=ts(i,a),d="";if(t?.reload&&l){let y=await t.reload();d=y.ok?`
|
|
82
82
|
Reload: ${y.summary}`:`
|
|
83
|
-
Reload failed: ${y.error}`}else l?
|
|
84
|
-
Send /reload while omnish run is active (gatewayMode / Telegram).`:
|
|
85
|
-
Send /reload to pick up changes where applicable.`;let m=
|
|
83
|
+
Reload failed: ${y.error}`}else l?d=`
|
|
84
|
+
Send /reload while omnish run is active (gatewayMode / Telegram).`:d=`
|
|
85
|
+
Send /reload to pick up changes where applicable.`;let m=c?`
|
|
86
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}
|
|
87
|
+
telegramBotToken saved (not echoed).`:"",f=`Set *${i}* (saved).${h}${m}${d}`,g=`<b>Set ${Te(i)}</b> (saved).${h?"<br/>token saved (not echoed).":""}${c?"<br/>\u26A0 shell path changed.":""}${Te(d)}`;return ge(f,g)}catch(l){return p(`Error: ${String(l)}`)}}return p("Unknown /config command. Try /config help or /config show")}ue();var Sa=[...sn,"platformToken","platformDeviceId"],xa={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 Sa)xa[e]=e;function ns(e){let t=e.trim().toLowerCase().replace(/-/g,"_");return xa[t]??null}function pd(){return Object.keys(xa).sort()}function Ne(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 vy(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 A[e]}function Zn(e,t){let n=vy(t),o=S();switch(e){case"platformToken":return N({platformToken:n});case"platformDeviceId":return N({platformDeviceId:n});case"gatewayMode":{let r=Gn(n);if(!r)throw new Error('gatewayMode: "whatsapp", "telegram", or "both"');return o.gatewayMode=r,Ge(o),o}case"telegramBotToken":if(!vt(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=Ne(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=Ne(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=Ne(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=Ne(n);if(r===null)throw new Error("appsSkipClearOnPasswordPrompt: true or false");o.appsSkipClearOnPasswordPrompt=r}else if(e==="appsPasswordPromptHint"){let r=Ne(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=Ne(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=Ne(n);if(r===null)throw new Error("recipesRunAttach: true or false");o.recipesRunAttach=r}else if(e==="serviceInstallFromChat"){let r=Ne(n);if(r===null)throw new Error("serviceInstallFromChat: true or false");o.serviceInstallFromChat=r}else if(e==="updateCheckEnabled"){let r=Ne(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=Ne(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=Ne(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=Ne(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=Ne(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=Ne(n);if(r===null)throw new Error("watchAutoRestore: true or false");return N({watchAutoRestore:r})}else if(e==="mediaSendFiles"){let r=Ne(n);if(r===null)throw new Error("mediaSendFiles: true or false");return N({mediaSendFiles:r})}else if(e==="mediaInstallFromChat"){let r=Ne(n);if(r===null)throw new Error("mediaInstallFromChat: true or false");return N({mediaInstallFromChat:r})}else if(e==="mediaUrlAutoDl"){let r=Ne(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=Ne(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 Ge(o),o}function md(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,Ge(n),n}function Rn(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}ue();G();import eo from"node:fs";import $a from"node:os";import qo from"node:path";import yd from"node:crypto";var Ra="\u2063omnish/c v1",xy=/([nlra])=([^\s\]]*)/g;function ht(e){return e.replace(/-/g,"").slice(0,8)}function Ca(e){return e.replace(/[\s\]\r\n]/g,"_").slice(0,64)}function Cy(e){let t=Ca(ht(e.nodeId)),n=Ca(e.label||""),o=e.role==="primary"?"p":"s",r=Ca(e.activeNodeId?ht(e.activeNodeId):"");return`${Ra} [n=${t} l=${n} r=${o} a=${r}]`}function hd(e,t){let n=Cy(t);if(e.includes(n))return e;let o=e.replace(/\s+$/,"");return o.length===0?n:`${o}
|
|
88
88
|
|
|
89
|
-
${n}`}function
|
|
90
|
-
`,{mode:384}),t}function
|
|
91
|
-
`,r
|
|
92
|
-
`);for(let l of a){let
|
|
89
|
+
${n}`}function fd(e){if(!e||!e.includes(Ra))return null;let t=e.indexOf(Ra),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(xy))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 wd=3,Ry="node-id",Ty="cluster-local.json",gd=8;function bd(){return qo.join(D,Ty)}function Pa(){return qo.join(D,Ry)}function at(){B(D);let e=Pa();try{if(eo.existsSync(e)){let n=eo.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=yd.randomUUID();return eo.writeFileSync(e,`${t}
|
|
90
|
+
`,{mode:384}),t}function kd(){return{schemaVersion:wd,updatedAt:new Date().toISOString(),peers:[],senderBindings:{}}}function $y(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 ke(){let e=bd();try{let t=eo.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=$y(n.senderBindings);return{schemaVersion:wd,updatedAt:typeof n.updatedAt=="string"?n.updatedAt:new Date().toISOString(),peers:o,senderBindings:r}}catch{return kd()}}function Py(e,t){let n=qo.dirname(e);B(n);let o=`${JSON.stringify(t,null,2)}
|
|
91
|
+
`,r=qo.join(n,`.${qo.basename(e)}.tmp.${process.pid}.${yd.randomBytes(4).toString("hex")}`);eo.writeFileSync(r,o,{mode:384}),eo.renameSync(r,e)}function Ea(e){let t=bd(),n=kd();for(let o=0;o<gd;o++){n=ke(),e(n),n.updatedAt=new Date().toISOString();try{return Py(t,n),n}catch{}}throw new Error(`Could not write cluster local state after ${gd} attempts: ${t}`)}function vd(e){return(e.clusterLabel??"").trim()||$a.hostname()}function ve(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function Sd(e,t){let n=e.peers.findIndex(o=>o.nodeId===t.nodeId);n>=0?e.peers[n]=t:e.peers.push(t)}function zo(e){return{nodeId:ht(at()),label:vd(e),role:e.clusterRole,lastSeenIso:new Date().toISOString()}}function xd(e,t,n){let o=n.trim();if(!o)return{ok:!1,reason:"not-found"};let r=zo(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 Wt(e,t){let n=ke(),o=n.senderBindings[t];if(o&&o.nodeId)return o;let r=e.clusterSenderBindings?.[t];if(typeof r=="string"&&r.trim()){let s=xd(n,e,r);if(s.ok)return{senderKey:t,nodeId:s.peer.nodeId,sinceIso:new Date(0).toISOString(),source:"config"}}return null}function Ma(e,t,n="chat"){let o=S(),r=ke(),s=xd(r,o,t);if(!s.ok)return{state:r,resolved:s};let i=new Date().toISOString();return{state:Ea(l=>{l.senderBindings[e]={senderKey:e,nodeId:s.peer.nodeId,sinceIso:i,source:n},Sd(l,{...s.peer,lastSeenIso:i})}),resolved:s}}function My(e){let t=null;return{state:Ea(o=>{o.senderBindings[e]&&(t=o.senderBindings[e]??null,delete o.senderBindings[e])}),removed:t}}function Cd(e,t){let n=ht(at()),o=new Date().toISOString();return Ea(r=>{if(Sd(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 Rd(e,t){let n=Wt(t,e);return n?n.nodeId===ht(at()):!1}function Ey(e,t){let n=zo(t).nodeId,o=new Set([n]);for(let s of e.peers)o.add(s.nodeId);return[...o].sort()[0]??n}function Ye(e,t){return Ey(e,t)===ht(at())}function os(e,t,n){let o=ht(at()),r=["*Computers*",""],s=n?Wt(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,zo(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,d=[(s?l.nodeId===s.nodeId:!1)?"your binding":null,c?"you":null].filter(Boolean).join(", "),m=d?` (${d})`:"";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
|
|
96
|
-
`);for(let l of a){let
|
|
97
|
-
`)}function
|
|
98
|
-
`),
|
|
99
|
-
`);return{wa:u,tg:
|
|
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 <label-or-id> \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: ${
|
|
101
|
-
`);return
|
|
102
|
-
`),g=[`<b>Bound to ${
|
|
103
|
-
`);return
|
|
104
|
-
`),u=["<b>Bound to this machine.</b>","","Your messages now route here. Other senders are not affected.","",
|
|
105
|
-
`);return
|
|
106
|
-
`),h=[`<b>Bound to ${
|
|
107
|
-
`);return
|
|
95
|
+
`)}function Ta(e,t,n){let o=ht(at()),r=["<b>Computers</b>",""],s=n?Wt(t,n):null;n&&(s?r.push(`Your binding: <code>${ve(s.nodeId)}</code> (${ve(s.source)})`):r.push("Your binding: (none \u2014 send /c use <label-or-id>)"),r.push(""));let i=new Map;i.set(o,zo(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,d=[(s?l.nodeId===s.nodeId:!1)?"your binding":null,c?"you":null].filter(Boolean).join(", "),m=d?` (${ve(d)})`:"";r.push(`\u2022 <b>${ve(l.label)}</b>${m}<br/> id <code>${ve(l.nodeId)}</code> \xB7 role ${ve(l.role)}<br/> seen ${ve(l.lastSeenIso)}`)}return r.push(""),r.push(`Updated: ${ve(e.updatedAt)}`),r.join(`
|
|
97
|
+
`)}function Aa(e,t,n){return os(e,t,n??null).replace(/\*([^*]+)\*/g,"$1").replace(/`([^`]+)`/g,"$1")}function Ia(e,t){let n=at(),o=ht(n),r=ke(),s=e.clusterRole,i=vd(e),a=t?Wt(e,t):null,l=a?a.nodeId===o:!1,c=t?a?`your binding: ${a.nodeId} (${a.source}${l?", here":""})`:"your binding: (none)":"",u=["*This computer*","",`label: ${i}`,`node id: \`${o}\` (full id in ${Pa()})`,`host: ${$a.hostname()}`,`clusterRole: ${s}`,c,`peers known: ${r.peers.length}`,`senderBindings (chat): ${Object.keys(r.senderBindings).length}`].filter(Boolean).join(`
|
|
98
|
+
`),d=["<b>This computer</b>","",`label: ${ve(i)}`,`node id: <code>${ve(o)}</code> (full id in ${ve(Pa())})`,`host: ${ve($a.hostname())}`,`clusterRole: ${ve(s)}`,t?ve(c):"",`peers known: ${r.peers.length}`,`senderBindings (chat): ${Object.keys(r.senderBindings).length}`].filter(Boolean).join(`
|
|
99
|
+
`);return{wa:u,tg:d}}function Ay(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 <label-or-id> \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: ${ve(t)}`].join(`
|
|
101
|
+
`);return ge(n,o)}function Iy(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 Td(e,t,n){let o=t.trim().split(/\s+/),r=o[0]?.toLowerCase()??"";if(!r||r==="help"){if(!e.clusterEnabled&&r!=="help"){let l=ke();return Ye(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=ke();return Ye(a,e)?Ay(e):null}let s=ht(at());if(r==="use"||r==="bind"){let a=o.slice(1).join(" ").trim();if(!n){let y=ke();return Ye(y,e)?p("/c use is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}if(!a){let y=ke();return Ye(y,e)?p("Usage: /c use <label-or-id>"):null}e.clusterEnabled||Wr(!0);let c=ke().senderBindings[n]??null,{state:u,resolved:d}=Ma(n,a,"chat");if(!d.ok)return Ye(u,e)?Iy(a,d):null;let m=d.peer.nodeId===s,h=c?c.nodeId===s:!1;if(!m)return null;let f=[`*Bound to ${d.peer.label}*`,`id \`${d.peer.nodeId}\``,"","Your messages now route to this machine. Other senders are not affected.","",os(u,e,n)].join(`
|
|
102
|
+
`),g=[`<b>Bound to ${ve(d.peer.label)}</b>`,`id <code>${ve(d.peer.nodeId)}</code>`,"","Your messages now route to this machine. Other senders are not affected.","",Ta(u,e,n)].join(`
|
|
103
|
+
`);return ge(f,g)}if(r==="here"||r==="take"){if(!n){let d=ke();return Ye(d,e)?p("/c here is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}e.clusterEnabled||Wr(!0);let{state:a,resolved:l}=Ma(n,s,"chat");if(!l.ok)return Ye(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.","",os(a,e,n)].join(`
|
|
104
|
+
`),u=["<b>Bound to this machine.</b>","","Your messages now route here. Other senders are not affected.","",Ta(a,e,n)].join(`
|
|
105
|
+
`);return ge(c,u)}if(r==="using"){if(!n){let c=ke();return Ye(c,e)?p("/c using is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}let a=Wt(e,n);if(a){if(a.nodeId!==s)return null;let d=[...ke().peers,zo(e)].find(f=>f.nodeId===a.nodeId)?.label??"(unknown label)",m=[`*Bound to ${d}*`,`id \`${a.nodeId}\` \xB7 source ${a.source}`,a.source==="chat"?`since ${a.sinceIso}`:"(from config)"].join(`
|
|
106
|
+
`),h=[`<b>Bound to ${ve(d)}</b>`,`id <code>${ve(a.nodeId)}</code> \xB7 source ${ve(a.source)}`,a.source==="chat"?`since ${ve(a.sinceIso)}`:"(from config)"].join(`
|
|
107
|
+
`);return ge(m,h)}let l=ke();return Ye(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=ke();return Ye(m,e)?p("/c unuse is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}let l=ke().senderBindings[n]??null,{state:c}=My(n);if(l){if(l.nodeId!==s)return null}else if(!Ye(c,e))return null;let u=Wt(e,n),d=u?`
|
|
108
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.${
|
|
109
|
+
No config default is set; nobody will answer until you /c use <label-or-id>.`;return p(`Cleared your chat binding.${d}`)}if(r==="step-down"||r==="stepdown"){let a=ke();return Ye(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=Ia(e,n);return ge(a.wa,a.tg)}if(r==="list"){let a=ke(),l=n?Wt(e,n):null;if(l){if(l.nodeId!==s)return null}else if(!Ye(a,e))return null;return ge(os(a,e,n??null),Ta(a,e,n??null))}let i=ke();return Ye(i,e)?p(`Unknown /c subcommand "${r}". Try /c help`):null}function $d(e,t){return Wr(!0),Ma(e,t,"chat")}ot();xe();function Ly(e){return`wa:${e}`}function La(e){return`tg:${e}`}function Pd(e){return{peerKey:Ly(e.fromJid),text:e.text,waMessageId:e.messageId,mediaSavedPath:e.mediaSavedPath,mediaError:e.mediaError}}G();function rs(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
|
-
`)}
|
|
113
|
+
`)}ue();import gt from"node:process";G();function ss(){let e=jn,t=dt,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
|
-
`)}
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
`)}ue();import $w from"node:path";G();import Oy from"node:net";import Ny from"node:fs";function Fy(){try{let e=Ny.readFileSync(Bn,"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 Tt(e){let t=Fy();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=Oy.connect({host:t.host,port:t.port},()=>{l.write(o)});l.setTimeout(6e5),l.on("data",c=>{i+=c.toString("utf8");let u=i.indexOf(`
|
|
119
|
+
`);if(u>=0){let d=i.slice(0,u).trim();try{let m=JSON.parse(d);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 Md(e,t,n){return`Step ${e+1}/${t}: ${n}\u2026`}function an(){return{async stepStart(){},async stepFail(){}}}async function Ed(e,t){let n=e.peerKey?.trim();if(n){if(e.sendToPeer){try{await e.sendToPeer(n,t)}catch{}return}Tt({op:"sendPeerText",peerKey:n,text:t}).catch(()=>{})}}function is(e){return!e.enabled||!e.peerKey?.trim()?an():{async stepStart(t,n,o){await Ed(e,Md(t,n,o.label))},async stepFail(t){await Ed(e,`Step failed: ${String(t)}`)}}}function Ad(){return process.env.OMNISH_PEER_KEY?.trim()||null}import as from"node:fs";import Ko from"node:path";import{spawn as _y}from"node:child_process";function to(e,t,n){return new Promise((o,r)=>{let s=_y(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 Id(e){return Ko.join(e,"%(title).200B.%(ext)s")}function Ld(e,t){let n=[],o=r=>{let s;try{s=as.readdirSync(r,{withFileTypes:!0})}catch{return}for(let i of s){let a=Ko.join(r,i.name);if(i.isDirectory())o(a);else if(i.isFile())try{as.statSync(a).mtimeMs>=t-2e3&&n.push(a)}catch{}}};return o(e),n.sort()}async function Wy(e,t){let n=await to(e,["--no-download","--print","title",t],6e4);if(n.code===0)return n.stdout.trim().split(`
|
|
120
|
+
`)[0]?.trim()||void 0}async function Dt(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");as.mkdirSync(s,{recursive:!0});let a=Date.now(),l=Id(s),c=Math.min(9e5,Math.max(6e4,t.syncTimeoutMs*3)),u=await Wy(i,o),d=[],m=[],h=[];t.mediaMaxBytes>0&&h.push("--max-filesize",String(Math.floor(t.mediaMaxBytes)));let f=async b=>{let k=await to(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",Ko.dirname(n.ffmpeg),...h,"-o",l,o])}else if(b==="audio"){let T=n.ffmpeg?["--ffmpeg-location",Ko.dirname(n.ffmpeg)]:[];await f(["-x","--audio-format","m4a",...T,...h,"-o",l,o])}else b==="subs"&&await f(["--skip-download","--write-subs","--write-auto-subs","--sub-langs","en.*,en","-o",l,o]);d.push(...Ld(s,k))}let y=[...new Set(d)];return{title:u,outputDir:s,files:y,log:m.join(`
|
|
120
122
|
---
|
|
121
|
-
`)}}async function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
`)}}
|
|
125
|
-
|
|
123
|
+
`)}}async function Od(e){let{tools:t,url:n,outputDir:o,timeoutMs:r}=e,s=t.ytDlp;if(!s)throw new Error("yt-dlp not found.");as.mkdirSync(o,{recursive:!0});let i=Id(o),a=Date.now(),l=t.ffmpeg?["--ffmpeg-location",Ko.dirname(t.ffmpeg)]:[],c=await to(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 u=Ld(o,a).filter(d=>/\.(m4a|mp3|opus|webm|wav)$/i.test(d));if(u.length===0)throw new Error("No audio file produced.");return u[0]}G();import{spawnSync as Fa}from"node:child_process";import ye from"node:fs";import Dy from"node:os";import Qe from"node:path";var Uy="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp",Hy="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe";function Fd(){return process.platform==="win32"?"yt-dlp.exe":"yt-dlp"}function _d(){return process.platform==="win32"?"ffmpeg.exe":"ffmpeg"}function Oa(){return process.platform==="win32"?"whisper.exe":"whisper"}function oo(e){return Qe.join(jn,e)}function Na(){return process.platform==="win32"?Qe.join(dt,"Scripts","whisper.exe"):Qe.join(dt,"bin","whisper")}function Tn(e){try{return!ye.existsSync(e)||!ye.statSync(e).isFile()?!1:(process.platform==="win32"||ye.accessSync(e,ye.constants.X_OK),!0)}catch{return!1}}function no(e){let t=process.platform==="win32"?"where":"which",n=Fa(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&&Tn(o)?o:null}function Nd(e,t,n){let o=e.trim();if(o&&Tn(o))return o;let r=oo(t);if(Tn(r))return r;for(let s of n){let i=no(s);if(i)return i}return null}function et(e){let t=Nd(e.pullYtDlpPath,Fd(),["yt-dlp","yt-dlp.exe"]),n=Nd(e.pullFfmpegPath,_d(),["ffmpeg","ffmpeg.exe"]),o=e.pullWhisperPath.trim(),r=null;o&&Tn(o)?r=o:Tn(oo(Oa()))?r=oo(Oa()):Tn(Na())?r=Na():r=no(process.platform==="win32"?"whisper.exe":"whisper");let s=process.platform==="win32"?no("python")??no("py"):no("python3")??no("python");return{ytDlp:t,ffmpeg:n,whisper:r,python:s}}function cs(e){let t=et(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 ls(e,t){B(Qe.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());ye.writeFileSync(t,o),process.platform!=="win32"&&ye.chmodSync(t,493)}function Yo(e,t,n={}){if(Fa(e,t,{stdio:"inherit",cwd:n.cwd,timeout:6e5,windowsHide:!0}).status!==0)throw new Error(`Command failed: ${e} ${t.join(" ")}`)}async function By(){let e=oo(Fd()),t=process.platform==="win32"?Hy:Uy;return await ls(t,e),e}async function jy(){let e=oo(_d());B(jn);let t=Qe.join(D,"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",c=Qe.join(t,"ffmpeg.zip");await ls(l,c),Yo("powershell",["-NoProfile","-Command",`Expand-Archive -Force -Path '${c.replace(/'/g,"''")}' -DestinationPath '${t.replace(/'/g,"''")}'`],{cwd:t});let d=ye.readdirSync(t,{withFileTypes:!0}).find(h=>h.isDirectory()&&h.name.startsWith("ffmpeg"))?.name;if(!d)throw new Error("Could not find ffmpeg folder in archive.");let m=Qe.join(t,d,"bin","ffmpeg.exe");if(!ye.existsSync(m))throw new Error("ffmpeg.exe not found in archive.");return ye.copyFileSync(m,e),e}if(process.platform==="darwin"){let l="https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip",c=Qe.join(t,"ffmpeg.zip");await ls(l,c),Yo("unzip",["-o",c,"-d",t],{cwd:t});let u=Qe.join(t,"ffmpeg");if(!ye.existsSync(u))throw new Error("ffmpeg binary not found after unzip.");return ye.copyFileSync(u,e),ye.chmodSync(e,493),e}let n=Dy.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=Qe.join(t,"ffmpeg.tar.xz");await ls(o,r),Yo("tar",["-xf",r,"-C",t],{cwd:t});let i=ye.readdirSync(t).filter(l=>l.startsWith("ffmpeg"))[0];if(!i)throw new Error("Could not find ffmpeg directory in tarball.");let a=Qe.join(t,i,"ffmpeg");if(!ye.existsSync(a))throw new Error("ffmpeg binary not found in tarball.");return ye.copyFileSync(a,e),ye.chmodSync(e,493),e}function Gy(){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(Fa(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 Jy(){B(Qe.dirname(dt));let e=Gy();if(ye.existsSync(dt))try{ye.rmSync(dt,{recursive:!0,force:!0})}catch{}Yo(e.cmd,[...e.args,"-m","venv",dt]);let t=process.platform==="win32"?Qe.join(dt,"Scripts","pip.exe"):Qe.join(dt,"bin","pip");Yo(t,["install","-U","pip","openai-whisper"]);let n=Na();if(!Tn(n))throw new Error("Whisper install finished but whisper executable was not found.");let o=oo(Oa());try{ye.existsSync(o)&&ye.unlinkSync(o),ye.symlinkSync(n,o)}catch{ye.copyFileSync(n,o),process.platform!=="win32"&&ye.chmodSync(o,493)}return n}async function us(e){let t=[],n=e.whisper?3:2,o=0;e.progress&&await e.progress.stepStart(o++,n,"Installing yt-dlp");let r=await By();t.push(`yt-dlp \u2192 ${r}`);let s;e.progress&&await e.progress.stepStart(o++,n,"Installing ffmpeg");try{s=await jy(),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 Jy(),t.push(`whisper \u2192 ${i}`)),{ytDlp:r,ffmpeg:s,whisper:i,messages:t}}var qy=/^https?:\/\/\S+$/i;function Qo(e){let t=e.trim();if(!qy.test(t))return!1;try{return He(t),!0}catch{return!1}}function He(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}tt();import ms from"node:fs";import Ba from"node:path";import Hd from"node:fs";var Wd=Number(process.env.PLATFORM_INBOUND_MEDIA_MAX_BYTES)>0?Number(process.env.PLATFORM_INBOUND_MEDIA_MAX_BYTES):54525952,Dd=50;function Ud(e){try{return JSON.parse(e)}catch{return null}}function Ut(e){return e.fileSendMaxBytes<=0?8388608:Math.min(e.fileSendMaxBytes,8388608)}function Ua(e){if(Hd.statSync(e.absPath).size>8388608)throw new Error(`File too large for attached mode (max ${8388608} bytes).`);let n=Hd.readFileSync(e.absPath).toString("base64");return{name:e.displayName,mimetype:e.mimetype,category:e.category,dataBase64:n,...e.caption?{caption:e.caption}:{}}}function ps(e,t,n,o){if(t.kind==="texts")return{body:t.bodies.map(s=>de(s,o).text).filter(s=>s.trim()).join(`
|
|
125
|
+
|
|
126
|
+
`),...n?{messageId:n}:{}};if(t.kind==="text")return{body:de(t.body,o).text,...n?{messageId:n}:{}};if(t.kind==="bundle"){let r=(t.texts??[]).map(i=>de(i,o).text).filter(i=>i.trim()),s=(t.files??[]).map(Ua);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:[Ua(t.spec)]}:{...n?{messageId:n}:{},files:t.specs.map(Ua)}}var Ha=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"]),Yy=new Set(["html","htm","php","asp","aspx","jsp","cgi"]);function Xo(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 Bd(e){let t=typeof e=="string"?He(e):e,n=Xo(t);return!n||Yy.has(n)?!1:Ha.has(n)}var Qy="omnish/1.0 (+https://omnish.dev)";function Vy(e){let t=[];return e.mediaMaxBytes>0&&t.push(e.mediaMaxBytes),e.mediaSendFiles&&(e.fileSendMaxBytes>0&&t.push(e.fileSendMaxBytes),oe()&&t.push(Ut(e))),t.length===0?0:Math.min(...t)}function io(e){let n=Ba.basename(e.replace(/\\/g,"/")).replace(/[^\w.\-()+@]/g,"_").replace(/^\.+/,"");return!n||n==="_"?"":n.slice(0,200)}function Xy(e){if(!e)return null;let t=/filename\*\s*=\s*[^']*'[^']*'([^;]+)/i.exec(e);if(t?.[1])try{return io(decodeURIComponent(t[1].trim()))}catch{return io(t[1].trim())}let n=/filename\s*=\s*("([^"]+)"|([^;\s]+))/i.exec(e),o=n?.[2]??n?.[3];return o?io(o.trim()):null}function Zy(e,t){let n=Xy(t);if(n)return n;let o=io(decodeURIComponent(e.pathname.split("/").pop()??""));if(o)return o;let r=Xo(e);return`download-${Date.now()}${r?`.${r}`:""}`}async function jd(e){let t=He(e.url),n=Vy(e.cfg),o=await fetch(t.href,{redirect:"follow",headers:{"User-Agent":Qy,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 d=Number(r);if(Number.isFinite(d)&&d>n)throw new Error(`File too large (${d} bytes; max ${n}).`)}let s=Zy(t,o.headers.get("content-disposition"));ms.mkdirSync(e.outputDir,{recursive:!0});let i=Ba.join(e.outputDir,s),a=o.body;if(!a)throw new Error("Empty response body.");let l=ms.createWriteStream(i,{mode:384}),c=a.getReader(),u=0;try{for(;;){let{done:d,value:m}=await c.read();if(d)break;if(m?.length){if(u+=m.length,n>0&&u>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(d){l.close();try{ms.unlinkSync(i)}catch{}throw d}if(await new Promise((d,m)=>{l.end(h=>h?m(h):d())}),u===0){try{ms.unlinkSync(i)}catch{}throw new Error("Download returned no data.")}return Ba.resolve(i)}async function Ht(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 Gd from"node:fs";import Jd from"node:path";var ew=new Set(["jpg","jpeg","png","gif","webp","bmp","tif","tiff","heic","avif"]),tw=new Set(["mp4","mov","webm","mkv","avi","m4v","3gp"]),nw=new Set(["mp3","ogg","opus","wav","m4a","flac","aac","wma"]),hs={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 ow(e){let t=e.replace(/^\./,"").toLowerCase();return ew.has(t)?{category:"image",mimetype:hs[t]??"image/jpeg"}:tw.has(t)?{category:"video",mimetype:hs[t]??"video/mp4"}:nw.has(t)?{category:"audio",mimetype:hs[t]??"audio/mpeg"}:{category:"document",mimetype:hs[t]??"application/octet-stream"}}function ft(e,t){let n;try{n=Gd.realpathSync(e)}catch{return{error:"File not found or unreadable."}}let o;try{o=Gd.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=Jd.basename(n),s=Jd.extname(n).slice(1),{category:i,mimetype:a}=ow(s);return{absPath:n,category:i,mimetype:a,displayName:r}}tt();function rw(e,t){let n=oe()?Ut(e):e.fileSendMaxBytes,o=[];for(let r of t){let s=ft(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(u=>typeof u=="string"&&u.length>0),i=[...o];if(r&&i.push(r),!t.mediaSendFiles){let u=s.length?s.map(d=>` ${d}`):[" (no files)"];return{kind:"text",body:p([...i,"Files:",...u].filter(Boolean).join(`
|
|
129
|
+
`))}}let a=rw(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(u=>p(u)):void 0,files:a}}import qd from"node:fs";import sw from"node:path";var iw="omnish/1.0 (+https://omnish.dev)",zd=2*1024*1024,Kd=48e3;function aw(e){return e.replace(/<script[\s\S]*?<\/script>/gi,"").replace(/<style[\s\S]*?<\/style>/gi,"").replace(/<nav[\s\S]*?<\/nav>/gi,"").replace(/<footer[\s\S]*?<\/footer>/gi,"").replace(/<header[\s\S]*?<\/header>/gi,"")}function ao(e){return e.replace(/ /gi," ").replace(/&/gi,"&").replace(/</gi,"<").replace(/>/gi,">").replace(/"/gi,'"').replace(/'/gi,"'")}function lw(e){let t=aw(e);t=t.replace(/<title[^>]*>([\s\S]*?)<\/title>/gi,(n,o)=>`# ${ao(o.trim())}
|
|
132
|
+
|
|
133
|
+
`);for(let n=1;n<=6;n+=1){let o=new RegExp(`<h${n}[^>]*>([\\s\\S]*?)<\\/h${n}>`,"gi");t=t.replace(o,(r,s)=>`${"#".repeat(n)} ${ao(s.replace(/<[^>]+>/g,"").trim())}
|
|
134
|
+
|
|
135
|
+
`)}return t=t.replace(/<a[^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi,(n,o,r)=>`[${ao(r.replace(/<[^>]+>/g,"").trim())||o}](${o})`),t=t.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi,(n,o)=>`- ${ao(o.replace(/<[^>]+>/g,"").trim())}
|
|
136
|
+
`),t=t.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi,(n,o)=>`${ao(o.replace(/<[^>]+>/g,"").trim())}
|
|
137
|
+
|
|
138
|
+
`),t=t.replace(/<br\s*\/?>/gi,`
|
|
139
|
+
`),t=t.replace(/<[^>]+>/g,""),t=ao(t),t=t.replace(/\n{3,}/g,`
|
|
140
|
+
|
|
141
|
+
`).trim(),t.length>Kd&&(t=`${t.slice(0,Kd)}
|
|
142
|
+
|
|
143
|
+
\u2026 (truncated)`),t}async function Yd(e){let t=He(e.url),n=await fetch(t.href,{redirect:"follow",headers:{"User-Agent":iw,Accept:"text/html,application/xhtml+xml"}});if(!n.ok)throw new Error(`Page fetch failed (HTTP ${n.status}): ${e.url}`);let o=n.body?.getReader();if(!o)throw new Error("Empty response body.");let r=[],s=0;for(;;){let{done:c,value:u}=await o.read();if(c)break;if(u?.length){if(s+=u.length,s>zd)throw await o.cancel().catch(()=>{}),new Error(`Page too large (max ${zd} bytes).`);r.push(u)}}let i=Buffer.concat(r).toString("utf8"),a=lw(i);if(!a.trim())throw new Error("Could not extract readable content from page.");let l;if(a.length>12e3){qd.mkdirSync(e.outputDir,{recursive:!0});let c=io(t.hostname.replace(/\./g,"-")+".md")||"page.md";l=sw.join(e.outputDir,c),qd.writeFileSync(l,a,{encoding:"utf8",mode:384})}return{markdown:a,savedPath:l}}function Qd(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}import cw from"node:fs";import Vd from"node:path";function un(e,t,n){let o=e.mediaOutputDir.trim(),r;return o?r=Vd.isAbsolute(o)?o:Vd.resolve(t,o):n&&zr(n)==="sessionCwd"?r=t:r=Yr(),cw.mkdirSync(r,{recursive:!0}),r}var uw=2e4,dw=new Set(["generic","genericembed","htm5","html5","html5media","mime"]);function pw(e){let t=e.trim().toLowerCase();return!t||dw.has(t)}async function Xd(e,t,n=uw){let r=et(e).ytDlp;if(!r)return{supported:!1,reason:"no_tool"};try{let s=await to(r,["--simulate","--no-warnings","--no-playlist","--print","%(extractor_key)s",t],n);if(s.code!==0)return{supported:!1,reason:"unsupported",detail:(s.stderr||s.stdout).trim().slice(-500)};let i=s.stdout.trim().split(`
|
|
144
|
+
`).pop()?.trim()??"";return i?pw(i)?{supported:!1,reason:"weak",detail:i}:{supported:!0,extractorKey:i}:{supported:!1,reason:"unsupported"}}catch(s){return{supported:!1,reason:"error",detail:String(s)}}}var mw=[/^application\/pdf\b/i,/^application\/zip\b/i,/^application\/x-zip/i,/^application\/octet-stream\b/i,/^application\/vnd\./i,/^application\/msword\b/i,/^application\/json\b/i,/^image\//i,/^audio\//i,/^video\/(?!text)/i],hw="omnish/1.0 (+https://omnish.dev)";function fw(e){let t=e.pathname.toLowerCase();if(/\/pdf(?:\/|$)/i.test(t)||/\/download(?:\/|$)/i.test(t))return!0;let n=Xo(e);return!!(n&&Ha.has(n))}function gw(e){if(!e)return!1;let t=e.split(";")[0]?.trim()??"";return mw.some(n=>n.test(t))}function yw(e){if(!e)return!1;let t=e.split(";")[0]?.trim().toLowerCase()??"";return t==="text/html"||t==="application/xhtml+xml"}async function ww(e,t){let n=He(e),o=new AbortController,r=setTimeout(()=>o.abort(),t);try{return(await fetch(n.href,{method:"HEAD",redirect:"follow",headers:{"User-Agent":hw,Accept:"*/*"},signal:o.signal})).headers.get("content-type")}catch{return null}finally{clearTimeout(r)}}async function Zd(e,t,n={}){if(n.force==="file")return"file";if(n.force==="video")return"video";let o=He(t);if(Bd(o)||fw(o))return"file";let r=n.allowHead!==!1,s=null;return r&&(s=await ww(t,5e3),gw(s))?"file":(await Xd(e,t)).supported?"video":yw(s)||s===null&&r?"html":"file"}async function ep(e){He(e.url);let t=un(e.cfg,e.sessionCwd,e.peerKey),n=e.reporter??an(),o=await Zd(e.cfg,e.url,e.classify??{});if(o==="file"){let[c]=await Ht(n,[{label:"Downloading file",run:async()=>jd({cfg:e.cfg,url:e.url,outputDir:t})}]);return cn({cfg:e.cfg,files:[c],textLines:[`Saved: ${c}`,`Output: ${t}`]})}if(o==="html"){let[c]=await Ht(n,[{label:"Fetching page",run:async()=>Yd({url:e.url,outputDir:t})}]),u=Math.min(e.cfg.appsMaxWaChars||3500,4e3),d=Qd(c.markdown,u),m=[`Source: ${e.url}`];c.savedPath&&m.push(`Saved: ${c.savedPath}`);let h=d.map((f,g)=>p(g===0?`${m.join(`
|
|
145
|
+
`)}
|
|
146
|
+
|
|
147
|
+
${f}`:f));return c.savedPath&&e.cfg.mediaSendFiles?cn({cfg:e.cfg,files:[c.savedPath],textLines:m}):{kind:"bundle",texts:h}}let r=et(e.cfg);if(!r.ytDlp)return{kind:"text",body:p("yt-dlp missing. Run: omnish pull install")};if(!r.ffmpeg)return{kind:"text",body:p("ffmpeg missing. Run: omnish pull install")};let[s]=await Ht(n,[{label:"Downloading video",run:async()=>Dt({cfg:e.cfg,tools:r,url:e.url,mode:"video",outputDir:t})}]),i=s.files.filter(c=>/\.(mp4|mkv|webm|mov)$/i.test(c)),a=i.length?i:s.files,l=[s.title?`Title: ${s.title}`:"",`Saved under: ${t}`].filter(Boolean);return cn({cfg:e.cfg,files:a,textLines:l})}import{spawnSync as bw}from"node:child_process";import kw from"node:fs";import lo from"node:path";import tp from"node:fs";import np from"node:path";function fs(e,t){let n=e.trim();if(!n)throw new Error("URL or file path required.");if(/^https?:\/\//i.test(n))return He(n),{kind:"url",url:n};let o=np.isAbsolute(n)?n:np.resolve(t,n);if(!tp.existsSync(o))throw new Error(`File not found: ${o}`);if(!tp.statSync(o).isFile())throw new Error(`Not a file: ${o}`);return{kind:"file",absPath:o}}function ja(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 op(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=ja(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=ja(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=ja(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 vw(e,t){return t.format?t.format.replace(/^\./,""):t.audioOnly?"m4a":lo.extname(e).replace(/^\./,"")||"mp4"}function Sw(e,t,n,o,r){let s=lo.extname(t).toLowerCase(),i=lo.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=bw(e,a,{encoding:"utf8",timeout:r,windowsHide:!0});if(l.status!==0)throw new Error(`ffmpeg failed:
|
|
148
|
+
${(l.stderr||l.stdout).slice(-2e3)}`);if(!kw.existsSync(n))throw new Error("ffmpeg did not produce output file.")}async function rp(e){let t=et(e.cfg),n=t.ffmpeg;if(!n)return{kind:"text",body:p("ffmpeg missing. Run: omnish pull install")};let{targetRaw:o,opts:r}=op(e.args),s=un(e.cfg,e.sessionCwd,e.peerKey),i=e.reporter??an(),a=fs(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 Ht(i,[{label:"Downloading",run:async()=>{let g=await Dt({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 u=vw(c,r),d=lo.basename(c,lo.extname(c)),m=lo.join(s,`${d}-edit.${u}`),[h]=await Ht(i,[{label:"Editing",run:async()=>(Sw(n,c,m,r,l),m)}]);return cn({cfg:e.cfg,files:[h],textLines:[`Edited: ${h}`]})}import Cw from"node:fs";import{spawnSync as Ga}from"node:child_process";import sp from"node:fs";import Zo from"node:path";function xw(e,t){let n=Zo.basename(t,Zo.extname(t));return[".txt",".srt",".vtt",".json"].map(r=>Zo.join(e,n+r)).filter(r=>sp.existsSync(r))}async function Ja(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(!sp.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=Ga(s,[o,"--model",a,"--output_dir",r,"--output_format","all"],{encoding:"utf8",timeout:i,windowsHide:!0});if(l.status!==0&&n.ffmpeg){let c=Zo.join(r,`_whisper_audio${Zo.extname(o)||".m4a"}`);Ga(n.ffmpeg,["-y","-i",o,"-vn","-ar","16000","-ac","1",c],{encoding:"utf8",timeout:i,windowsHide:!0}).status===0&&(l=Ga(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:
|
|
149
|
+
${(l.stderr||l.stdout).slice(-2e3)}`);return{files:xw(r,o),log:l.stderr||l.stdout||""}}async function qa(e){let{cfg:t,tools:n,url:o,outputDir:r}=e,s=Math.min(9e5,Math.max(12e4,t.syncTimeoutMs*4)),i=await Od({tools:n,url:o,outputDir:r,timeoutMs:s});return Ja({cfg:t,tools:n,inputPath:i,outputDir:r})}function Rw(e){try{return Cw.readFileSync(e,"utf8").trim()}catch{return""}}function Tw(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 ip(e){let t=et(e.cfg);if(!t.whisper)return{kind:"text",body:p("whisper missing. Run: omnish pull install --whisper")};let n=fs(e.targetRaw,e.sessionCwd),o=un(e.cfg,e.sessionCwd,e.peerKey),r=e.reporter??an(),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 Dt({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()=>Ja({cfg:e.cfg,tools:t,inputPath:s,outputDir:o})}),a.push({label:"Preparing results",run:async()=>{}});let l=await Ht(r,a),c=l[l.length-2],u=c.files.find(y=>y.endsWith(".txt")),d=c.files.find(y=>y.endsWith(".srt")||y.endsWith(".vtt")),m=u?Rw(u):"",h=Math.min(e.cfg.appsMaxWaChars,e.cfg.syncMaxBytes)||3500,f=m?Tw(m,h):["(no transcript text produced)"],g=[];return d&&g.push(d),n.kind==="url"&&i&&g.push(i),cn({cfg:e.cfg,files:g,textLines:f})}async function co(e,t){let n=await Tt({op:"sendPeerText",peerKey:e,text:t});if(n)throw new Error(n)}async function za(e,t,n){let o=await Tt({op:"sendPeerMedia",peerKey:e,absPath:t,...n?{caption:n}:{}});if(o)throw new Error(o)}async function ap(e,t,n){let o=e.trim();if(o){if(t.kind==="text"){await co(o,t.body.wa);return}if(t.kind==="file"){n.mediaSendFiles?await za(o,t.spec.absPath,t.spec.caption):await co(o,`Saved: ${t.spec.absPath}`);return}if(t.kind==="files"){if(!n.mediaSendFiles){let r=t.specs.map(s=>` ${s.absPath}`).join(`
|
|
150
|
+
`);await co(o,`Files:
|
|
151
|
+
${r}`);return}for(let r of t.specs)await za(o,r.absPath,r.caption);return}if(t.kind==="bundle"){for(let r of t.texts??[])await co(o,r.wa);if(!n.mediaSendFiles){let r=(t.files??[]).map(s=>` ${s.absPath}`).join(`
|
|
152
|
+
`);r&&await co(o,`Files:
|
|
153
|
+
${r}`);return}for(let r of t.files??[])try{await za(o,r.absPath,r.caption)}catch{await co(o,`Could not send: ${r.absPath}`)}}}}function lp(e){if(e.kind==="text"){process.stdout.write(`${e.body.wa}
|
|
154
|
+
`);return}if(e.kind==="bundle"){for(let t of e.texts??[])process.stdout.write(`${t.wa}
|
|
155
|
+
|
|
156
|
+
`);for(let t of e.files??[])process.stdout.write(`FILE: ${t.absPath}
|
|
157
|
+
`);return}if(e.kind==="file"){process.stdout.write(`FILE: ${e.spec.absPath}
|
|
158
|
+
`);return}if(e.kind==="files")for(let t of e.specs)process.stdout.write(`FILE: ${t.absPath}
|
|
159
|
+
`)}async function Ka(e,t,n){if(lp(e),t)try{await ap(t,e,n)}catch(o){process.stderr.write(`Peer delivery failed: ${String(o)}
|
|
160
|
+
`)}}function Pw(e){let t=e.toLowerCase();return t==="dlf"?{force:"file"}:t==="dlv"?{force:"video"}:{}}async function Ya(e){let[t,n,o]=e,r=(t??"").toLowerCase(),s=S(),i=o?.trim()?$w.resolve(o):process.cwd(),a=Ad(),l=is({peerKey:a,enabled:s.progressUpdates});if(r==="dl"||r==="dlf"||r==="dlv"){await Ka(await ep({cfg:s,url:n??"",sessionCwd:i,peerKey:a??void 0,reporter:l,classify:Pw(r)}),a,s);return}if(r==="tr"){await Ka(await ip({cfg:s,targetRaw:n??"",sessionCwd:i,peerKey:a??void 0,reporter:l}),a,s);return}if(r==="edit"){await Ka(await rp({cfg:s,args:n??"",sessionCwd:i,peerKey:a??void 0,reporter:l}),a,s);return}process.stderr.write(`Unknown media-exec command: ${r}
|
|
161
|
+
`),process.exitCode=1}ue();G();import Mw from"node:fs";import Ew from"node:path";function Aw(e,t){return un(e,t)}async function Iw(e){He(e.url);let t=et(e.cfg),n=Aw(e.cfg,e.sessionCwd),o=[],r;if(e.mode==="transcript"){let a=await qa({cfg:e.cfg,tools:t,url:e.url,outputDir:n});o.push(...a.files)}else if(e.mode==="all"){let a=await Dt({cfg:e.cfg,tools:t,url:e.url,mode:"all",outputDir:n});r=a.title,o.push(...a.files);try{let l=await qa({cfg:e.cfg,tools:t,url:e.url,outputDir:n});o.push(...l.files)}catch{}}else{let a=await Dt({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"&&Mw.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(`
|
|
162
|
+
`)}}async function cp(e){let[t,n,o]=e,r=t??"audio",s=S(),i=o?.trim()?Ew.resolve(o):Gi,a=await Iw({cfg:s,mode:r,url:n??"",sessionCwd:i});process.stdout.write(a.summary+`
|
|
163
|
+
`)}function Qa(){let e=gt.stdout;console.log(Ce(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|dlf|dlv|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 up(e){let t=(e[0]??"").toLowerCase(),n=e.slice(1);if(!t||t==="help"||t==="-h"||t==="--help"){Qa();return}if(t==="doctor"){let o=S(),{text:r}=cs(o);console.log(U(gt.stdout,r));return}if(t==="instructions"||t==="setup"){console.log(ss());return}if(t==="install"){let o=n.some(s=>s==="--whisper"),r=gt.stdout;console.log(U(r,"Installing pull tools into ~/.omnish/bin \u2026"));try{let s=await us({whisper:o});for(let i of s.messages)console.log(U(r,i))}catch(s){console.error(C(gt.stderr,String(s))),gt.exitCode=1}return}console.error(C(gt.stderr,`Unknown subcommand "${t}". Try: omnish pull help`)),gt.exitCode=1}async function dp(e){try{let t=(e[0]??"").toLowerCase();if(t==="dl"||t==="tr"||t==="edit"){await Ya(e);return}await cp(e)}catch(t){console.error(C(gt.stderr,String(t))),gt.exitCode=1}}async function pp(e){try{await Ya(e)}catch(t){console.error(C(gt.stderr,String(t))),gt.exitCode=1}}G();import{execFileSync as dn}from"node:child_process";import uo from"node:fs";import tr from"node:os";import yt from"node:path";import Je from"node:process";function Bt(){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=yt.resolve(t),o=Je.env.OMNISH_HOME?.trim(),r=o?yt.resolve(o):D;return{nodePath:e,scriptPath:n,omnishHome:r}}function mp(e){return/[ "'\\\s]/.test(e)?`"${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`:e}function Lw(e){return`Environment="OMNISH_HOME=${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}function Ow(e){return`[Unit]
|
|
126
164
|
Description=omnish gateway (WhatsApp/Telegram)
|
|
127
165
|
After=network-online.target
|
|
128
166
|
Wants=network-online.target
|
|
129
167
|
|
|
130
168
|
[Service]
|
|
131
169
|
Type=simple
|
|
132
|
-
ExecStart=${`${
|
|
133
|
-
${
|
|
170
|
+
ExecStart=${`${mp(e.nodePath)} ${mp(e.scriptPath)} run`}
|
|
171
|
+
${Lw(e.omnishHome)}
|
|
134
172
|
Restart=on-failure
|
|
135
173
|
RestartSec=5
|
|
136
174
|
|
|
137
175
|
[Install]
|
|
138
176
|
WantedBy=default.target
|
|
139
|
-
`}function
|
|
177
|
+
`}function er(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function Nw(e){let t=tr.homedir(),n=yt.join(e.omnishHome,"logs","launchd-stdout.log"),o=yt.join(e.omnishHome,"logs","launchd-stderr.log");B(yt.dirname(n));let r=er(e.nodePath),s=er(e.scriptPath),i=er(e.omnishHome),a=er(n),l=er(o);return`<?xml version="1.0" encoding="UTF-8"?>
|
|
140
178
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
141
179
|
<plist version="1.0">
|
|
142
180
|
<dict>
|
|
@@ -163,39 +201,40 @@ WantedBy=default.target
|
|
|
163
201
|
<string>${l}</string>
|
|
164
202
|
</dict>
|
|
165
203
|
</plist>
|
|
166
|
-
`}function
|
|
204
|
+
`}function gs(){let e=Bt();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=yt.join(tr.homedir(),"Library/LaunchAgents/dev.omnish.gateway.plist");B(yt.dirname(t));let n=Nw(e);uo.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{dn("launchctl",["bootout",r,t],{stdio:"pipe"})}catch{}return dn("launchctl",["bootstrap",r,t],{stdio:"inherit"}),dn("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=yt.join(tr.homedir(),".config","systemd","user");B(t);let n=yt.join(t,"omnish.service"),o=Ow(e);return uo.writeFileSync(n,o,{mode:420}),dn("systemctl",["--user","daemon-reload"],{stdio:"inherit"}),dn("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 ys(){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=yt.join(tr.homedir(),"Library/LaunchAgents/dev.omnish.gateway.plist"),n=`gui/${typeof Je.getuid=="function"?Je.getuid():0}`;if(uo.existsSync(e)){try{dn("launchctl",["bootout",n,e],{stdio:"pipe"})}catch{}uo.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=yt.join(tr.homedir(),".config","systemd","user","omnish.service");try{dn("systemctl",["--user","disable","--now","omnish.service"],{stdio:"pipe"})}catch{}uo.existsSync(e)&&uo.unlinkSync(e);try{dn("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 hp from"node:fs";var ws=48e3;function bs(e,t){try{if(!hp.existsSync(e))return"(no log file yet \u2014 start with `omnish run -d` or the systemd/LaunchAgent service.)";let n=hp.readFileSync(e),r=(n.length>ws?n.subarray(n.length-ws):n).toString("utf8");return n.length>ws&&(r=`\u2026(truncated to last ${ws} bytes)
|
|
167
205
|
${r}`),r.split(/\r?\n/).slice(-t).join(`
|
|
168
|
-
`).trimEnd()||"(empty)"}catch(n){return`Could not read log: ${String(n)}`}}import
|
|
169
|
-
`,{mode:384})}function
|
|
170
|
-
### `)}function
|
|
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
|
|
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
|
|
173
|
-
`
|
|
174
|
-
|
|
175
|
-
`,
|
|
176
|
-
|
|
206
|
+
`).trimEnd()||"(empty)"}catch(n){return`Could not read log: ${String(n)}`}}import pn from"node:fs";import nb from"node:path";G();import Fw from"node:crypto";import Za from"node:fs";import _w from"node:path";var el=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,mo="__omnish_recipes_global__",tl=new Set(["help","list","show","add","set","remove","rm","del","run","r","online"]),po=new Map,fp=!1;function Ww(){try{let e=Za.readFileSync(Mr,"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]=Ve(i))}po.set(n,r)}}catch{}}function Dw(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 Ve(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=Dw(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 vs(){B(_w.dirname(Mr));let e={};for(let[t,n]of po)Object.entries(n).length>0&&(e[t]={...n});Za.writeFileSync(Mr,JSON.stringify(e,null,2)+`
|
|
207
|
+
`,{mode:384})}function Ss(){fp||(Ww(),fp=!0)}function gp(e){return Ss(),po.get(e)??{}}function Va(e){Ss();let t=po.get(e);return t||(t={},po.set(e,t)),t}function Uw(e,t){let n=e.taskEnv??"OMNISH_TASK",o=t.recipesMacroDefaultCommand.trim();return $n(o,n).ok?Ve({...e,command:o,promptTemplate:e.command}):e}function Hw(e,t){if(e.promptTemplate||$n(e.command,t).ok||!ho(e.command,t))return!1;let n=e.command;return n.includes("```")||n.length>2e3||/^"Create\b/i.test(n)||n.includes("<<<")||n.includes(`
|
|
208
|
+
### `)}function nl(e,t){let n=Ve(e),o=n.taskEnv??"OMNISH_TASK";return!n.promptTemplate&&Hw(n,o)&&(n=Uw(n,t)),n}function Bw(e){try{let t=Za.readFileSync(Pr,"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(!(!el.test(i)||tl.has(i))&&s&&typeof s=="object"&&typeof s.command=="string"){let l=nl(s,e),c=Pn(l);if(!c.ok){console.warn(`[omnish] recipes.json: skipping "${i}": ${c.error}`);continue}o[i]=l}}return o}catch{return{}}}function jw(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 ks(e,t,n){for(let[o,r]of Object.entries(t)){let s=o.toLowerCase();!el.test(s)||tl.has(s)||r.command.trim()&&e.set(s,{...r,name:s,source:n})}}function Xa(e,t){let n=gp(e),o={};for(let[r,s]of Object.entries(n)){let i=nl(s,t);Pn(i).ok&&(o[r]=i)}return o}function yp(e,t){let n=new Map;return ks(n,jw(t),"builtin"),ks(n,Bw(t),"global"),ks(n,Xa(mo,t),"shared"),ks(n,Xa(e,t),"peer"),n}function Xe(e,t,n){let o=n.trim().toLowerCase();return yp(e,t).get(o)}function wp(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 ol(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 rl(e){let t=ol(e);return{scope:t.scope,remainder:t.remainder}}function bp(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 Gw(e){let t=e.trim().toLowerCase();if(t==="--global"||t==="-g")return"global";if(t==="--chat"||t==="-p")return"chat"}function kp(e){let t=e.trim().match(/^(\S+)\s+(--global|-g|--chat|-p)\s*$/i);if(!t?.[1]||!t[2])return;let n=Gw(t[2]);if(n)return{name:t[1],target:n}}function jt(e,t,n,o){let r=o.trim().toLowerCase(),s=e==="global"?mo:t,i=gp(s)[r];if(i===void 0)return;let a=nl(i,n);if(Pn(a).ok)return{...a,name:r,source:e==="global"?"shared":"peer"}}function vp(e,t,n){if(n==="merged")return[];let o=n==="global"?mo:e,r=n==="global"?"shared":"peer",s=Xa(o,t);return Object.entries(s).map(([i,a])=>({...a,name:i,source:r})).sort((i,a)=>i.name.localeCompare(a.name))}function $t(e){let t=e.trim();if(!t)return{ok:!1,error:"Name is empty."};let n=t.toLowerCase();return el.test(n)?tl.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 xs(e,t){let n=e.replace(/\r\n/g,`
|
|
209
|
+
`).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 ho(e,t){return e.includes("$")?e.includes(t):!1}function $n(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(!ho(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 Pn(e){let t=e.taskEnv??"OMNISH_TASK",n=$n(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 Jw(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 u=l===0?"":t[l-1];if(u&&!/\s/.test(u))continue;let d=t.slice(l+2),m=/^(template|tamplate)\b/i.exec(d);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 sl(e,t){let n=e.replace(/\r\n/g,`
|
|
210
|
+
`).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 Ve({command:h[0].cmd,steps:h})}let r="OMNISH_TASK",{command:s,template:i}=Jw(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 d=$n(s,r);if(!d.ok)throw new Error(d.error);return Ve({command:s,promptTemplate:i})}let a=/\n---\n/,l=n.search(a);if(l>=0){let d=n.slice(0,l).trim(),m=n.slice(l).replace(/^\n---\n/,"").trim();if(!d)throw new Error("Command part (before ---) is empty.");if(!m)throw new Error("Template part (after ---) is empty.");let h=$n(d,r);if(!h.ok)throw new Error(h.error);return Ve({command:d,promptTemplate:m})}if($n(n,r).ok)return Ve({command:n});let c=t.trim(),u=$n(c,r);if(!u.ok)throw new Error(`recipesMacroDefaultCommand invalid (${u.error}). Fix config or paste runner then --- then template.`);return Ve({command:c,promptTemplate:n})}function Cs(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 fo(e,t,n,o="chat",r){let s=$t(t);if(!s.ok)throw new Error(s.error);let i=Ve({...n,command:n.command});if(r?.skipCommandValidation){if(!i.command.trim())throw new Error("Command is empty.")}else{let c=Pn(i);if(!c.ok)throw new Error(c.error)}let a=o==="global"?mo:e,l=Va(a);l[s.normalized]=i,vs()}function Sp(e,t,n="chat"){let o=t.trim().toLowerCase(),r=n==="global"?mo:e;Ss();let s=po.get(r);return!s||!(o in s)?!1:(delete s[o],vs(),!0)}function il(e,t,n,o){let r=$t(t);if(!r.ok)return{ok:!1,error:r.error};let s=r.normalized,i=e,a=mo;Ss();let l=Va(i),c=Va(a),u=l[s],d=c[s],m=h=>{let f=Ve({...h}),g=Pn(f);if(!g.ok)throw new Error(g.error);return f};try{if(n==="global"){if(u!==void 0){let g=m(u);return c[s]=g,delete l[s],vs(),{ok:!0,kind:"moved",target:"global",name:s}}if(d!==void 0)return{ok:!0,kind:"noop",message:`Recipe "${s}" is already gateway-shared.`};let f=Xe(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(d!==void 0){let f=m(d);return l[s]=f,delete c[s],vs(),{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=Xe(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 xp(e,t){let n=[...yp(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 Rs(e){let t=Fw.randomBytes(4).toString("hex");return`r-${e.replace(/[^a-zA-Z0-9_-]/g,"").slice(0,12)}-${t}`.slice(0,32)}function Cp(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}
|
|
211
|
+
${c}`}function Rp(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(`
|
|
212
|
+
`)}G();var Tp={enter:"\r",cr:"\r",lf:`
|
|
213
|
+
`,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 qw(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+=`
|
|
214
|
+
`,n++;continue}if(r==="t"){t+=" ",n++;continue}if(r==="\\"){t+="\\",n++;continue}}t+=o}return t}function zw(e){let t=e.trim();if(!t)return"";if(t.startsWith("\\"))return qw(t);let n=t.toLowerCase();if(Tp[n])return Tp[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 Ts(e){let t=e.split(",").map(n=>n.trim()).filter(Boolean);return t.length===0?"":t.map(n=>zw(n)).join("")}var nr="\x1B";function $s(e){let t=e;return t=t.replace(new RegExp(`${nr}\\[[\\d;?]*[ -/]*[@-~]`,"g"),""),t=t.replace(new RegExp(`${nr}\\][^${nr}\\u0007]*(?:\\u0007|${nr}\\\\)`,"g"),""),t=t.replace(new RegExp(`${nr}[@-Z\\\\-_]`,"g"),""),t=t.replace(/\u0007/g,""),t}function Kw(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 Ps=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=$s(l)),!l.trim()))return;let c=r.appsMaxFlushBytes;if(l.length>c){let h=l.slice(c);l=l.slice(0,c)+`
|
|
215
|
+
[\u2026+${h.length} chars; /apps since ${o} to read more]`,this.pending.set(t,(this.pending.get(t)??"")+h)}let d=(this.opts.getRunningCount(n)>1?`[${o}] `:"")+l,m=Kw(d,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 Ms=4096,Qw=/\[sudo\]\s+password\s+for\s+/i,Vw=/passphrase\s+for\s+/i,Xw=/(?:^|\n)\s*(?:Password|password):\s*$/;function Es(e){let n=$s(e).replace(/\r/g,"").slice(-512);return!!(Qw.test(n)||Vw.test(n)||Xw.test(n))}G();import Zw from"node:fs";import eb from"node:path";import*as $p from"node-pty";var tb=1024*1024,As=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>tb&&this.ringChunks.length>0;){let n=this.ringChunks.shift();this.ringBytes-=n.length}}static start(t){B(No(t.peerKey));let n=eb.join(No(t.peerKey),`${t.name}.log`),o=Zw.createWriteStream(n,{flags:"a",mode:384}),r={...process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...t.cwd?{PWD:t.cwd}:{},...t.extraEnv??{}},s=$p.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 al(e){return new Promise(t=>setTimeout(t,e))}async function Is(e,t,n){let o=n.appsSubmitDelayMs,r=n.appsClearInputDelayMs,s=n.appsClearInput!==!1,a=n.appsSkipClearOnPasswordPrompt!==!1&&Es(e.recentOutputTail(Ms)),l=s&&!a?Ts(n.appsClearInputSequence||"^A,^K"):"",u=t.replace(/\r\n/g,`
|
|
177
216
|
`).replace(/\r/g,"").split(`
|
|
178
|
-
`);l&&(r>0&&await
|
|
179
|
-
`).trimEnd()}catch{return""}}var
|
|
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=
|
|
217
|
+
`);l&&(r>0&&await al(r),e.write(l));for(let d of u)d.length>0&&e.write(d),o>0&&await al(o),e.write("\r"),l&&(r>0&&await al(r),e.write(l))}var ob=/^[a-zA-Z0-9_-]{1,32}$/;function qe(e){return ob.test(e)?null:"Session name must be 1\u201332 chars: letters, digits, _ or -."}function _e(e,t){return`${e}:${t}`}function rb(e){let t=e.lastIndexOf(":");return t<=0?null:{peerKey:e.slice(0,t),name:e.slice(t+1)}}function sb(e){return/\bstarted\b/i.test(e)&&!/^Session "/.test(e)&&!/^Per-chat app/.test(e)&&!/^Global app/.test(e)}function ll(e,t){let n=e??t.recipesRunAttach;return{attach:n,mute:!n}}function ib(e,t){return e===0&&(t===0||t==null)}function ab(e,t){try{let o=pn.readFileSync(e,"utf8").split(/\r?\n/);return o.slice(Math.max(0,o.length-t)).join(`
|
|
218
|
+
`).trimEnd()}catch{return""}}var mn=class{constructor(t,n){this.getCfg=t;this.send=n,this.router=new Ps({getCfg:()=>this.getCfg(),send:n,isMuted:(o,r)=>this.muted.has(_e(o,r)),isRaw:(o,r)=>this.rawMode.has(_e(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 nb.join(No(t),`${n}.log`)}getSession(t,n){return this.sessions.get(_e(t,n))}removeSessionRecord(t,n){let o=_e(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=_e(t,n),r=this.getSession(t,n);if(!r?.alive)return;let s=this.getCfg(),i=r.recentOutputTail(Ms);if(!Es(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 Is(r,n,this.getCfg()),!0):!1}async writeNamedLine(t,n,o){let r=qe(n);if(r)return r;let s=this.getSession(t,n);return s?.alive?(await Is(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})`:"",d=s-1,m=d>0?`${d} 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 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(`
|
|
219
|
+
`)}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=Rs(r.recipeLabel),i=this.runQueue.get(t)?.length??0,a=r.startOptions??ll(null,n),l=this.start(t,s,r.command,n,r.extraEnv,a);if(!sb(l))return o.unshift(r),this.runQueue.set(t,o),this.runQueuePaused.set(t,!0),this.activeQueuedRunHead.set(t,null),`${l}
|
|
181
220
|
Run queue paused; fix the issue, then /run queue resume.`;this.activeQueuedRunHead.set(t,{sessionName:s,recipeLabel:r.recipeLabel});let u=i>0?`
|
|
182
221
|
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=
|
|
184
|
-
Example: /apps start sh bash`;if(this.sessions.has(
|
|
185
|
-
If install skipped native builds: pnpm approve-builds && pnpm install (needs @whiskeysockets/baileys, sharp, protobufjs, esbuild, node-pty).`}let
|
|
222
|
+
Run queue: started head above; no further queued jobs.`;return`${l}${u}`}start(t,n,o,r,s,i){let a=qe(n);if(a)return a;if(!o.trim())return`Usage: /apps start <name> <command\u2026>
|
|
223
|
+
Example: /apps start sh bash`;if(this.sessions.has(_e(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,c={...process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...l?{PWD:l}:{},...s??{}},u;try{u=As.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)}
|
|
224
|
+
If install skipped native builds: pnpm approve-builds && pnpm install (needs @whiskeysockets/baileys, sharp, protobufjs, esbuild, node-pty).`}let d=_e(t,n);this.sessions.set(d,u);let m=i?.attach!==!1,h=i?.mute??!m;if(m&&this.focus.set(t,n),h&&this.muted.add(d),m)return`App "${n}" started and attached.
|
|
186
225
|
[cwd: ${l}]
|
|
187
226
|
Plain DMs go to this session until /apps detach. Use >othername text for another session.`;let f=h?`
|
|
188
227
|
Output muted \u2014 /apps unmute `+n+" or /apps attach "+n+" to stream to chat.":"";return`App "${n}" started (detached).
|
|
189
228
|
[cwd: ${l}]`+f+`
|
|
190
229
|
/apps attach ${n} \u2014 focus for plain DMs
|
|
191
230
|
>${n} <text> \u2014 send a line
|
|
192
|
-
/apps tail ${n} \u2014 recent output`}async handleSessionExit(t,n,o){let r=
|
|
231
|
+
/apps tail ${n} \u2014 recent output`}async handleSessionExit(t,n,o){let r=_e(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 u=this.getCfg(),d=ib(o.exitCode,o.signal),m=this.runQueue.get(t)?.length??0;if(d){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=qe(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=rb(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
232
|
App sessions:
|
|
194
233
|
${n.join(`
|
|
195
|
-
`)}`}getSessionCommand(t,n){if(
|
|
196
|
-
`)}async sendText(t,n,o){let r=
|
|
197
|
-
`);return await
|
|
198
|
-
[...truncated]`}function
|
|
234
|
+
`)}`}getSessionCommand(t,n){if(qe(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=qe(r);if(s)return s;let i=this.getSession(t,r),a=this.logPath(t,r),l=0;try{l=pn.statSync(a).size}catch{}let c=this.muted.has(_e(t,r)),u=this.rawMode.has(_e(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: ${u}`].join(`
|
|
235
|
+
`)}async sendText(t,n,o){let r=qe(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,`
|
|
236
|
+
`);return await Is(s,i,this.getCfg()),`Sent to "${n}" (${i.length} chars + Enter per line).`}sendKey(t,n,o){let r=qe(n);if(r)return r;let s=this.getSession(t,n);if(!s?.alive)return`No session "${n}".`;let i=Ts(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=qe(n);if(r)return r;let s=this.logPath(t,n);if(!pn.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 ab(s,a)||"(empty log)"}readSince(t,n,o){let r=this.logPath(t,n);try{let i=pn.statSync(r).size,a=pn.openSync(r,"r");try{let l=Math.min(o,i),c=i-l,u=Buffer.alloc(c);return c>0&&pn.readSync(a,u,0,c,l),{text:u.toString("utf8"),nextOffset:i}}finally{pn.closeSync(a)}}catch{return{text:"",nextOffset:o}}}mute(t,n){let o=qe(n);return o||(this.muted.add(_e(t,n)),`Muted chat output for "${n}" (log still grows). /apps unmute ${n}`)}unmute(t,n){let o=qe(n);return o||(this.muted.delete(_e(t,n)),`Unmuted "${n}".`)}setRaw(t,n,o){let r=qe(n);if(r)return r;let s=_e(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=qe(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=qe(n);if(o)return o;let r=this.getSession(t,n);if(!r?.alive)return`No running session "${n}".`;r.kill("SIGTERM");let s=_e(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=qe(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=qe(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(_e(t,n)),this.rawMode.delete(_e(t,n)),this.passwordHintSent.delete(_e(t,n));let s=this.logPath(t,n);try{pn.rmSync(s,{force:!0})}catch{}return`Removed "${n}" metadata and log.`}};ot();xe();import rr from"node:fs";import sr from"node:path";import lb from"node:os";import*as Mp from"node-pty";function cb(e,t){return e.length<=t?e:`${e.slice(0,t)}
|
|
237
|
+
[...truncated]`}function ub(e){if(e===void 0||e===0)return null;let t=lb.constants.signals;for(let[n,o]of Object.entries(t))if(o===e)return n;return null}function Pp(e){try{process.platform==="win32"?e.kill():e.kill("SIGTERM")}catch{e.kill()}}function Mn(e,t,n){return new Promise(o=>{let r=Date.now(),s=!1,i="",a=n.cwd,l=null,c=!1,u=null,d=null,m,h=y=>{if(c)return;c=!0,m!==void 0&&clearTimeout(m),u?.dispose(),u=null,o(y);let b=d;d=null,queueMicrotask(()=>b?.dispose())},g={...n.env??process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...a?{PWD:a}:{}};try{l=Mp.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&&Pp(l)},n.timeoutMs),u=l.onData(y=>{i+=y,i.length>n.maxBytes&&(i=cb(i,n.maxBytes),l&&Pp(l))}),d=l.onExit(y=>{h({code:y.exitCode,stdout:i,stderr:"",durationMs:Date.now()-r,timedOut:s,signal:ub(y.signal)})})})}function or(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 db(e,t,n){let o=e;for(let r=0;r<14;r++){let s=or(o,t,n),i=new Date(s).getDay();if(i>=1&&i<=5)return s;o=s}return or(o,t,n)}function pb(e,t,n,o){let r=e;for(let s=0;s<370;s++){let i=or(r,n,o);if(new Date(i).getDay()===t)return i;r=i}return or(r,n,o)}function mb(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 hb(e,t){return e.kind==="ondemand"||e.kind==="heartbeat"?Number.POSITIVE_INFINITY:e.kind==="daily"?or(t,e.hour,e.minute):e.kind==="weekdays"?db(t,e.hour,e.minute):e.kind==="hourly"?mb(t,e.minute):pb(t,e.weekday,e.hour,e.minute)}function Ip(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=hb(e,a);if(l>o)break;i.push(l),a=l}return i}var fb={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 cl(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 gb(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 Ls(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=cl(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=cl(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=gb(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=fb[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=cl(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=Ep(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=Ep(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 Ep(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 Ap(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 go(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 ${Ap(e.intervalMs)} grace ${Ap(e.graceMs)}`}}G();xe();import yb from"better-sqlite3";var Lp=2,hn=null;function Op(){B(kt);let e=new yb($c);e.pragma("journal_mode = WAL"),e.exec(`
|
|
199
238
|
CREATE TABLE IF NOT EXISTS cowork_meta (
|
|
200
239
|
key TEXT PRIMARY KEY,
|
|
201
240
|
value TEXT NOT NULL
|
|
@@ -214,120 +253,127 @@ ${n.join(`
|
|
|
214
253
|
last_ok INTEGER NOT NULL,
|
|
215
254
|
updated_at_ms INTEGER NOT NULL
|
|
216
255
|
);
|
|
217
|
-
`);let t=e.prepare("SELECT value FROM cowork_meta WHERE key = 'schema_version'").get(),n=t?Number(t.value):0;return n<
|
|
256
|
+
`);let t=e.prepare("SELECT value FROM cowork_meta WHERE key = 'schema_version'").get(),n=t?Number(t.value):0;return n<Lp&&(n<2&&e.exec(`
|
|
218
257
|
CREATE TABLE IF NOT EXISTS cowork_task_state (
|
|
219
258
|
task_id TEXT PRIMARY KEY,
|
|
220
259
|
last_ok INTEGER NOT NULL,
|
|
221
260
|
updated_at_ms INTEGER NOT NULL
|
|
222
261
|
);
|
|
223
|
-
`),e.prepare("INSERT OR REPLACE INTO cowork_meta (key, value) VALUES ('schema_version', ?)").run(String(
|
|
262
|
+
`),e.prepare("INSERT OR REPLACE INTO cowork_meta (key, value) VALUES ('schema_version', ?)").run(String(Lp))),e}function En(e){hn||(hn=Op()),bb(e)}function Np(){if(hn){try{hn.close()}catch{}hn=null}}function An(){return hn||(hn=Op()),hn}function wb(e){let n=An().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 Os(e){let t=wb(e.id);return t??e.lastCompletedSlotMs}function Ns(e,t){if(t.length===0)return;let n=An(),o=n.prepare(`
|
|
224
263
|
INSERT INTO cowork_slot_completion (task_id, slot_ms, kind, completed_at_ms, log_path)
|
|
225
264
|
VALUES (?, ?, ?, ?, ?)
|
|
226
|
-
`);n.transaction(()=>{for(let s of t)o.run(e,s.slotMs,s.kind,s.completedAtMs,s.logPath)})()}function
|
|
265
|
+
`);n.transaction(()=>{for(let s of t)o.run(e,s.slotMs,s.kind,s.completedAtMs,s.logPath)})()}function ul(e){An().prepare("INSERT OR REPLACE INTO cowork_task_state (task_id, last_ok, updated_at_ms) VALUES (?, 1, ?)").run(e,Date.now())}function Fs(e){return An().prepare("SELECT updated_at_ms FROM cowork_task_state WHERE task_id = ?").get(e)?.updated_at_ms??null}function _s(e){let t=An().prepare("SELECT last_ok FROM cowork_task_state WHERE task_id = ?").get(e);return t==null?null:t.last_ok===1}function Ws(e,t){An().prepare("INSERT OR REPLACE INTO cowork_task_state (task_id, last_ok, updated_at_ms) VALUES (?, ?, ?)").run(e,t?1:0,Date.now())}function bb(e){let n=An().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{Ns(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 dl(e,t){if(e.startsWith("tg:")){let n=e.slice(3);return Lr(t.telegramAllowFrom).has(n)}return Lc(Ir(t.allowFrom),e.replace(/^wa:/,""))}var kb=8,Fp=12e4,_p=10,vb=1e3;function Sb(e){return e.toISOString().replace(/[:.]/g,"-")}function xb(e){let t="";for(let n of e)n==="*"?t+="[^/\\\\]*":n==="?"?t+="[^/\\\\]":/[.+^${}()|[\]\\]/.test(n)?t+=`\\${n}`:t+=n;return new RegExp(`^${t}$`)}function Cb(e,t,n){let o=Ze(e,t),r=sr.dirname(o),s=sr.basename(o);if(r.includes("*")||r.includes("?"))return[];if(!s.includes("*")&&!s.includes("?")){try{let c=rr.statSync(o);if(c.isFile()&&c.mtimeMs>=n)return[o]}catch{}return[]}let i;try{i=rr.readdirSync(r)}catch{return[]}let a=xb(s),l=[];for(let c of i){if(!a.test(c))continue;let u=sr.join(r,c);try{let d=rr.statSync(u);d.isFile()&&d.mtimeMs>=n&&l.push(u)}catch{}}return l}function Wp(e,t,n){let o=ft(e,t.fileSendMaxBytes);return"error"in o?{ok:!1,displayName:n??sr.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 Dp(e,t,n,o){let r=ie(t.ownerPeerKey),s=t.cwd.trim()?t.cwd:r.cwd,i=Ze(t.outputDir,r.cwd);try{rr.mkdirSync(i,{recursive:!0,mode:448})}catch(M){P.warn({err:String(M),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(),u=await Mn(e.shell,t.command,{timeoutMs:e.syncTimeoutMs,maxBytes:e.syncMaxBytes,cwd:s}),d=Date.now()-c,m=`${Sb(new Date)}-${t.id}-${l}.log`,h=sr.join(i,m),g=[`cowork task=${t.name} id=${t.id}`,`slot=${a} kind=${l}`,`cwd=${s}`,`exit=${u.code} timedOut=${u.timedOut} durationMs=${d}`,"---",""].join(`
|
|
227
266
|
`)+(u.stdout||"")+(u.stderr?`
|
|
228
267
|
--- stderr ---
|
|
229
|
-
${u.stderr}`:"");try{
|
|
230
|
-
`)
|
|
231
|
-
[...input truncated]`}function
|
|
268
|
+
${u.stderr}`:"");try{rr.writeFileSync(h,g,{mode:384})}catch(M){P.warn({err:String(M),logPath:h},"cowork write log")}let y=u.code===0&&!u.timedOut&&u.signal===null,k=Me().find(M=>M.id===t.id&&M.ownerPeerKey===t.ownerPeerKey),T=k?.notify??t.notify,$=k?.notifyWhen??t.notifyWhen??"always",L=k?.attachLog??t.attachLog,x=k?.attachFiles??t.attachFiles,O=!0;if($==="failure")O=!y;else if($==="state-change"){let M=_s(t.id);O=M===null||M!==y}Ws(t.id,y);let E=u.timedOut?"timeout":u.signal?`signal ${u.signal}`:u.code!==0&&u.code!==null?`exit ${u.code}`:null,K=`slot: ${a} \xB7 ${l}${E?` \xB7 ${E}`:""}`,te=(u.stdout||"").replace(/\s+$/,""),ce=(u.stderr||"").trim(),Se=te||(ce?`(stderr) ${ce}`:"(no output)"),_=$==="state-change"?y?" [recovered]":" [failing]":"",fe=`${t.name}${_} : ${E?`[${E}] `:""}${Se}`,V=n.onDemand?[K,`output: ${Se}`]:[fe];if(O){let M=[],j=[],Y=c-vb,J=[],ae=new Set;if(Array.isArray(x))for(let R of x){let I;try{I=Cb(R,s,Y)}catch(Z){P.warn({err:String(Z),pat:R},"cowork attach glob"),M.push(`attach: ${R} skipped (glob error)`);continue}if(I.length!==0)for(let Z of I)ae.has(Z)||(ae.add(Z),J.push(Z))}if(L){let R=Wp(h,e,`${t.name}-${l}.log`);R.ok?j.push(R.spec):M.push(`attach: ${R.displayName} skipped (${R.reason})`)}let q=0;for(let R of J){if(j.length>=_p){q+=1;continue}let I=Wp(R,e);I.ok?j.push(I.spec):M.push(`attach: ${I.displayName} skipped (${I.reason})`)}q>0&&M.push(`attached: skipped ${q} file(s) over cap ${_p}`),M.length>0&&V.push(...M);let Be=V.join(`
|
|
269
|
+
`),$e=p(Be),Le=Kn(T,t.ownerPeerKey,e);for(let R of Le){let I=de($e,R.startsWith("tg:")?"telegram":"whatsapp").text;try{await o.sendToPeer(R,I)}catch(Z){P.warn({err:String(Z),pk:R},"cowork notify failed")}for(let Z of j)try{await o.sendMediaToPeer(R,Z)}catch(be){P.warn({err:String(be),pk:R,file:Z.displayName},"cowork media notify failed")}}}return{commandOk:y,logPath:h}}function Ds(e){let t=!1;En(Me());let n=async()=>{if(t)return;t=!0;let s=Date.now(),i=0,a=0,l=0,c=0,u=0;try{let d=e.getConfig(),{batch:m,remainingAfter:h}=Dc(kb);a=m.length,l=h,i=m.length+h;for(let y of m)try{let k=Me().find(T=>T.name===y.name.toLowerCase()&&T.ownerPeerKey===y.ownerPeerKey);if(k&&k.enabled){if(!dl(k.ownerPeerKey,d)){P.warn({task:k.name,peer:k.ownerPeerKey},"cowork: skipping on-demand run \u2014 owner no longer on allowlist");continue}c+=1,await Dp(d,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=Me();En(f);let g=Date.now();for(let y of f){if(!y.enabled||y.schedule.kind!=="heartbeat"||!dl(y.ownerPeerKey,d))continue;let b=Fs(y.id);if(b===null)continue;let k=b+y.schedule.intervalMs+y.schedule.graceMs;if(g>k){if(_s(y.id)===!1)continue;Ws(y.id,!1);let $=Math.round((g-b)/6e4),L=`${y.name} [heartbeat missed] \u2014 last check-in ${$}m ago`,x=y.notify,O=Kn(x,y.ownerPeerKey,d);for(let E of O)try{await e.sendToPeer(E,L)}catch(K){P.warn({err:String(K),pk:E},"cowork heartbeat notify failed")}}else if(g<=k&&_s(y.id)===!1){Ws(y.id,!0);let $=`${y.name} [heartbeat recovered]`,L=y.notify,x=Kn(L,y.ownerPeerKey,d);for(let O of x)try{await e.sendToPeer(O,$)}catch(E){P.warn({err:String(E),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(!dl(y.ownerPeerKey,d)){P.warn({task:y.name,peer:y.ownerPeerKey},"cowork: skipping scheduled run \u2014 owner no longer on allowlist");continue}let b=Os(y),k=Ip(y.schedule,b,y.createdAtMs,g);if(k.length===0)continue;let T=k[k.length-1],$=g-T>Fp;try{u+=1;let{commandOk:L,logPath:x}=await Dp(d,y,{slotMs:T,catchUp:$,onDemand:!1},e);if(L){let O=Date.now(),E=g-T<=Fp?"on_time":"catch_up";if(k.length===1)Ns(y.id,[{slotMs:T,kind:E,logPath:x,completedAtMs:O}]);else{let te=k.slice(0,-1).map(ce=>({slotMs:ce,kind:"coalesced",logPath:null,completedAtMs:O}));te.push({slotMs:T,kind:E,logPath:x,completedAtMs:O}),Ns(y.id,te)}}}catch(L){P.warn({err:String(L),task:y.name},"cowork scheduled run failed")}}}finally{let d=Date.now()-s;P.info({tickMs:d,pendingDepthStart:i,pendingDequeued:a,pendingRemainingAfter:l,pendingRunsStarted:c,scheduledRan:u},"cowork tick"),t=!1}},o=setInterval(()=>void n(),3e4),r=setTimeout(()=>void n(),5e3);return()=>{clearInterval(o),clearTimeout(r),Np()}}import Rb from"node:fs";import Up from"node:path";import{fileURLToPath as Tb}from"node:url";var Us=null;function lt(){if(Us!==null)return Us;let e=Up.dirname(Tb(import.meta.url)),t=Up.join(e,"..","package.json"),n=Rb.readFileSync(t,"utf8"),o=JSON.parse(n);return Us=typeof o.version=="string"&&o.version.trim()?o.version.trim():"0.0.0",Us}xe();G();import{spawn as $b}from"node:child_process";import Hp from"node:fs";import Bp from"node:path";var Pb=new Set(["PATH","HOME","USER","LOGNAME","SHELL","LANG","LC_ALL","LC_CTYPE","LC_MESSAGES","TMPDIR","TZ"]),Mb=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 Eb(e,t){let n={OMNISH_PEER_KEY:e,OMNISH_CHAT_MESSAGE:t};for(let o of Pb){let r=process.env[o];r!==void 0&&(n[o]=r)}for(let[o,r]of Object.entries(process.env))r&&(o.startsWith("OMNISH_")||Mb.has(o))&&(n[o]=r);return n}var jp=new Map;function Ab(e,t){let o=(jp.get(e)??Promise.resolve()).then(t).catch(r=>{P.warn({peerKey:e,err:String(r)},"chat LLM fallback queue task failed")});jp.set(e,o)}function Ib(e){se();let t=e.chatLlmWorkDir.trim();if(t){let o=Bp.resolve(t);return B(o),{cwd:o,cleanup:()=>{}}}let n=Hp.mkdtempSync(Bp.join(D,"chat-llm-"));return{cwd:n,cleanup:()=>{try{Hp.rmSync(n,{recursive:!0,force:!0})}catch{}}}}function Lb(e,t){return e.length<=t?e:`${e.slice(0,t)}
|
|
270
|
+
[...input truncated]`}function Ob(e,t){let n=[],o=e.stdout.trimEnd(),r=e.stderr.trimEnd();return o&&n.push(o.length>t?`${o.slice(0,t)}
|
|
232
271
|
[...truncated]`:o),r&&(n.push("\u2014 stderr \u2014"),n.push(r.length>t?`${r.slice(0,t)}
|
|
233
272
|
[...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
|
|
235
|
-
[...truncated]`;else{let L
|
|
236
|
-
[...truncated]`}try{u?.kill("SIGTERM")}catch{}}};try{u
|
|
237
|
-
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null})}),(()=>{if(!(!g||
|
|
238
|
-
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}
|
|
239
|
-
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}
|
|
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
|
|
241
|
-
`,{mode:384})}function
|
|
242
|
-
`)}var
|
|
273
|
+
`)}function Hs(e){let t=e&&typeof e=="object"&&"code"in e?String(e.code):"";return t==="EPIPE"||t==="EOF"}function Gp(e){try{e?.stdin?.end()}catch(t){if(!Hs(t))throw t}}function Nb(e,t,n,o){return new Promise(r=>{let s=Date.now(),i=!1,a="",l="",c=!1,u=null,d,m=b=>{c||(c=!0,clearTimeout(d),r(b))},h=o.maxOutChars,f=(b,k)=>{b==="out"?a+=k:l+=k;let T=a.length+l.length;if(T>h){let $=T-h;if(l.length>=$)l=`${l.slice(0,Math.max(0,l.length-$))}
|
|
274
|
+
[...truncated]`;else{let L=$-l.length;l="",a=`${a.slice(0,Math.max(0,a.length-L))}
|
|
275
|
+
[...truncated]`}try{u?.kill("SIGTERM")}catch{}}};try{u=$b(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}d=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=>{Hs(b)||m({code:null,stdout:a,stderr:`${l}
|
|
276
|
+
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null})}),(()=>{if(!(!g||c))try{g.write(n,"utf8",b=>{if(b&&!Hs(b)){try{u?.kill("SIGTERM")}catch{}m({code:null,stdout:a,stderr:`${l}
|
|
277
|
+
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}Gp(u)})}catch(b){if(!Hs(b)){m({code:null,stdout:a,stderr:`${l}
|
|
278
|
+
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}Gp(u)}})(),u.on("error",b=>{m({code:null,stdout:a,stderr:`${l}
|
|
279
|
+
${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 Fb(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}=Ib(e);try{let a=Lb(n,e.chatLlmMaxInputChars),l=Eb(t,a),c=e.chatLlmMaxOutputChars,u;e.chatLlmNeedsTty?u=await Mn(e.shell,r,{cwd:s,timeoutMs:e.chatLlmTimeoutMs,maxBytes:c,env:l}):u=await Nb(e.shell,r,a,{cwd:s,timeoutMs:e.chatLlmTimeoutMs,maxOutChars:c,env:l});let d=Ob(u,c);await o(d)}catch(a){P.warn({peerKey:t,err:String(a)},"chat LLM fallback run failed"),await o(`(assistant error) ${String(a)}`)}finally{i()}}function yo(e,t,n,o){P.info({peerKey:t,len:n.length},"chat LLM fallback enqueued"),Ab(t,async()=>{await Fb(e,t,n,o),P.info({peerKey:t},"chat LLM fallback completed")})}G();import{spawn as _b}from"node:child_process";import Wb from"node:crypto";import nt from"node:fs";import pl from"node:path";var Jp=64,Db=/^[a-zA-Z0-9._-]+$/;function Ub(e){let t=e.trim();return t?t.length>Jp?`Job name must be at most ${Jp} characters.`:Db.test(t)?null:"Job name may only use letters, digits, and . _ -":"Job name must not be empty."}function qp(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,u]of a){let d=i.match(l);if(d){n=d[c],t=d[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=Ub(n);if(i)return{error:i}}return s?{cmd:s,name:n,notify:o}:{error:"Add a shell command after the flags."}}function Hb(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 ir(e,t){nt.writeFileSync(e,JSON.stringify(t,null,2)+`
|
|
280
|
+
`,{mode:384})}function ar(e){try{return JSON.parse(nt.readFileSync(e,"utf8"))}catch{return null}}function Bs(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(`
|
|
281
|
+
`)}var Gt=class{running=new Map;onJobExit;constructor(t={}){this.onJobExit=t.onJobExit}finishJob(t,n,o,r){ir(t,o),r.onComplete&&r.onComplete(o),this.onJobExit&&this.onJobExit(o)}metaPath(t){return pl.join(ut,`${t}.meta.json`)}logPath(t){return pl.join(ut,`${t}.log`)}spawnJob(t,n,o={}){se();let r=Wb.randomBytes(4).toString("hex"),s=this.logPath(r),i=this.metaPath(r);nt.writeFileSync(s,"",{flag:"w",mode:384});let a=nt.createWriteStream(s,{flags:"a"}),l=new Date().toISOString(),c=o.cwd,u=_b(t,["-c",n],{stdio:["ignore","pipe","pipe"],env:{...process.env,TERM:"dumb",...c?{PWD:c}:{}},...c?{cwd:c}:{}});this.running.set(r,u);let d={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 ir(i,d),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=ar(i)??d,g={...f,status:f.status==="killed"?"killed":"done",exitCode:m,signal:h??null,finishedAt:new Date().toISOString()};this.finishJob(i,d,g,o)}),u.on("error",m=>{this.running.delete(r),a.end(()=>{try{nt.appendFileSync(s,`
|
|
243
282
|
[spawn error] ${String(m)}
|
|
244
|
-
`)}catch{}});let f={...
|
|
283
|
+
`)}catch{}});let f={...ar(i)??d,status:"done",exitCode:null,signal:null,finishedAt:new Date().toISOString()};this.finishJob(i,d,f,o)}),{id:r,meta:d}}list(){se();let t=[];try{t=nt.readdirSync(ut)}catch{return[]}let n=[];for(let o of t){if(!o.endsWith(".meta.json"))continue;let r=ar(pl.join(ut,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(!nt.existsSync(o))return"(no log file)";let s=nt.readFileSync(o,"utf8").split(`
|
|
245
284
|
`);return s.slice(Math.max(0,s.length-n)).join(`
|
|
246
|
-
`).trimEnd()||"(empty log)"}readSince(t,n){let o=this.logPath(t);if(!
|
|
247
|
-
`).trim()}function
|
|
285
|
+
`).trimEnd()||"(empty log)"}readSince(t,n){let o=this.logPath(t);if(!nt.existsSync(o))return{text:"",nextOffset:n};let s=nt.statSync(o).size;if(n>=s)return{text:"",nextOffset:s};let i=Buffer.alloc(s-n),a=nt.openSync(o,"r");try{nt.readSync(a,i,0,i.length,n)}finally{nt.closeSync(a)}return{text:i.toString("utf8"),nextOffset:s}}resolveJobRef(t){return Hb(this.list(),t)}kill(t){let n=this.metaPath(t),o=ar(n);if(!o)return`Unknown job id: ${t}`;let r=this.running.get(t);if(r&&!r.killed)return r.kill("SIGTERM"),ir(n,{...o,status:"killed",signal:"SIGTERM"}),`Sent SIGTERM to job ${t} (pid ${o.pid??"?"})`;if(o.pid)try{return process.kill(o.pid,"SIGTERM"),ir(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=ar(this.metaPath(t));o&&ir(this.metaPath(t),{...o,status:"killed",signal:"SIGTERM"})}this.running.clear()}};import vk from"node:fs";import{Bot as Sk,InputFile as xk}from"grammy";G();import zp from"node:fs";import wo from"node:path";function Bb(e){return e.replace(/[^a-zA-Z0-9._-]+/g,"_").slice(0,120)||"unknown"}function jb(e){let o=wo.basename(e.trim()||"file").replace(/[^a-zA-Z0-9._-]+/g,"_").replace(/^\.+/,"").slice(0,180);return o.length>0?o:"file"}function Gb(e,t){let n=new Date().toISOString().slice(0,10),o=Bb(t);return wo.join(e,o,n)}function bo(e,t,n){let o=Gb(e,t);B(o);let r=jb(n),s=wo.join(o,r);if(!zp.existsSync(s))return s;let i=wo.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=wo.join(o,c),!zp.existsSync(s))return s}return wo.join(o,`${a}-${Date.now()}${i}`)}xe();import Jb from"node:dns";import qb from"node:https";import{URL as zb}from"node:url";function ko(e){if(e instanceof Error){let t=e.cause;return t!=null?`${e.message} (${String(t)})`:e.message}return String(e)}xe();function Kp(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 Kb(e,t){let o=t.split("/").filter(r=>r.length>0).map(encodeURIComponent).join("/");return`https://api.telegram.org/file/bot${e}/${o}`}var Yp="omnish (Telegram bot; https://github.com/labKnowledge/whatsLive)";function Yb(e){let t=new zb(e);return new Promise((n,o)=>{let r=qb.request({hostname:t.hostname,port:t.port||443,path:t.pathname+t.search,method:"GET",headers:{"User-Agent":Yp,Accept:"*/*"},lookup(s,i,a){Jb.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 Qp(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=Kb(e.token,o.file_path),s;try{let i=await fetch(r,{headers:{"User-Agent":Yp,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 Yb(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: ${ko(i)}; IPv4 fallback: ${ko(a)}).`}}}return n>0&&s.byteLength>n?{error:`Media too large (max ${n} bytes).`}:{buffer:s}}ue();ot();import{downloadMediaMessage as Zb,extensionForMediaMessage as ek,getContentType as js,isJidGroup as em,isLidUser as tk}from"@whiskeysockets/baileys";import nk from"node:fs";xe();var fn=new Map;function Qb(){let e=Date.now();for(let[t,n]of fn)e-n>6e5&&fn.delete(t);for(;fn.size>500;){let t=fn.keys().next().value;if(t===void 0)break;fn.delete(t)}}function Vb(e){!e||typeof e!="string"||(Qb(),fn.set(e,Date.now()))}function ml(e){Vb(e?.key?.id??void 0)}function Xb(e){if(!e)return!1;let t=fn.get(e);return t===void 0?!1:Date.now()-t>6e5?(fn.delete(e),!1):!0}function Vp(e){return!Xb(e)}function ok(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 rk(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 tm(e){return[ok(e),rk(e)].filter(n=>n.trim().length>0).join(`
|
|
286
|
+
`).trim()}function hl(e){return e.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g,"").replace(/\uFEFF/g,"").trim()}function nm(e){let t=js(e??void 0);return t==="imageMessage"||t==="videoMessage"||t==="audioMessage"||t==="documentMessage"||t==="stickerMessage"}function sk(e){let t=js(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 Xp(e){try{if(!e)return"file.bin";let t=ek(e);return`file${t&&t.length>0?t.startsWith(".")?t:`.${t}`:".bin"}`}catch{return`${(js(e??void 0)??"file").replace("Message","")||"file"}.bin`}}function ik(e){let t=js(e??void 0);if(!t)return Xp(e);let n=e?.[t];return n?.fileName&&typeof n.fileName=="string"&&n.fileName.trim()?n.fileName.trim():Xp(e)}async function Zp(e,t,n,o){let r=t.message??void 0;if(!nm(r))return{};let s=o.fileReceiveMaxBytes,i=sk(r);if(s>0&&i!==void 0&&i>s)return{error:`Media too large (max ${s} bytes).`};let a;try{a=await Zb(t,"buffer",{},{logger:e.logger,reuploadRequest:e.updateMediaMessage})}catch(d){return P.warn({err:String(d),detail:ko(d)},"whatsapp downloadMediaMessage failed"),{error:`Could not download media from WhatsApp (${ko(d)}).`}}if(s>0&&a.length>s)return{error:`Media too large (max ${s} bytes).`};let l;try{l=en(o,n)}catch(d){return{error:String(d)}}let c=ik(r),u=bo(l,n,c);try{nk.writeFileSync(u,a,{mode:384})}catch{return{error:"Could not write media to inbox."}}return{path:u}}async function ak(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(tk(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 lk(e){try{if(!S().clusterEnabled)return;let n=tm(e.message??void 0);if(!n)return;let o=fd(n);if(!o)return;let r=e.key?.remoteJid??"",s=null;if(r&&!em(r)){let i=ne(r);i&&(s=`wa:${i}`)}Cd(o,s)}catch(t){P.warn({err:String(t)},"cluster footer observation failed")}}function om(e,t){let n=o=>{o.type==="notify"&&(async()=>{for(let r of o.messages){let s=r.key;if(!s||s.fromMe&&(lk(r),!Vp(s.id)))continue;let i=s.remoteJid;if(!i||em(i)||i.toLowerCase().endsWith("@status")||i==="status@broadcast")continue;let l=hl(tm(r.message??void 0)),{fromJid:c,fromE164:u}=await ak(e,s),d=`wa:${c}`,m=nm(r.message??void 0);if(m&&!l){(async()=>{try{let g=S(),y=await Zp(e,r,d,g);await t({fromJid:c,fromE164:u,text:"",messageId:s.id??void 0,mediaSavedPath:y.path,mediaError:y.error})}catch(g){P.error({err:String(g),fromJid:c},"whatsapp media-only background task failed")}})();continue}let h,f;if(m){let g=S(),y=await Zp(e,r,d,g);h=y.path,f=y.error}!l&&!h&&!f||await t({fromJid:c,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 gk from"node:fs";import ck from"node:process";import uk,{DisconnectReason as In,fetchLatestBaileysVersion as dk,makeCacheableSignalKeyStore as pk,useMultiFileAuthState as mk}from"@whiskeysockets/baileys";import hk from"qrcode-terminal";G();xe();var fk="0.1.0";function fl(e){return e?.error?.output?.statusCode}async function Gs(e={}){se();let t=e.authDir??le,n=e.verbose===!0,o=Nc(),{state:r,saveCreds:s}=await mk(t),{version:i}=await dk(),a=uk({version:i,logger:o,printQRInTerminal:!1,browser:["omnish","cli",fk],auth:{creds:r.creds,keys:pk(r.keys,o)},syncFullHistory:!1,markOnlineOnConnect:!1});return a.ev.on("creds.update",s),a.ev.on("connection.update",l=>{let{connection:c,lastDisconnect:u,qr:d}=l;if(d&&(e.onQr?.(d),e.printQr)){let m=ck.stdout,h=w(m,"\xB7".repeat(42));console.log(X(m,"Scan with WhatsApp \u2192 Linked devices")),console.log(h),hk.generate(d,{small:!0}),console.log(h)}if(c==="close"){let m=fl(u);n&&m===In.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 vo(e){try{e.end(new Error("omnish: socket closed"))}catch{e.ws?.close()}}function gl(e){return e?.output?.statusCode??e?.status??e?.error?.output?.statusCode}function Js(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 Ln(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 lm=3500,yk=400,rm=18e4,sm=9e4,im=3e5;function yl(e,t=lm){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
287
|
|
|
249
|
-
`);i>Math.floor(t*.35)&&(r=o+i+2)}n.push(e.slice(o,r)),o=r}return n}function
|
|
288
|
+
`);i>Math.floor(t*.35)&&(r=o+i+2)}n.push(e.slice(o,r)),o=r}return n}function qs(e){return new Promise(t=>setTimeout(t,e))}async function am(e,t){await Promise.race([e,qs(rm).then(()=>{P.warn({jid:t,ms:rm},"whatsapp outbound self-heal: prior send chain waited too long; continuing")})])}async function wk(e,t,n,o=3){let r;for(let s=1;s<=o;s+=1)try{let i=await Ln(e.sendMessage(t,{text:n}),sm,`whatsapp sendMessage timed out after ${sm}ms`);ml(i);return}catch(i){r=i;let a=String(i);if(/not connected|closed|timed out|timeout/i.test(a)&&s<o){await qs(800*s);continue}throw i}throw r}function bk(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 kk(e,t,n,o=3){let r;for(let s=1;s<=o;s+=1)try{let i=await Ln(e.sendMessage(t,n),im,`whatsapp sendMedia timed out after ${im}ms`);ml(i);return}catch(i){r=i;let a=String(i);if(/not connected|closed|timed out|timeout/i.test(a)&&s<o){await qs(800*s);continue}throw i}throw r}function cm(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=am(a,r).then(async()=>{let c=yl(i,lm);for(let u=0;u<c.length;u+=1)await wk(e,r,c[u]??""),u<c.length-1&&await qs(yk)}).catch(c=>{P.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=gk.readFileSync(s.absPath),a=bk(s,i),l=n.get(r)??Promise.resolve(),c=am(l,r).then(async()=>{await kk(e,r,a)}).catch(u=>{P.error({err:String(u),jid:r},"sendMedia failed")});n.set(r,c),await c.finally(()=>{n.get(r)===c&&n.delete(r)})}}}function um(e){let t=e.trim();return t?/^\/id(?:@[\w_]+)?$/i.test(t):!1}function dm(e){let t=String(e).replace(/\D/g,"");return`Your Telegram user id: ${t}
|
|
250
289
|
Add to allowlist on this gateway host:
|
|
251
|
-
omnish allow tg:${t}`}var
|
|
252
|
-
`));if(g&&
|
|
290
|
+
omnish allow tg:${t}`}var Ck=400;async function pm(e,t,n,o){let r=t(),s=await Qp(e,o.fileId,r.fileReceiveMaxBytes);if("error"in s)return{mediaError:s.error};let i;try{i=en(r,n)}catch(l){return{mediaError:String(l)}}let a=bo(i,n,o.baseName);try{return vk.writeFileSync(a,s.buffer,{mode:384}),{mediaSavedPath:a}}catch{return{mediaError:"Could not write media to inbox."}}}function Rk(e){return new Promise(t=>setTimeout(t,e))}async function wl(e,t,n,o={}){let r=new Sk(e);await r.api.deleteWebhook({drop_pending_updates:!1});let s=new Map,i=o.decorate??(u=>u);async function a(u,d){let m=Math.min(t().appsMaxWaChars,4096),h=de(d,"telegram"),f=i(h.text,La(u)),g=h.parseModeHtml,b=(s.get(u)??Promise.resolve()).then(async()=>{let k=yl(f,m);for(let T=0;T<k.length;T+=1){let $=k[T]??"",L=g?{parse_mode:"HTML"}:void 0;try{await r.api.sendMessage(u,$,L)}catch(x){if(g){P.warn({err:String(x),chatId:u},"telegram HTML send failed; retrying plain");let O=$.replace(/<[^>]+>/g,"");await r.api.sendMessage(u,O)}else throw x}T<k.length-1&&await Rk(Ck)}}).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,d){let m=new xk(d.absPath,d.displayName),h=d.caption,g=(s.get(u)??Promise.resolve()).then(async()=>{switch(d.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 d=u.chat,m=u.message;if(!d||d.type!=="private"||!u.from||!m)return;let h="text"in m&&m.text?m.text:"",f="caption"in m&&m.caption?m.caption:"",g=hl([h,f].filter(Boolean).join(`
|
|
291
|
+
`));if(g&&um(g)){let x=u.from.id;await r.api.sendMessage(d.id,dm(x));return}let y=Kp(m),b=La(d.id),k=d.id,T=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 pm(r,t,b,y);if(!x.mediaSavedPath&&!x.mediaError)return;await n({peerKey:b,text:"",tgChatId:d.id,tgReplyToMessageId:m.message_id,mediaSavedPath:x.mediaSavedPath,mediaError:x.mediaError},T)}catch(x){P.error({err:String(x),chatId:k},"telegram media-only background task failed")}})();return}let $,L;if(y){let x=await pm(r,t,b,y);$=x.mediaSavedPath,L=x.mediaError}!g&&!$&&!L||await n({peerKey:b,text:g,tgChatId:d.id,tgReplyToMessageId:m.message_id,mediaSavedPath:$,mediaError:L},T)}),r.catch(u=>{P.error({err:String(u)},"telegram bot error")});let c=r.start();return{bot:r,sendText:a,sendMedia:l,stop:async()=>{await r.stop(),await c.catch(()=>{})}}}ot();import Tk from"node:fs";import $k from"node:path";function Pk(e){let t=$k.join(e,"creds.json");try{let n=Tk.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 mm(e){let t=Pk(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 Ks from"node:fs";import ct from"node:process";G();G();import hm from"node:fs";var Mk="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 zs(e){let t=String(e).toLowerCase();return t.includes("401")||t.includes("logged out")?!0:gl(e)===In.loggedOut}function Ek(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 bl(e){let t=!1;for(let n=0;n<3;n++){if(e.signal?.aborted)throw new Error("Pairing cancelled.");let o=await Gs({authDir:e.authDir,printQr:e.printQr===!0,verbose:e.verbose===!0,onQr:e.onQr});e.onSocketReady?.(o);try{let r=Ln(Js(o),6e5,Mk);e.signal?await Promise.race([r,Ek(e.signal)]):await r;return}catch(r){if(r instanceof Error&&r.message==="Pairing cancelled.")throw r;if(gl(r)===In.restartRequired&&!t){t=!0,e.onRestart515?.(),await new Promise(i=>setTimeout(i,1500));continue}throw r}finally{e.onSocketClosed?.(),vo(o)}}throw new Error("Pairing failed after restart (515) retries.")}async function fm(e){let t=e.authDir??le;se();for(let n=1;n<=2;n++)try{await bl({...e,authDir:t});return}catch(o){if(o instanceof Error&&o.message==="Pairing cancelled.")throw o;if(n===1&&zs(o)){hm.rmSync(t,{recursive:!0,force:!0}),hm.mkdirSync(t,{recursive:!0,mode:448});continue}throw o}}async function Ak(e,t){let n=ct.stdout;console.log(`
|
|
253
292
|
${Ce(n,"omnish link")} ${w(n,"\u2014 QR appears below; scan from WhatsApp \u2192 Linked devices.")}
|
|
254
|
-
`),await
|
|
255
|
-
${
|
|
256
|
-
`)}})}async function
|
|
257
|
-
`));for(let o=1;o<=2;o++)try{await
|
|
258
|
-
${
|
|
259
|
-
`);return}catch(r){if(o===1&&
|
|
260
|
-
${
|
|
261
|
-
${
|
|
262
|
-
`),
|
|
263
|
-
${
|
|
264
|
-
${w(
|
|
293
|
+
`),await bl({authDir:e,verbose:t,printQr:!0,onRestart515:()=>{console.warn(`
|
|
294
|
+
${U(ct.stderr,"WhatsApp requested a restart after pairing (code 515). This is normal. Opening a new connection\u2026")}
|
|
295
|
+
`)}})}async function gm(e={}){let t=e.authDir??le,n=e.verbose===!0;se(),e.force&&(Ks.rmSync(t,{recursive:!0,force:!0}),Ks.mkdirSync(t,{recursive:!0,mode:448}),console.log(`${we(ct.stdout,"Cleared saved session (--force).")} ${w(ct.stdout,"Requesting a new QR\u2026")}
|
|
296
|
+
`));for(let o=1;o<=2;o++)try{await Ak(t,n),console.log(`
|
|
297
|
+
${he(ct.stdout,"Linked.")} ${v(ct.stdout,"Session saved. You can run")} ${he(ct.stdout,"omnish run")} ${v(ct.stdout,"now.")}
|
|
298
|
+
`);return}catch(r){if(o===1&&zs(r)){console.warn(`
|
|
299
|
+
${U(ct.stderr,"WhatsApp returned logged-out (401). This often happens after Ctrl+C during link or corrupt auth files.")}
|
|
300
|
+
${U(ct.stderr,"Clearing auth dir and retrying once with a fresh QR\u2026")}
|
|
301
|
+
`),Ks.rmSync(t,{recursive:!0,force:!0}),Ks.mkdirSync(t,{recursive:!0,mode:448});continue}throw zs(r)&&console.error(`
|
|
302
|
+
${C(ct.stderr,"Still failing after a clean auth directory. Try:")}
|
|
303
|
+
${w(ct.stderr,` pnpm approve-builds && pnpm install
|
|
265
304
|
(pnpm may have skipped Baileys/sharp/protobuf build scripts.)
|
|
266
305
|
Then: omnish link --force
|
|
267
|
-
`)}`),r}}G();import{spawn as
|
|
268
|
-
|
|
269
|
-
`)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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}
|
|
306
|
+
`)}`),r}}G();import{spawn as Ik,spawnSync as Lk}from"node:child_process";import So from"node:fs";import Ok from"node:path";import xo from"node:process";function Ys(e,t={}){se(),B(Ok.dirname(e));let n=xo.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=So.openSync(e,"a"),s=Ik(xo.execPath,[n,...o],{detached:!0,stdio:["ignore",r,r],env:{...xo.env,OMNISH_BACKGROUND_GATEWAY:"1"}});return So.closeSync(r),s.unref(),s.pid?{ok:!0,pid:s.pid}:{ok:!1,message:"Failed to start background gateway."}}function Qs(){if(se(),!So.existsSync(me))return{outcome:"no_pidfile"};let e=So.readFileSync(me,"utf8").trim(),t=Number(e);if(!Number.isFinite(t)||t<=0){try{So.unlinkSync(me)}catch{}return{outcome:"invalid_pidfile"}}try{xo.kill(t,0)}catch{try{So.unlinkSync(me)}catch{}return{outcome:"stale_cleaned",pid:t}}try{return xo.kill(t,"SIGTERM"),{outcome:"sent_signal",pid:t}}catch(n){return xo.platform==="win32"&&Lk("taskkill",["/PID",String(t),"/T"],{windowsHide:!0}).status===0?{outcome:"taskkill_ok",pid:t}:{outcome:"failed",message:`could not signal process: ${String(n)}`}}}import ym from"node:crypto";import kl from"node:fs";import Nk from"node:net";ot();xe();G();var lr=null;function Fk(){try{let e=kl.readFileSync(Bn,"utf8"),t=JSON.parse(e);return typeof t.token=="string"?t.token:""}catch{return""}}function _k(e){kl.writeFileSync(Bn,JSON.stringify(e,null,2)+`
|
|
307
|
+
`,{mode:384})}function Wk(){try{kl.unlinkSync(Bn)}catch{}}async function Dk(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()?Ut(n):n.fileSendMaxBytes,i=ft(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 u=Yt(c);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 d=n.gatewayMode;if(d!=="whatsapp"&&d!=="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 d=`tg:${e.chatId}`;try{return await u(d,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(u){return{ok:!1,error:String(u)}}}return{ok:!1,error:"Unknown channel."}}async function Uk(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 u=Yt(c);try{return await l(`wa:${u}`,o),{ok:!0}}catch(d){return{ok:!1,error:String(d)}}}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=Yt(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 Hk(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||!ym.timingSafeEqual(r,s))return{ok:!1,error:"Unauthorized."}}catch{return{ok:!1,error:"Unauthorized."}}return o.op==="sendMedia"?Dk(o,t):o.op==="sendText"?Uk(o,t):o.op==="sendPeerText"?jk(o,t):o.op==="sendPeerMedia"?Bk(o,t):{ok:!1,error:"Unsupported operation."}}async function Bk(e,t){let n=typeof e.peerKey=="string"?e.peerKey.trim():"",o=typeof e.absPath=="string"?e.absPath.trim():"";if(!n)return{ok:!1,error:"Missing peerKey."};if(!o)return{ok:!1,error:"Missing absPath."};let r=t.getCfg(),s=typeof e.caption=="string"&&e.caption.length>0?e.caption:void 0,i=t.sendPlatformMedia&&!t.getWaOutbound()&&!t.getTgSendMedia()?Ut(r):r.fileSendMaxBytes,a=ft(o,i);if("error"in a)return{ok:!1,error:a.error};let l={absPath:a.absPath,category:a.category,mimetype:a.mimetype,displayName:a.displayName,caption:s};if(t.sendPeerMedia)try{return await t.sendPeerMedia(n,l),{ok:!0}}catch(c){return{ok:!1,error:String(c)}}if(t.sendPlatformMedia)try{return await t.sendPlatformMedia(n,l),{ok:!0}}catch(c){return{ok:!1,error:String(c)}}if(n.startsWith("wa:")){let c=t.getWaOutbound();if(!c)return{ok:!1,error:"WhatsApp outbound is not connected."};let u=r.gatewayMode;if(u!=="whatsapp"&&u!=="both")return{ok:!1,error:"gatewayMode does not include WhatsApp."};try{return await c.sendMedia(n.slice(3),l),{ok:!0}}catch(d){return{ok:!1,error:String(d)}}}if(n.startsWith("tg:")){let c=t.getTgSendMedia();if(!c)return{ok:!1,error:"Telegram outbound is not connected."};let u=r.gatewayMode;if(u!=="telegram"&&u!=="both")return{ok:!1,error:"gatewayMode does not include Telegram."};let d=Number(n.slice(3));if(!Number.isFinite(d))return{ok:!1,error:"Invalid Telegram peer key."};try{return await c(d,l),{ok:!0}}catch(m){return{ok:!1,error:String(m)}}}return{ok:!1,error:"Unknown peer key prefix."}}async function jk(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 Vs(e){if(lr)return;let t=ym.randomBytes(32).toString("hex"),n=Nk.createServer(o=>{let r="";o.setTimeout(12e4),o.on("data",s=>{r+=s.toString("utf8");let i=r.indexOf(`
|
|
308
|
+
`);if(i===-1)return;let a=r.slice(0,i).trim();r=r.slice(i+1);let l=Fk();Hk(a,e,l).then(c=>{o.write(`${JSON.stringify(c)}
|
|
309
|
+
`),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};_k(r),P.info({port:o.port},"gateway control listening")}),n.on("error",o=>{P.error({err:String(o)},"gateway control server error")}),lr=n}function Co(){if(lr){try{lr.close()}catch{}lr=null,Wk()}}xe();import Gk from"node:crypto";import Jk from"node:http";var qk=256*1024;function zk(e,t){if(!e||!t)return!1;try{let n=Buffer.from(e,"utf8"),o=Buffer.from(t,"utf8");return n.length===o.length&&Gk.timingSafeEqual(n,o)}catch{return!1}}function Kk(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??"?",u=a.head_branch??"?",d=a.html_url??"",m=e.repository?.full_name??"?";return`[${t}] ${m} \u2014 ${l}
|
|
310
|
+
result: ${c}
|
|
311
|
+
branch: ${u}${d?`
|
|
312
|
+
${d}`:""}`}if(e.object_kind==="pipeline"&&e.object_attributes&&typeof e.object_attributes=="object"){let a=e.object_attributes,l=a.status??"?",c=a.ref??"?",u=a.id??"?",d=e.project?.path_with_namespace??"?";return`[${t}] ${d} \u2014 pipeline #${u}
|
|
276
313
|
status: ${l}
|
|
277
|
-
ref: ${
|
|
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
|
|
280
|
-
${w(n,`${i.localHost}:${i.localPort}`)}`);return}if(t.kind==="stop"){let s=await
|
|
281
|
-
`,{mode:384})}function
|
|
282
|
-
`).replace(/\n/g," ").trim();return t?t.length>
|
|
283
|
-
Script: ${s.scriptPath}`,u=["*Service status*","",`platform: ${
|
|
284
|
-
`),
|
|
285
|
-
`);return
|
|
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,
|
|
288
|
-
${
|
|
314
|
+
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(`
|
|
315
|
+
`)}var vl=null;function Xs(e,t){if(vl)return{stop:()=>{}};let n=Jk.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(!zk(i??a,e.token)){r.writeHead(401,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Unauthorized"}));return}let c="",u=0;o.on("data",d=>{if(u+=d.length,u>qk){r.writeHead(413,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Payload too large"})),o.destroy();return}c+=d.toString("utf8")}),o.on("end",()=>{let d;try{d=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&&(d.source=h);let f=typeof d.peerKey=="string"&&d.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=Kk(d);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")}),vl=n,{stop:()=>{try{n.close()}catch{}vl=null}}}ue();import ES from"node:crypto";import rc from"node:fs";xe();import Yk from"node:fs";function wm(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=en(e,t)}catch(a){return{mediaError:String(a)}}let i=bo(s,t,n.name);try{return Yk.writeFileSync(i,r,{mode:384}),{mediaSavedPath:i}}catch{return{mediaError:"Could not write media to inbox."}}}G();cr();xe();import Pl from"ws";import ev from"ws";var Zk=["/platform/device","/control/device"];function ni(e){let t=e.replace(/\/$/,"");return Zk.map(n=>{let o=new URL(n,t);return o.protocol=o.protocol==="https:"?"wss:":"ws:",o.toString()})}var tv=Math.ceil(Wd*1.4)+512*1024;function nv(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 Sm(e,t){let n=ni(e),o=null;for(let s of n)try{return{ws:await new Promise((a,l)=>{let c=new ev(s,{headers:{Authorization:`Bearer ${t}`},maxPayload:tv});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)),nv(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 ov=1e3,rv=6e4,oi=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 Sm(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(ov*2**this.reconnectAttempt,rv);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=Ud(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===Pl.OPEN){this.sendRaw(t);return}if(this.outboundQueue.length>=Dd){P.warn("platform outbound queue full; dropping reply");return}this.outboundQueue.push(t)}flushOutboundQueue(){for(;this.outboundQueue.length>0&&this.ws?.readyState===Pl.OPEN;){let t=this.outboundQueue.shift();t&&this.sendRaw(t)}}sendRaw(t){this.ws?.readyState===Pl.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 Il from"node:readline/promises";import{stdin as Ll,stdout as ui}from"node:process";ue();var sv=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;function Cm(e){let t=e.trim().toLowerCase();return t?t.length>63?"Tunnel name must be at most 63 characters.":sv.test(t)?null:"Tunnel name may only use letters, digits, and hyphens.":"Tunnel name must not be empty."}function Rm(e){let t=Number.parseInt(e,10);return!Number.isInteger(t)||t<1||t>65535?null:t}function xm(e,t){let n="127.0.0.1",o,r,s=!1,i=[];for(let c=0;c<t.length;c++){let u=t[c];if(u==="--host"){let d=t[++c];if(!d)return{kind:"error",message:"--host requires an address."};n=d;continue}if(u.startsWith("--host=")){n=u.slice(7);continue}if(u==="--relay"){let d=t[++c];if(!d)return{kind:"error",message:"--relay requires a URL."};o=d;continue}if(u.startsWith("--relay=")){o=u.slice(8);continue}if(u==="--name"){let d=t[++c];if(!d)return{kind:"error",message:"--name requires a slug."};r=d;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=Rm(a);if(l===null)return{kind:"error",message:"Port must be an integer between 1 and 65535."};if(r){let c=Cm(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 Ml(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 u=n[c];if(u==="--token"){r=n[++c];continue}if(u.startsWith("--token=")){r=u.slice(8);continue}if(u==="--email"){s=n[++c];continue}if(u.startsWith("--email=")){s=u.slice(8);continue}if(u==="--phone"){i=n[++c];continue}if(u.startsWith("--phone=")){i=u.slice(8);continue}if(u==="--password"){a=n[++c];continue}if(u.startsWith("--password=")){a=u.slice(11);continue}if(u==="--relay"){l=n[++c];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"?xm("http",n):o==="tcp"?xm("tcp",n):{kind:"error",message:`Unknown tunnel subcommand "${t}". Try: omnish tunnel help`}}function iv(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 Tm(e){let t=e.trim();if(!t||t==="help")return{kind:"help"};let n=iv(t),o=n[0]?.toLowerCase();if(o==="login"||o==="logout"||o==="status"||o==="signup")return Ml(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=Rm(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 u=Cm(l);if(u)return{kind:"error",message:u}}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 av from"ws";import{URL as $m}from"node:url";function Ro(e){let t=new $m(e);return t.protocol=t.protocol==="https:"?"wss:":"ws:",(!t.pathname||t.pathname==="/")&&(t.pathname="/control"),t.toString()}function Pm(e){return new $m("/health",e).toString()}async function ri(e,t,n=1e4){let o=t.trim();if(!o)return{ok:!1,healthOk:!1,controlOk:!1,error:"Tunnel token is missing."};let r=Pm(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 u=await c.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(c){return{ok:!1,healthOk:!1,controlOk:!1,error:`Health fetch failed: ${String(c)}`}}let a=Ro(e),l=!1;try{await new Promise((c,u)=>{let d=new av(a,{headers:{Authorization:`Bearer ${o}`}}),m=setTimeout(()=>{d.terminate(),u(new Error("WSS auth timeout"))},n);d.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}d.close(),c()}catch(f){u(f)}}),d.once("error",h=>{clearTimeout(m),u(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}}xn();Cn();Cn();xn();import Am from"node:crypto";import lv from"node:http";import cv from"node:net";import gn from"ws";function si(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 Mm(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 To(e){return JSON.stringify(e)}function uv(e){let t=Am.createHash("sha1").update(e,"utf8").digest("hex").slice(0,8);return Number.parseInt(t,16)>>>0}var ii=class{constructor(t){this.opts=t;let n=Am.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=Ro(this.opts.relayUrl),n=new gn(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===gn.OPEN&&n.send(To({type:"ping"}))},3e4),n.send(To({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=Mm(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!==gn.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=lv.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(To({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(To({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!==gn.OPEN)return;let o=uv(t);this.tcpStreamIds.set(t,o);let r=cv.connect({host:this.opts.expose.host,port:this.opts.expose.port});this.tcpStreams.set(t,r),r.on("data",s=>{n.readyState===gn.OPEN&&n.send(si(2,o,Buffer.isBuffer(s)?s:Buffer.from(s)))}),r.on("close",()=>{n.readyState===gn.OPEN&&n.send(si(3,o)),this.tcpStreams.delete(t),this.tcpStreamIds.delete(t)}),r.on("error",()=>{n.readyState===gn.OPEN&&n.send(si(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===gn.OPEN&&(t.send(To({type:"unregister",id:this.record.id})),t.close()),this.cleanupTcpStreams(),this.setStatus("stopped")}};var ai=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=Rt();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||mt(t.tunnelRelayUrl||Ee),i=new ii({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 Im(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 li(e,t){let n=new URL("/auth/signup",e).toString();return Im(n,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:t.password})}function ci(e,t){let n=new URL("/auth/login",e).toString();return Im(n,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:t.password})}var $o=new ai;function Nn(){return $o}function dv(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(`
|
|
316
|
+
`))}async function pv(){let e=Il.createInterface({input:Ll,output:ui});try{return(await e.question("Tunnel token: ")).trim()}finally{e.close()}}async function Lm(e){let t=Ml(e),n=ui,o=process.stderr;if(t.kind==="help"){dv(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||mt(r.tunnelRelayUrl||Ee),i=Il.createInterface({input:Ll,output:ui});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 u=await li(s,{...a?{email:a}:{},...l?{phone:l}:{},password:c});if(!u.ok){console.error(C(o,u.error)),process.exitCode=1;return}Ct({token:u.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||mt(r.tunnelRelayUrl||Ee),l=Il.createInterface({input:Ll,output:ui});try{let c=t.password||(await l.question("Password: ")).trim(),u=await ci(a,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:c});if(!u.ok){console.error(C(o,u.error)),process.exitCode=1;return}Ct({token:u.token,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(U(n,"Logged in. Token saved."))}finally{l.close()}return}let i=t.token?.trim()||await pv();if(!i){console.error(C(o,"Tunnel token is required.")),process.exitCode=1;return}Ct({token:i,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(U(n,"Tunnel token saved."));return}if(t.kind==="logout"){Zr(),console.log(U(n,"Tunnel token removed."));return}if(t.kind==="status"){let s=t.relayUrl||mt(r.tunnelRelayUrl||Ee),i=Rt(),a=!!process.env.OMNISH_TUNNEL_TOKEN?.trim(),l=await ri(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($o.getActiveCount()))}`);return}if(t.kind==="list"){let s=$o.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)"}
|
|
317
|
+
${w(n,`${i.localHost}:${i.localPort}`)}`);return}if(t.kind==="stop"){let s=await $o.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||mt(r.tunnelRelayUrl||Ee),i=await $o.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 $o.stop(i.id),a()};process.once("SIGINT",l),process.once("SIGTERM",l)}))}}function Om(e){let t=e.trim().match(/^(\d+)\.(\d+)\.(\d+)/);return t?[Number(t[1]),Number(t[2]),Number(t[3])]:null}function Nm(e,t){let n=Om(e),o=Om(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 mv="https://registry.npmjs.org",Fm=null,Ol=0;function ur(){return Fm}function hv(e){let t=e.trim();return t.length<1||t.length>214?!1:!/\s/.test(t)}function _m(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 fv(e){let t=e.trim(),n=`${mv}/${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 gv(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=_m(c)),{message:a,link:l}}catch(t){return{error:`updateInfoUrl: ${String(t)}`}}}function yv(e){return!Number.isFinite(e)||e<36e5?36e5:e>6048e5?6048e5:Math.floor(e)}async function dr(e,t){let n=hv(t.updateCheckPackageName)?t.updateCheckPackageName.trim():"omnish",o=await fv(n),r="version"in o?o.version:null,s="error"in o?o.error:null,i=!1;r&&!s&&(i=Nm(e,r)<0);let a=null,l=null,c=null,u=_m(t.updateInfoUrl);if(u){let m=await gv(u);"error"in m?c=m.error:(a=m.message,l=m.link)}let d={runningVersion:e,checkedAtIso:new Date().toISOString(),registryPackage:n,registryLatest:r,registryError:s,updateAvailable:i,infoMessage:a,infoLink:l,infoError:c};return Fm=d,d}function di(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 pi(e){let t=!1,n=async()=>{if(t)return;let s=e.getConfig();if(!s.updateCheckEnabled)return;let i=yv(s.updateCheckIntervalMs),a=Date.now();if(!(Ol!==0&&a-Ol<i)){Ol=a;try{let l=await dr(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)}}ue();tt();import Wm from"node:path";import{glob as wv,stat as bv}from"node:fs/promises";function mi(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 kv(e){return/[*?[]/.test(e)}function vv(e){return e.split(",").map(t=>t.trim()).filter(t=>t.length>0)}async function pr(e,t){let n=vv(t),o=new Set,r=[];for(let s of n){if(kv(s)){for await(let a of wv(s,{cwd:e,withFileTypes:!1})){let l=Wm.resolve(e,a);o.has(l)||(o.add(l),r.push(l))}continue}let i=Wm.resolve(e,s);o.has(i)||(o.add(i),r.push(i))}return r}async function mr(e){for(let t of e)try{if(!(await bv(t)).isFile())return{ok:!1,error:`Not a file: ${t}`}}catch{return{ok:!1,error:`File not found: ${t}`}}return{ok:!0}}G();import Hm from"node:fs";import Sv from"node:path";var Fn="__omnish_shortcuts_global__",Dm=500,Bm=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,xv=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"]),hr=new Map,Um=!1;function Cv(){try{let e=Hm.readFileSync($r,"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())}hr.set(n,r)}}catch{}}function hi(){B(Sv.dirname($r));let e={};for(let[t,n]of hr)Object.entries(n).length>0&&(e[t]={...n});Hm.writeFileSync($r,JSON.stringify(e,null,2)+`
|
|
318
|
+
`,{mode:384})}function fi(){Um||(Cv(),Um=!0)}function Rv(e){return xv.has(e.trim().toLowerCase())}function Mt(e){let t=e.trim();if(!t)return{ok:!1,error:"Name is empty."};let n=t.toLowerCase();return Bm.test(n)?Rv(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 Nl(e){let t=e.replace(/\r\n/g,`
|
|
319
|
+
`).replace(/\n/g," ").trim();return t?t.length>Dm?{ok:!1,error:`Body too long (max ${Dm} characters).`}:{ok:!0,body:t}:{ok:!1,error:"Body is empty."}}function Pt(e,t){fi();let n=hr.get(e);return!n&&t&&(n={},hr.set(e,n)),n??{}}function jm(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 Fl(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 _l(e){let t=Fl(e);return{scope:t.scope,remainder:t.remainder}}function Gm(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 Tv(e){let t=e.trim().toLowerCase();if(t==="--global"||t==="-g")return"global";if(t==="--chat"||t==="-p")return"chat"}function Jm(e){let t=e.trim().match(/^(\S+)\s+(--global|-g|--chat|-p)\s*$/i);if(!t?.[1]||!t[2])return;let n=Tv(t[2]);if(n)return{name:t[1],target:n}}function $v(e,t){fi();let n=e,o=Pt(n,!1),r=Pt(Fn,!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 qm(e,t="merged"){return $v(e,t)}function Wl(e,t){let n=t.trim().toLowerCase(),o=Pt(e,!1)[n];return o!==void 0?o:Pt(Fn,!1)[n]}function gi(e,t){let n=Mt(t);if(!n.ok)return;let o=n.normalized,r=Pt(e,!1)[o];if(r!==void 0)return{name:o,body:r,scope:"chat"};let s=Pt(Fn,!1)[o];if(s!==void 0)return{name:o,body:s,scope:"global"}}function Jt(e,t,n){let o=n.trim().toLowerCase(),r=e==="global"?Fn:t;return Pt(r,!1)[o]}function fr(e,t,n,o="chat"){let r=Mt(t);if(!r.ok)throw new Error(r.error);let s=Nl(n);if(!s.ok)throw new Error(s.error);let i=o==="global"?Fn:e,a=Pt(i,!0);a[r.normalized]=s.body,hi()}function zm(e,t,n="chat"){let o=t.trim().toLowerCase(),r=n==="global"?Fn:e;fi();let s=hr.get(r);return!s||!(o in s)?!1:(delete s[o],hi(),!0)}function Dl(e,t,n){let o=Mt(t);if(!o.ok)return{ok:!1,error:o.error};let r=o.normalized,s=e;fi();let i=Pt(s,!0),a=Pt(Fn,!0),l=i[r],c=a[r];if(n==="global"){if(l!==void 0){let u=Nl(l);return u.ok?(a[r]=u.body,delete i[r],hi(),{ok:!0,kind:"moved",target:"global",name:r}):{ok:!1,error:u.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 u=Nl(c);return u.ok?(i[r]=u.body,delete a[r],hi(),{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 Km(e){let t=e.trim();return t.length>0&&!/\s/.test(t)&&Bm.test(t.toLowerCase())}import Ym from"node:fs";var Qm=64,Pv=1024*1024;function Vm(e){return e.fileReceiveMaxBytes>0?e.fileReceiveMaxBytes:Pv}function Xm(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>Qm)return{ok:!1,error:`Too many jobs (max ${Qm}).`};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 Zm(e,t,n){let o=[];for(let r=0;r<n.length;r++){let{recipe:s,task:i}=n[r],a=Xe(e,t,s);if(!a)return{ok:!1,error:`Job ${r+1}: unknown recipe "${s}".`};let l=a.taskEnv??"OMNISH_TASK";if(!ho(a.command,l))return{ok:!1,error:`Job ${r+1}: recipe "${s}" command must reference "$${l}".`};let c=xs(i,t.recipesMaxTaskChars);if(!c.ok)return{ok:!1,error:`Job ${r+1}: ${c.error}`};let u=a.promptTemplate?Cs(a.promptTemplate,l,c.task):c.task,d={[l]:u};o.push({command:a.command,extraEnv:d,recipeLabel:s})}return{ok:!0,items:o}}function Ul(e,t){let n;try{n=Ym.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:Ym.readFileSync(e,"utf8")}}catch(o){return{ok:!1,error:String(o)}}}G();import eh from"node:fs";import Po from"node:process";var Mv=120;function qt(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function th(e){let t=e.trim();return t==="/service"||t.startsWith("/service ")?t.slice(8).trim():null}async function nh(e,t){let n=t.trim().split(/\s+/),o=(n[0]??"").toLowerCase();if(!t.trim()||o==="help")return Q(Eu(e));if(o==="status"){let s=Bt(),i=(()=>{try{return eh.existsSync(me)?`gateway.pid: ${eh.readFileSync(me,"utf8").trim()}`:"gateway.pid: (missing)"}catch(m){return`gateway.pid: (read error: ${String(m)})`}})(),a=Po.env.OMNISH_BACKGROUND_GATEWAY==="1"?"This process: background gateway (OMNISH_BACKGROUND_GATEWAY=1).":"This process: foreground gateway session.",l=typeof Po.env.OMNISH_HOME=="string"&&Po.env.OMNISH_HOME.trim()?`OMNISH_HOME env: ${Po.env.OMNISH_HOME.trim()}`:"OMNISH_HOME env: (not set \u2014 using default data dir)",c=s.error?s.error:`Node: ${s.nodePath}
|
|
320
|
+
Script: ${s.scriptPath}`,u=["*Service status*","",`platform: ${Po.platform}`,a,l,`data dir: ${D}`,i,`default log: ${je}`,"",c,"",e.serviceInstallFromChat?"Install from chat: enabled (/service install).":"Install from chat: off \u2014 `/config set serviceInstallFromChat true` to allow /service install."].join(`
|
|
321
|
+
`),d=["<b>Service status</b>","",`<code>${qt(Po.platform)}</code>`,`<br/><code>${qt(a)}</code>`,`<br/><code>${qt(l)}</code>`,`<br/>data dir: <code>${qt(D)}</code>`,`<br/><code>${qt(i)}</code>`,`<br/>default log: <code>${qt(je)}</code>`,"",`<pre>${qt(c)}</pre>`,"",e.serviceInstallFromChat?"Install from chat: enabled.":"Install from chat: off \u2014 <code>/config set serviceInstallFromChat true</code>."].join(`
|
|
322
|
+
`);return ge(u,d)}if(o==="instructions"){let s=Bt();if(s.error)return p(s.error);let i=rs(s);return p(`*Install hints*
|
|
323
|
+
|
|
324
|
+
${i}`)}if(o==="logs"){let s=n.length>=2?Number.parseInt(n[1],10):80,i=Number.isFinite(s)&&s>0?Math.min(s,Mv):80,a=bs(je,i),l=[`*Gateway log* (last ${i} lines)
|
|
325
|
+
${je}
|
|
289
326
|
`,"```",a,"```"].join(`
|
|
290
|
-
`),
|
|
327
|
+
`),c=`<b>Gateway log</b> (last ${i} lines)<br/><code>${qt(je)}</code><pre>${qt(a)}</pre>`;return ge(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=gs();return p(s.ok?`*Installed*
|
|
291
328
|
${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=
|
|
293
|
-
${s.detail}`)}return p("Unknown /service command. Try /service help")}
|
|
294
|
-
|
|
295
|
-
${
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
${
|
|
299
|
-
`)}`)}}catch(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
/
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
`))}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
`))
|
|
316
|
-
`);
|
|
317
|
-
`))
|
|
318
|
-
`))
|
|
319
|
-
`))}function
|
|
320
|
-
`))}
|
|
321
|
-
`))}
|
|
322
|
-
`))}
|
|
323
|
-
`))
|
|
324
|
-
`))
|
|
325
|
-
`)
|
|
326
|
-
`))
|
|
329
|
+
${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=ys();return p(`*Uninstall*
|
|
330
|
+
${s.detail}`)}return p("Unknown /service command. Try /service help")}function Mo(e,t){let n=e.trim(),o=`/${t}`;return n===o||n.startsWith(`${o} `)?n.slice(o.length).trim():null}var oh=e=>Mo(e,"dl"),rh=e=>Mo(e,"dlf"),sh=e=>Mo(e,"dlv"),ih=e=>Mo(e,"tr"),ah=e=>Mo(e,"edit"),lh=e=>Mo(e,"pull");function gr(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 Ev(e,t,n,o){let r=Bt();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 Av(e,t,n){return is({peerKey:t,enabled:e.progressUpdates,sendToPeer:n?.sendToPeer})}function yr(e,t,n){let o=ie(n.peerKey).cwd,r=Ev(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?`
|
|
331
|
+
Notify on completion: on`:"";return{kind:"text",body:p(`${n.label} job ${s} started.
|
|
332
|
+
/log ${i.name??s}
|
|
333
|
+
/tail ${i.name??s}${a}`)}}async function Iv(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=Av(e,n,o);try{let i=await us({whisper:r,progress:{stepStart:(a,l,c)=>s.stepStart(a,l,{label:c})}});return{kind:"text",body:p(`*Install*
|
|
334
|
+
|
|
335
|
+
${i.messages.join(`
|
|
336
|
+
`)}`)}}catch(i){return{kind:"text",body:p(`Install failed: ${String(i)}`)}}}async function _n(e,t,n,o,r){let s=t.trim(),i=(s.split(/\s+/)[0]??"").toLowerCase();if(!s||i==="help")return{kind:"text",body:Q(Bo(e))};if(i==="doctor"){let{text:c}=cs(e);return{kind:"text",body:p(`*Media tools*
|
|
337
|
+
|
|
338
|
+
${c}`)}}if(i==="setup"||i==="instructions")return{kind:"text",body:p(ss())};if(i==="install")return Iv(e,s,n,r);let{body:a,notify:l}=gr(s);return a?yr(e,o,{sub:"dl",body:a,peerKey:n,jobName:"dl",label:"Download",notify:l}):{kind:"text",body:Q(Bo(e))}}async function ch(e,t,n,o,r){let{body:s,notify:i}=gr(t.trim());return s?yr(e,o,{sub:"dlf",body:s,peerKey:n,jobName:"dlf",label:"File download",notify:i}):{kind:"text",body:Q(Bo(e))}}async function uh(e,t,n,o,r){let{body:s,notify:i}=gr(t.trim());return s?yr(e,o,{sub:"dlv",body:s,peerKey:n,jobName:"dlv",label:"Video download",notify:i}):{kind:"text",body:Q(Bo(e))}}async function Hl(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)*
|
|
339
|
+
|
|
340
|
+
/tr <url|path>
|
|
341
|
+
|
|
342
|
+
URL: downloads video, transcribes, sends text + .srt + video.
|
|
343
|
+
Path: transcribes local file, sends text + .srt.
|
|
344
|
+
|
|
345
|
+
Runs in background. Use /tr --notify for a completion ping when the shell job ends.`)};if(i==="doctor"||i==="setup"||i==="install")return _n(e,s,n,o,r);let{body:a,notify:l}=gr(s);return a?et(e).whisper?yr(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 dh(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(`
|
|
346
|
+
`))};if(i==="doctor"||i==="setup"||i==="install")return _n(e,s,n,o,r);let{body:a,notify:l}=gr(s);return a?yr(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 ph(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&&Qo(s)?_n(e,a||s,n,o,r):i==="transcript"||i==="subs"?Hl(e,a,n,o,r):i==="doctor"||i==="setup"||i==="install"||i==="help"||!s?_n(e,s,n,o,r):{kind:"text",body:p(`/pull is deprecated. Use:
|
|
347
|
+
/dl <url> \u2014 download video
|
|
348
|
+
/tr <url|path> \u2014 transcribe
|
|
349
|
+
/edit <url|path> \u2014 trim/convert
|
|
350
|
+
|
|
351
|
+
/dl help for full usage.`)}}async function mh(e,t,n,o,r){return!e.mediaUrlAutoDl||!Qo(t)?null:_n(e,t.trim(),n,o,r)}ue();tt();Cn();function yi(){let e=oe();return e?.platformUrl?e.platformUrl.replace(/\/$/,""):(S().tunnelRelayUrl||Ee).trim().replace(/\/$/,"")}function wi(){let e=oe();return e?.token?{Authorization:`Bearer ${e.token}`}:{}}async function wr(e){try{return await e.json()}catch{return{error:`HTTP ${e.status}`}}}function Lv(){return`Set platform URL: omnish config add tunnelRelayUrl ${Ee} \u2014 publish: omnish platform login`}function Bl(){return oe()?.token?{ok:!0}:{ok:!1,error:`Publish requires a platform account token. ${Lv()}`}}async function hh(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=`${yi()}/v1/catalog/trending${n?`?${n}`:""}`,r=await fetch(o,{headers:wi()}),s=await wr(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}async function fh(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=`${yi()}/v1/catalog/search?${n}`,r=await fetch(o,{headers:wi()}),s=await wr(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}async function gh(e){let t=encodeURIComponent(e.trim()),n=`${yi()}/v1/catalog/${t}`,o=await fetch(n,{headers:wi()}),r=await wr(o);return o.ok?r:{error:r.error??`HTTP ${o.status}`}}async function yh(e){let t=encodeURIComponent(e.trim()),n=`${yi()}/v1/catalog/${t}/download`,o=await fetch(n,{method:"POST",headers:wi()}),r=await wr(o);return o.ok?r:{error:r.error??`HTTP ${o.status}`}}async function wh(e){let t=Bl();if(!t.ok)return{error:t.error};let n=oe(),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 wr(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}function bh(e){return e==="global"?"global":"chat"}function kh(e,t,n,o="global"){switch(e.kind){case"recipe":{let r=e.payload,s=Ve({...r,dangerous:void 0}),i=Pn(s);if(!i.ok)return{ok:!1,error:i.error};let a=$t(e.name);if(!a.ok)return{ok:!1,error:a.error};try{fo(t,a.normalized,s,bh(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=Ve({command:r.command.trim(),label:r.label??e.title,description:r.description??e.description,category:e.category||"app"}),i=$t(e.name);if(!i.ok)return{ok:!1,error:i.error};try{fo(t,i.normalized,s,bh(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=Mt(e.name);if(!s.ok)return{ok:!1,error:s.error};try{fr(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=Me();if(rt(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:Wo(),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 vh(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 Ve(n)}var jl=new Map;function Gl(e,t){return`${e}:${t}`}function Jl(e,t,n){jl.set(Gl(e,t),n)}function xh(e,t){return jl.get(Gl(e,t))}function bi(e,t,n){let o=Number.parseInt(n,10);if(!Number.isFinite(o)||o<1)return null;let r=jl.get(Gl(e,t));return!r||o>r.length?null:r[o-1].publicId}function Wn(e){return!!(e&&typeof e=="object"&&typeof e.error=="string")}function ki(e,t){return`${e.commandPrefix} online ${t}`}function Ch(e){let t=r=>ki(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(`
|
|
352
|
+
`))}function vi(e,t,n){if(e.length===0)return p(`${t}
|
|
353
|
+
(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: ${ki(n,"<publicId> download")} \u2014 or ${ki(n,"trending <n> download")}`),p(o.join(`
|
|
354
|
+
`))}function Rh(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("",ki(t,`${e.publicId} download`)),p(n.join(`
|
|
355
|
+
`))}var ql=new Set(["recipe","app","cowork","shortcut"]),Ov={commandPrefix:"/run",listScope:"run"},Th={commandPrefix:"/apps",defaultKind:"app",listScope:"apps"},$h={commandPrefix:"/cowork",defaultKind:"cowork",listScope:"cowork"},Ph={commandPrefix:"/shortcut",defaultKind:"shortcut",listScope:"shortcut"};function Nv(e){if(!e)return;let t=e.toLowerCase();return ql.has(t)?t:void 0}function zt(e,t){return`${e.commandPrefix} online ${t}`}function Fv(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 _v(e,t){if(e.defaultKind){if(t.length>1&&ql.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&&ql.has(t[t.length-1].toLowerCase())&&(n=t.pop().toLowerCase()),{kind:n,query:t.join(" ")}}async function Eo(e,t,n,o){let r=e.trim();if(!r||/^help$/i.test(r))return Ch(o);let s=r.match(/^(.+?)\s+download(?:\s+(.*))?$/i);if(s){let c=s[1].trim(),u=(s[2]??"").trim(),d=/--chat|-p/i.test(u)?"chat":"global",m=null,h=/^trending\s+(\d+)$/i.exec(c),f=/^search\s+(\S+)\s+(\d+)$/i.exec(c);if(h){if(m=bi(t,o.listScope,h[1]),!m)return p(`No trending list in this chat \u2014 run ${zt(o,"trending")} first, or use ${zt(o,"<publicId> download")}.`)}else if(f){if(m=bi(t,o.listScope,f[2]),!m)return p(`No search list in this chat \u2014 run ${zt(o,"search <query>")} first, or use ${zt(o,"<publicId> download")}.`)}else if(/^\d+$/.test(c)){if(m=bi(t,o.listScope,c),!m)return p(`No list item #${c}. Run trending or search first, or use ${zt(o,"<publicId> download")}.`)}else m=c;let g=await yh(m);if(Wn(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=kh(g,t,n,d);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 gh(i[1]);return Wn(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]}.`):Rh(c,o)}let a=/^trending(?:\s+(\S+))?\s*$/i.exec(r);if(a){let c=Fv(o,a[1]);if(typeof c=="object"&&"error"in c)return p(c.error);let u=await hh(c?{kind:c}:void 0);if(Wn(u))return p(`Trending failed: ${u.error}`);Jl(t,o.listScope,u.items);let d=o.defaultKind?`Trending (${o.defaultKind})`:"Trending";return vi(u.items,d,o)}let l=/^search\s+([\s\S]+)$/i.exec(r);if(l){let c=l[1].trim().split(/\s+/),u=_v(o,c);if("error"in u)return p(u.error);let{kind:d,query:m}=u;if(!m)return p(`Usage: ${zt(o,"search <query>")}`);let h=await fh(m,d?{kind:d}:void 0);return Wn(h)?p(`Search failed: ${h.error}`):(Jl(t,o.listScope,h.items),vi(h.items,`Search: ${m}`,o))}if(/^list\s*$/i.test(r)){let c=xh(t,o.listScope);return c?.length?vi(c,"Last list",o):p(`No cached list. ${zt(o,"trending")} or ${zt(o,"search <query>")}`)}return p(`Unknown ${o.commandPrefix} online command. ${zt(o,"help")}`)}async function Mh(e,t,n){return Eo(e,t,n,Ov)}function br(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 u of c.split(/[,;]/)){let d=u.trim();d&&n.push(d.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 Wv(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 Eh(e,t,n,o){let r=br(o),s=n.trim().toLowerCase(),i=Xe(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:Wv(i.name,i,r)}:{ok:!1,error:`Unknown recipe "${n}".`}}function Ah(e,t,n){let o=br(n),r=gi(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 Ih(e,t,n){let o=br(n),r=Me(),s=rt(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:vh(s)}}:{ok:!1,error:`Unknown cowork task "${t}" for this chat.`}}function Lh(e,t,n){let o=br(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 Ao(e){let t=Bl();if(!t.ok)return{ok:!1,error:t.error};let n=await wh(e);if(Wn(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 Dv(){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(`
|
|
356
|
+
`))}function Oh(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 Uv(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 Fh(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return Dv();let r=/^online\b([\s\S]*)$/i.exec(o);if(r)return Eo((r[1]??"").trim(),t,n,$h);let s=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(o);if(s){let h=Ih(t,s[1],s[2]??"");if(!h.ok)return p(h.error);let f=await Ao(h.body);return p(f.ok?f.message:f.error)}let i=o.split(/\s+/)[0].toLowerCase();if(/^list$/i.test(i)){let h=Me().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} ${go(g.schedule)} notify=${g.notify}${b}${y}`});return p(f.join(`
|
|
357
|
+
`))}let a=o.match(/^show\s+(\S+)\s*$/i);if(a){let h=a[1],f=Me();En(f);let g=rt(f,h,t);if(!g)return p(`Unknown task "${h}". /cowork list`);let y=Os(g),b=[`name: ${g.name}`,`id: ${g.id}`,`schedule: ${go(g.schedule)}`,`enabled: ${g.enabled}`,`notify: ${g.notify}`,`notifyWhen: ${g.notifyWhen??"always"}`];if(g.schedule.kind==="heartbeat"){let k=Fs(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(`
|
|
358
|
+
`))}if(/^add$/i.test(i)){let h=o.slice(3).trim(),f=h.match(/^(\S+)\s+(heartbeat\s+.+)$/i);if(f){let x=Yi(f[1]);if(!x.ok)return p(x.error);let O=f[2].split(/\s+/).filter(Boolean),E=Ls(O);if(!E.ok)return p(E.error);let K=Me();if(rt(K,x.name,t))return p(`Task "${x.name}" already exists. Remove it first or pick another name.`);let te=Dr(x.name),ce={id:Wo(),name:x.name,ownerPeerKey:t,command:"",cwd:"",outputDir:te,schedule:E.schedule,enabled:!0,notify:"self",notifyWhen:"always",attachLog:!1,attachFiles:[],lastCompletedSlotMs:null,createdAtMs:Date.now()};return K.push(ce),De(K),En(K),ul(ce.id),p([`Saved heartbeat task "${x.name}" (${go(E.schedule)}).`,`Send /cowork checkin ${x.name} to record a heartbeat.`,"Alerts if no check-in arrives within the expected interval + grace period."].join(`
|
|
359
|
+
`))}let g=Uv(h);if("error"in g)return p(g.error);let y=Yi(g.name);if(!y.ok)return p(y.error);let b=Ls(g.scheduleWords);if(!b.ok)return p(b.error);let k=Me();if(rt(k,y.name,t))return p(`Task "${y.name}" already exists. Remove it first or pick another name.`);let T=ie(t),$=Dr(y.name),L={id:Wo(),name:y.name,ownerPeerKey:t,command:g.command,cwd:"",outputDir:$,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}" (${go(b.schedule)}).`,`Output: ${$}`,`Notify: self \u2014 change with /cowork set ${y.name} notify wa|tg|all|none`].join(`
|
|
360
|
+
`))}let l=o.match(/^run\s+(\S+)\s*$/i);if(l){let h=l[1].toLowerCase(),f=rt(Me(),h,t);return f?f.enabled?(Wc({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=Me();En(f);let g=rt(f,h,t);return g?g.schedule.kind!=="heartbeat"?p(`"${g.name}" is not a heartbeat task.`):(ul(g.id),p(`Heartbeat recorded for "${g.name}".`)):p(`Unknown task "${c[1]}". /cowork list`)}let u=o.match(/^(?:remove|rm|del)\s+(\S+)\s*$/i);if(u){let h=u[1],f=Me(),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 d=o.match(/^enable\s+(\S+)\s*$/i);if(d)return Nh(d[1],t,!0);let m=o.match(/^disable\s+(\S+)\s*$/i);return m?Nh(m[1],t,!1):/^set$/i.test(i)?Gv(o.slice(3).trim(),t):p("Unknown /cowork command. Try /cowork help")}function Nh(e,t,n){let o=Me(),r=rt(o,e,t);return r?(r.enabled=n,De(o),p(`Cowork "${r.name}" ${n?"enabled":"disabled"}.`)):p(`Unknown task "${e}".`)}function Hv(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 Bv(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 jv(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 Gv(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=Me(),i=rt(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),u;if(c!==-1)u=a.slice(c+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,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=Ls(a);return l.ok?(i.schedule=l.schedule,De(s),p(`Schedule for "${i.name}": ${go(l.schedule)}`)):p(l.error)}if(r.toLowerCase().startsWith("out ")){let a=r.slice(4).trim(),l=ie(t);return i.outputDir=Ze(a,l.cwd),De(s),p(`outputDir: ${i.outputDir}`)}if(r.toLowerCase().startsWith("cwd ")){let a=r.slice(4).trim(),l=ie(t);return i.cwd=a?Ze(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=Hv(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=Bv(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=Oh(a);if(l.length!==1)return p("Usage: /cowork set <name> attach on|off");let c=jv(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=Oh(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")}ue();import dS from"node:fs";G();import _h from"node:os";import Jv from"node:path";var qv=new Set(["create","delete","rename","update"]);function zv(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 Kv(e){let t=e.split(",").map(n=>n.trim().toLowerCase());return t.length===0?!1:t.every(n=>qv.has(n))}function Yv(e){return e.split(",").map(t=>t.trim().toLowerCase())}function zl(e,t,n){let o=e.trim();if(o.startsWith("-")&&(o=o.slice(1)),!o)return{};let r=Ze(o,n);return o.includes("*")||o.includes("?")?{glob:o.replace(/\\/g,"/")}:Jv.isAbsolute(r)||o.startsWith("~")||o.startsWith("./")||o.startsWith("../")?{path:r}:o.startsWith("/")?{path:r}:{glob:o.includes("/")?o:`**/${o}`}}function Wh(e,t=_h.homedir()){let n=e.replace(/\s+&&\s+/g," ").trim(),o=zv(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 d=o[r];r+=1;let m=Ze(s,t),h=zl(d.startsWith("-")?d:`-${d}`,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 d=Ze(s,t),m=zl(u,d,t);m.path?a.push(m.path):m.glob&&l.push(m.glob),r+=1;continue}if(Kv(u)){i=Yv(u),r+=1;continue}return{ok:!1,error:`Unexpected token "${u}". Put path first, then events or -excludes.`}}return{ok:!0,value:{rootPath:Ze(s,t),events:i,excludePaths:a,excludeGlobs:l}}}function Kl(e,t,n=_h.homedir()){let o=zl(e.startsWith("-")?e:`-${e}`,t,n);return!o.path&&!o.glob?{error:"Empty exclude pattern."}:o}import{spawnSync as Si}from"node:child_process";import Dh from"node:fs";import Qv from"node:os";import Vv from"node:path";var Xv=40,Zv=10,xi=15e3,eS=["~/Projects","~/deploy","~/Downloads","~/src","/var/www","/srv"],Uh={linux:"/var/log/dpkg.log (also /var/log/apt/history.log)",darwin:"/var/log/install.log",win32:"Windows Application event log (install/remove)"};function tS(e){return e.startsWith("~/")?Vv.join(Qv.homedir(),e.slice(2)):e}function nS(e,t){return t?.trim()?e.toLowerCase().includes(t.trim().toLowerCase()):!0}function oS(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 rS(e){let t=[];for(let n of e.split(`
|
|
361
|
+
`)){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 sS(e){let t=[],n=e.split(`
|
|
362
|
+
`);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 iS(e){let t=[],n="";for(let o of e.split(`
|
|
363
|
+
`)){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 aS(){let e=Si("systemctl",["list-units","--type=service","--all","--no-pager","--plain","--no-legend"],{encoding:"utf8",timeout:xi});return e.error||e.status!==0?{entries:[],error:"systemctl unavailable \u2014 install systemd or run on Linux."}:{entries:rS(e.stdout??"")}}function lS(){let e=Si("launchctl",["list"],{encoding:"utf8",timeout:xi});return e.error||e.status!==0?{entries:[],error:"launchctl unavailable."}:{entries:sS(e.stdout??"")}}function cS(){let e=Si("sc",["query","type=","service","state=","all"],{encoding:"utf8",timeout:xi,windowsHide:!0});if(!e.error&&e.status===0&&(e.stdout??"").includes("SERVICE_NAME"))return{entries:iS(e.stdout??"")};let t=Si("powershell",["-NoProfile","-Command","Get-Service | ForEach-Object { $_.Name + ' ' + $_.Status }"],{encoding:"utf8",timeout:xi,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(`
|
|
364
|
+
`)){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 Ci(e){let t=e?.limit??Xv,n=e?.filter,o,r=process.platform;if(r==="linux")o=aS();else if(r==="darwin")o=lS();else if(r==="win32")o=cS();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=oS(o.entries.filter(l=>nS(l.name,n))),i=s.length,a=i>t;return{entries:s.slice(0,t),truncated:a,totalMatched:i}}function uS(e,t=Zv){let n=[];for(let o=0;o<e.length;o+=t)n.push(e.slice(o,o+t));return n}function Yl(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(`
|
|
365
|
+
`))}function Ql(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=uS(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(`
|
|
366
|
+
`))}function Hh(){let e=process.platform,t=e==="linux"?"linux":e==="darwin"?"darwin":"win32",n=["Watch hints",""];n.push("Package log (pkg watches):"),n.push(` ${Uh[t]??Uh.linux}`),n.push("","Filesystem roots that exist on this host:");let o=[];for(let r of eS){let s=tS(r);try{Dh.existsSync(s)&&Dh.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(`
|
|
367
|
+
`))}function H(e){return{replies:[e]}}function pS(...e){return{replies:e}}function mS(e){let t=e.trim();if(!t)return{};let n=t.split(/\s+/);if(n.length>=2){let s=Br(n[0]);return s.ok?{ruleName:s.name,filter:n.slice(1).join(" ")}:{filter:t}}let o=n[0],r=Br(o);if(r.ok){let s=Ci({filter:o,limit:1});if(s.totalMatched===0&&!s.error)return{ruleName:r.name}}return{filter:o}}function hS(){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(`
|
|
368
|
+
`))}function Bh(){return{excludePaths:[],excludeGlobs:[]}}function fS(e){return e.notify==="self"?`notify=self (${e.ownerPeerKey})`:`notify=${e.notify}`}function gS(e,t){ea(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 jh(e,t){let n=e.trim();if(!n||/^help$/i.test(n))return H(hS());let o=n.split(/\s+/)[0].toLowerCase();if(Ur(),/^hints$/i.test(o))return H(Hh());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=Ci({filter:h});return f.error?H(p(f.error)):f.entries.length===0?H(Yl(f,h)):pS(Yl(f,h),Ql(f.entries))}if(m==="templates"){let h=u.slice(9).trim(),{ruleName:f,filter:g}=mS(h),y=Ci({filter:g});return y.error?H(p(y.error)):H(Ql(y.entries,f))}return H(p("svc subcommands: list [filter] | templates [ruleName] [filter]"))}if(/^status$/i.test(o)){let u=S(),{summary:d}=Uo(),m=[`watchEnabled: ${u.watchEnabled} watchAutoRestore: ${u.watchAutoRestore}`,`debounce: ${u.watchDebounceMs}ms max/min: ${u.watchMaxEventsPerMinute}`,`rules file: ${Xi()} (${d.total} saved, ${d.active} active, ${d.paused} paused, ${d.disabled} disabled)`,`events db: ${Er}`];d.total>0&&!u.watchEnabled?m.push("Rules are on disk; adapters stopped \u2014 /watch on to start."):d.total>0&&u.watchEnabled&&!u.watchAutoRestore?m.push("watchAutoRestore is off \u2014 /watch reload to start adapters without restarting gateway."):d.paused>0&&m.push(`${d.paused} paused rule(s) stay stopped until /watch resume <name>.`);let h=mu();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)"),H(p(m.join(`
|
|
369
|
+
`)))}if(/^reload$/i.test(o)){let{summary:u}=Uo();return S().watchEnabled?(hu(),H(p(`Reloading from ${Xi()}: ${u.total} rule(s) on disk, ${u.active} eligible to run. /watch status for adapters.`))):H(p(`Rules on disk: ${u.total} (${u.active} active). watchEnabled is false \u2014 /watch on first.`))}if(/^list$/i.test(o)){let u=Ue();if(u.length===0)return H(p("(no watch rules on this device)"));let d=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} ${fS(m)} when=${m.notifyWhen} \u2014 ${f}`});return H(p(d.join(`
|
|
370
|
+
`)))}let r=n.match(/^recent(?:\s+(\d+))?\s*$/i);if(r){let u=r[1]?Number(r[1]):15,d=Ue(),m=new Set(d.map(g=>g.id)),h=Gc(u,m);if(h.length===0)return H(p("(no recent watch events)"));let f=h.map(g=>`${new Date(g.tsMs).toLocaleString()} ${g.ruleName} ${g.summary}`);return H(p(f.join(`
|
|
371
|
+
`)))}let s=n.match(/^show\s+(\S+)\s*$/i);if(s){let u=vn(Ue(),s[1]);if(!u)return H(p(`Unknown rule "${s[1]}". /watch list`));let d=[`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"&&(d.push(`path: ${u.path}`),d.push(`events: ${u.events.join(",")}`),d.push(`excludePaths: ${u.excludePaths.length?u.excludePaths.join(", "):"(none)"}`),d.push(`excludeGlobs: ${u.excludeGlobs.length?u.excludeGlobs.join(", "):"(none)"}`)),u.kind==="svc"&&d.push(`units: ${u.units.join(", ")||"(none)"}`),H(p(d.join(`
|
|
372
|
+
`)))}if(/^on$/i.test(o))return N({watchEnabled:!0}),Ke(),H(p("watchEnabled: true (gateway picks up watchers when running)."));if(/^off$/i.test(o))return N({watchEnabled:!1}),Ke(),H(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],d=i[2].toLowerCase(),m=(i[3]??"").trim(),h=Ue(),f=vn(h,u);if(!f)return H(p(`Unknown rule "${u}".`));if(f.kind!=="fs")return H(p("exclude applies to filesystem rules only."));if(d==="list"){let g=[`excludePaths: ${f.excludePaths.length?f.excludePaths.join(`
|
|
327
373
|
`):"(none)"}`,`excludeGlobs: ${f.excludeGlobs.length?f.excludeGlobs.join(`
|
|
328
|
-
`):"(none)"}`];return
|
|
329
|
-
`)))}if(
|
|
330
|
-
`))}async function
|
|
374
|
+
`):"(none)"}`];return H(p(g.join(`
|
|
375
|
+
`)))}if(d==="add"){if(!m)return H(p("Usage: /watch exclude <name> add <pattern>"));let g=Kl(m,f.path);if("error"in g)return H(p(g.error));if(g.path){if(st(g.path))return H(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),St(Yn(h,f)),Ke(),H(p(`Added exclude to "${f.name}".`))}if(d==="rm"||d==="remove"){if(!m)return H(p("Usage: /watch exclude <name> rm <pattern>"));let g=Kl(m,f.path);return"error"in g?H(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)),St(Yn(h,f)),Ke(),H(p(`Removed exclude from "${f.name}".`)))}return H(p("exclude subcommands: list | add | rm"))}if(/^add$/i.test(o)){let u=n.slice(3).trim(),d=u.split(/\s+/);if(d.length<2)return H(p("Usage: /watch add fs|pkg|svc <name> \u2026"));let m=d[0].toLowerCase(),h=m==="fs"||m==="pkg"||m==="svc"?m:null;if(!h)return H(p("Kind must be fs, pkg, or svc."));let f=d[1],g=Br(f);if(!g.ok)return H(p(g.error));let y=Ue();if(y.length>=Vi)return H(p(`Max ${Vi} watch rules on this device.`));if(vn(y,g.name))return H(p(`Rule "${g.name}" already exists.`));let b;if(h==="fs"){let $=u.slice(m.length+1+f.length).trim(),L=Wh($);if(!L.ok)return H(p(L.error));let{rootPath:x,events:O,excludePaths:E,excludeGlobs:K}=L.value;if(st(x))return H(p("That path is blocked (sensitive). Choose another directory."));if(!dS.existsSync(x))return H(p(`Path not found: ${x}`));for(let te of E)if(st(te))return H(p(`Excluded path blocked: ${te}`));b={id:jr(),name:g.name,ownerPeerKey:t,kind:"fs",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:x,events:O,units:[],excludePaths:E,excludeGlobs:K,adapterStatus:"",createdAtMs:Date.now()}}else if(h==="pkg")b={id:jr(),name:g.name,ownerPeerKey:t,kind:"pkg",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:"",events:[],units:[],...Bh(),adapterStatus:"",createdAtMs:Date.now()};else{let $=d.slice(2);if($.length===0)return H(p("Usage: /watch add svc <name> <unit\u2026>"));b={id:jr(),name:g.name,ownerPeerKey:t,kind:"svc",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:"",events:[],units:$,...Bh(),adapterStatus:"",createdAtMs:Date.now()}}St(Yn(y,b)),Ke();let T=S().watchEnabled?"":" Rule saved to disk. /watch on to start adapters.";return H(p(`Watch rule "${b.name}" added (${b.kind}).${T}`))}let a=n.match(/^set\s+(\S+)\s+(\S+)\s+(\S+)\s*$/i);if(a){let[,u,d,m]=a,h=Ue(),f=vn(h,u);if(!f)return H(p(`Unknown rule "${u}".`));let g=d.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 H(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 H(p("when must be always or state-change."));f.notifyWhen=b}else return H(p("set supports: notify, when"));return St(Yn(h,f)),Ke(),H(p(`Updated ${f.name} ${g}=${y}.`))}let l=n.match(/^(?:rm|remove)\s+(\S+)\s*$/i);if(l){let u=l[1],d=Ue(),m=vn(d,u);return m?(ea(m.id),St(Qc(d,u)),Ke(),H(p(`Removed watch rule "${u}".`))):H(p(`Unknown rule "${u}".`))}let c=n.match(/^(pause|stop|resume|enable|disable)\s+(\S+)\s*$/i);if(c){let u=c[1].toLowerCase(),d=c[2],m=Ue(),h=vn(m,d);if(!h)return H(p(`Unknown rule "${d}".`));gS(h,u==="stop"?"pause":u),St(Yn(m,h)),Ke();let g=u==="stop"?"paused (stop)":`${u}d`;return H(p(`${h.name}: ${g}.`))}return H(p("Unknown /watch command. Try /watch help"))}ue();xn();Cn();function Gh(){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(`
|
|
376
|
+
`))}async function Vl(e,t,n){let o=Tm(e);if(o.kind==="help")return Gh();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()||mt(a.tunnelRelayUrl||Ee),c=await li(l,{...r?{email:r}:{},...s?{phone:s}:{},password:i});return c.ok?(Ct({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()||mt(a.tunnelRelayUrl||Ee),c=await ci(l,{...o.email?{email:o.email}:{},...o.phone?{phone:o.phone}:{},password:i});return c.ok?(Ct({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?(Ct({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 Zr(),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()||mt(r.tunnelRelayUrl||Ee),i=Rt(),a=!!process.env.OMNISH_TUNNEL_TOKEN?.trim(),c=!!Nt()?.token?.trim(),u=await ri(s,i),d=[`Relay: ${s}`,`Token: ${i?"configured":"missing"}${a?" (OMNISH_TUNNEL_TOKEN)":c?" (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&&d.push(`Detail: ${u.error}`),p(d.join(`
|
|
331
377
|
`))}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
378
|
${s.publicUrl||"(pending)"}
|
|
333
379
|
${s.localHost}:${s.localPort}`).join(`
|
|
@@ -335,73 +381,71 @@ Notify on completion: on`:"";return{kind:"text",body:p(`Pull job ${c} started ($
|
|
|
335
381
|
`))}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
382
|
public: ${r.publicUrl}
|
|
337
383
|
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(`
|
|
384
|
+
id: ${r.id}`)}return Gh()}var Jh={version:1,generatedAt:"2026-05-23T21:08:15.442Z",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`, `/dlf`, `/dlv`, `/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 ( ). Background jobs deliver results over the gateway control channel when is active. 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 --------- -------- Auto: file (HTTP), video (yt-dlp), or HTML\u2192markdown Force HTTP file download (never yt-dlp) Force yt-dlp video download 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: / , / , / , / , . How classifies URLs File signals (always HTTP, never yt-dlp): pathname extension ( , , \u2026), path heuristics ( for arxiv, etc.), or from HEAD. Video: yt-dlp probe ( ) \u2014 if yt-dlp has a named site extractor (1000+ sites), download with yt-dlp. HTML page: fetch page and send markdown in chat (large pages may also save a file). Use when auto-detect might guess wrong (e.g. force HTTP on a video URL). Use to force yt-dlp. Save location Config / chat Where files go --------------- ---------------- set That directory (flat) for this chat Session cwd ( ) Default (flat) No dated subfolders. Background jobs , , , , and run as background jobs ( ). You get a job id immediately; finished files and markdown are sent to chat when the job completes (if is true). Optional flags (stripped before the payload): / \u2014 extra ping when the shell job finishes (exit status) / \u2014 no-op (already background) Step progress When is true (default), multi-step work sends chat messages such as while it runs. Auto-detect URLs When is true (default), a message that is only an or URL runs in the background \u2014 even when free shell ( ) is on. Focused PTY still wins over auto-dl. Legacy still works as aliases: / \u2192 , / \u2192 . Other modes show a deprecation hint."},{title:"Config keys",body:"Key Default Purpose ----- --------- --------- Send files to chat after download Allow from chat Lone URL \u2192 auto Output root (empty \u2192 Downloads/Omnish or cwd) 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","/dlf","/dlv","/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","/dlf","/dlv","/tr","/edit","/bin","/venvs","/whisper","/config set","/dl install","/dl doctor","/dl setup"]},{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 yn=Jh;function Ri(e,t){return`${e.replace(/\/$/,"")}/${t}`}function wS(e){return e.toLowerCase().replace(/[^a-z0-9\s/-]/g," ").split(/\s+/).filter(t=>t.length>1)}function bS(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 u of e.sections){let d=u.title.toLowerCase(),m=u.body.toLowerCase();d.includes(c)&&(i+=5,a??=u.title),m.includes(c)&&(i+=2,a??=u.title)}}return i<=0?null:{entry:e,score:i,matchedSection:a}}function Ti(e,t=12){let n=e.trim();if(!n)return[];let o=wS(n),r=[];for(let s of yn.entries){let i=bS(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 $i(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 kr(e){return yn.entries.find(t=>t.id===e)}function Pi(e){let t=e.replace(/\\/g,"/").replace(/^\.\//,"");return yn.entries.find(n=>n.path===t||n.id===t)}function Mi(e,t=1800){let n=[];e.summary&&n.push(e.summary);for(let r of e.sections){if(n.join(`
|
|
339
385
|
|
|
340
386
|
`).length>=t)break;n.push(`${r.title}
|
|
341
387
|
${r.body.slice(0,600)}`)}let o=n.join(`
|
|
342
388
|
|
|
343
|
-
`).trim();return o.length>t&&(o=`${o.slice(0,t-1)}\u2026`),o}function
|
|
344
|
-
`))}function
|
|
389
|
+
`).trim();return o.length>t&&(o=`${o.slice(0,t-1)}\u2026`),o}function Ei(e){return e.relatedCommands.find(n=>/\bhelp\b/i.test(n))??e.relatedCommands[0]}function qh(){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(`
|
|
390
|
+
`))}function Xl(e,t){if(e.length===0)return p(`${t}
|
|
345
391
|
(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
|
|
347
|
-
`);return p(a)}var
|
|
348
|
-
`))}async function
|
|
349
|
-
${
|
|
392
|
+
`))}function Zl(e,t){let n=Ri(yn.repoUrl,e.path),o=Mi(e),r=e.relatedCommands.slice(0,8),s=Ei(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(`
|
|
393
|
+
`);return p(a)}var ec=new Map;function zh(e,t){ec.set(e,t)}function Kh(e){return ec.get(e)}function tc(e,t){let n=Number.parseInt(t,10);if(!Number.isFinite(n)||n<1)return null;let o=ec.get(e);return!o||n>o.length?null:o[n-1]}var Ai;function Yh(e){Ai=e}function Qh(e){let t=Number.parseInt(e,10);return!Number.isFinite(t)||t<1||!Ai||t>Ai.length?null:Ai[t-1]}async function nc(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return{kind:"text",body:qh()};if(/^list\s*$/i.test(o)){let l=Kh(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=Ti(l).map($i);return zh(t,u),{kind:"text",body:Xl(u,`Search: ${l}`)}}let s=/^follow\s+(\d+)\s*$/i.exec(o);if(s){let l=tc(t,s[1]);if(!l)return{kind:"text",body:p(`No result #${s[1]}. Run /docs search <topic> first.`)};let c=kr(l.id);if(!c)return{kind:"text",body:p("Entry missing from index.")};let u=Ei(c);if(!u)return{kind:"text",body:p(`No related command for "${l.title}". Try /docs ${s[1]} for the doc excerpt.`)};let d=u.startsWith("/")?u:`/${u}`,m=await n(d);return m?.kind==="text",m}let i=/^(?:show\s+)?(\d+)\s*$/i.exec(o);if(i){let l=tc(t,i[1]);if(!l)return{kind:"text",body:p(`No result #${i[1]}. Run /docs search <topic> first.`)};let c=kr(l.id);return c?{kind:"text",body:Zl(c,Number.parseInt(i[1],10))}:{kind:"text",body:p("Entry missing from index.")}}let a=Pi(o);return a?{kind:"text",body:Zl(a)}:{kind:"text",body:p("Unknown /docs command. /docs help")}}function oc(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 kS(e,t){return`${e}:${t}`}function vS(e,t){return`${e}:apps:${t}`}async function SS(e,t){let n=e.trim();if(n===""||/^status$/i.test(n)||/^show$/i.test(n)||/^get$/i.test(n)){let a=S();return Iu({gatewayMode:a.gatewayMode,authPresent:pt(),tokenSet:!!Pe(a),allowN:a.allowFrom.length,tgAllowN:a.telegramAllowFrom.length,updateBrief:di(ur())})}if(/^help$/i.test(n))return Q(pa());let o=Gn(n);if(!o)return Ku();_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 Wu(r.gatewayMode,s,i)}function xS(e){let t=e.trim();if(!vt(t))return _u();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.'),Fu(o)}async function Vh(e,t,n){let o=ie(t),r=ku(n);if(r!==null){let a=vu(o.cwd,r),l=Su(a);return l.ok?(qr(t,a),p(`cwd: ${a}`)):p(`cd: ${l.error}`)}let s=await Mn(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(`
|
|
394
|
+
`))}async function wn(e,t,n,o,r,s,i,a,l=null,c=!1,u){let d=s.text.trim(),m=s.peerKey,h=d.match(/^!!\s*(start|stop)\s*$/i);if(h)return h[1].toLowerCase()==="start"?(r.set(m,!0),F(Ju())):(r.set(m,!1),F(p("Free shell mode off.")));if(d.startsWith(e.commandPrefix)){let k=d.slice(e.commandPrefix.length).trim();if(!k)return F(p(`Send ${e.commandPrefix}<command> or /help`));if(!c&&Km(k)){let T=Wl(m,k);if(T!==void 0)return await wn(e,t,n,o,r,{...s,text:T},i,a,l,!0,u)}return F(await Vh(e,m,k))}if(/^\/help\s+files$/i.test(d.trim()))return F(fa());let f=d.match(/^\/help\s+search\b(?:\s+([\s\S]*))?$/i);if(f){let k=(f[1]??"").trim();return await nc(k?`search ${k}`:"help",m,$=>wn(e,t,n,o,r,{...s,text:$},i,a,l,c,u))}let g=d.match(/^\/docs\b(?:\s+([\s\S]*))?$/i);if(g)return await nc(g[1]??"help",m,T=>wn(e,t,n,o,r,{...s,text:T},i,a,l,c,u));if(d==="/help"||d==="help")return F(Q(Vn(e)));if(d.startsWith("/")){if(/^\/files(?:\s+help)?$/i.test(d.trim()))return F(fa());let k=d.match(/^\/receive\b(?:\s+(\S+))?$/i);if(k){let R=(k[1]??"status").toLowerCase(),I=new Set(["here","cwd","session","dir"]),Z=new Set(["default","global","reset"]);if(I.has(R)){ra(m,"sessionCwd");let be=ie(m).cwd;return F(p(`Inbound files will save under this chat\u2019s session folder:
|
|
395
|
+
${be}
|
|
350
396
|
(layout: \u2026/<peer>/<date>/<file> under that root).
|
|
351
397
|
|
|
352
|
-
Change folder with ${e.commandPrefix}cd \u2026 Send /receive default to use the server config again.`))}return
|
|
353
|
-
Notify on completion: on`:"";return F(p(`Job ${
|
|
354
|
-
[cwd: ${
|
|
355
|
-
/log ${
|
|
356
|
-
/tail ${
|
|
357
|
-
${
|
|
358
|
-
|
|
359
|
-
`)))}let
|
|
360
|
-
(Gateway-shared recipe exists: /run show --global ${L})`:"",
|
|
361
|
-
(This chat stores an override: /run show --chat ${L})`:"",
|
|
362
|
-
/run remove --global ${
|
|
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=
|
|
364
|
-
`).trim()
|
|
365
|
-
(Shared shortcut exists: /shortcut show --global ${g})`:"",
|
|
366
|
-
(This chat overrides the name: /shortcut show --chat ${g})`:""
|
|
367
|
-
/shortcut remove --global ${f}`):
|
|
368
|
-
`,{mode:384})}catch(
|
|
369
|
-
${
|
|
370
|
-
|
|
371
|
-
${a.filter(u=>u.severity==="error").map(u=>{let
|
|
398
|
+
Change folder with ${e.commandPrefix}cd \u2026 Send /receive default to use the server config again.`))}return Z.has(R)?(ra(m,"default"),F(p("Per-chat inbound folder cleared. Uploads now follow fileReceiveRootMode in config.json (/files)."))):F(R==="help"?ga():R==="status"?Bu(e,m):ga())}if(d==="/reload"||d==="/restart"){if(!a?.reload)return F(p("Reload is only available while the omnish gateway (omnish run) is running."));let R=await a.reload();return F(p(R.ok?R.summary:`Reload failed: ${R.error}`))}if(d==="/updates"||d.startsWith("/updates ")){let R=d.slice(8).trim().toLowerCase();if(R==="cached"||R==="last"){let Z=ur();return F(Z?jo(Z):p("No update snapshot yet. Send /updates (live check) once, or enable updateCheckEnabled and wait for the scheduled check."))}let I=await dr(lt(),S());return F(jo(I))}let T=d.match(/^\/security(?:\s+(\S+))?\s*$/i);if(T){let R=(T[1]??"").toLowerCase(),I=S(),Z=_t(I);return F(R==="help"||R==="?"?Q(id()):R==="summary"||R==="brief"?p(Pu(Z,"Send /security for the full report.")):R==="tips"?Q(sd()):R===""||R==="full"||R==="report"?rd(Z):p("Unknown /security subcommand. Try /security, /security summary, /security tips, or /security help"))}let $=d.match(/^\/(gateway|gw|mode)\b(?:\s+(.*))?$/i);if($){let R=($[2]??"").trim();return F(await SS(R,a))}if(d==="/config"||d.startsWith("/config ")){let R=d.slice(7).trim();return F(await dd(R,a))}let L=th(d);if(L!==null)return F(await nh(e,L));let x=rh(d);if(x!==null)return await ch(e,x,m,t,u);let O=sh(d);if(O!==null)return await uh(e,O,m,t,u);let E=oh(d);if(E!==null)return await _n(e,E,m,t,u);let K=ih(d);if(K!==null)return await Hl(e,K,m,t,u);let te=ah(d);if(te!==null)return await dh(e,te,m,t,u);let ce=lh(d);if(ce!==null)return await ph(e,ce,m,t,u);let Se=oc(d);if(Se!==null){let R=Td(e,Se,l);return R===null?null:F(R)}let _=d.match(/^\/(send|file)\b(?:\s+([\s\S]+))?$/i);if(_){let R=(_[2]??"").trim();if(!R)return F(ha());let I=mi(R);if(!I)return F(ha());let Z=ie(m).cwd,be=await pr(Z,I.selectorPart);if(be.length===0)return F(p(`No files matched: ${I.selectorPart}`));let ze=await mr(be);if(!ze.ok)return F(p(ze.error));let Un=oe()?Ut(e):e.fileSendMaxBytes,Et=[];for(let Qf of be){let Hn=ft(Qf,Un);if("error"in Hn)return F(p(Hn.error));Et.push({absPath:Hn.absPath,category:Hn.category,mimetype:Hn.mimetype,displayName:Hn.displayName,caption:I.caption})}return Et.length===1?{kind:"file",spec:Et[0]}:{kind:"files",specs:Et}}if(d==="/allowlist"){let R=S();return F(Nu([{label:"allowFrom (WhatsApp)",items:R.allowFrom},{label:"telegramAllowFrom",items:R.telegramAllowFrom}]))}let fe=d.match(/^\/allow\b\s+(.+)$/i);if(fe)try{let R=Nr(fe[1].trim());return F(ma(R))}catch(R){return F(p(String(R)))}if(/^\/allow\b\s*$/i.test(d))return F(Du());let V=d.match(/^\/deny\b\s+(.+)$/i);if(V)try{let R=Fr(V[1].trim());return F(ma(R))}catch(R){return F(p(String(R)))}if(/^\/deny\b\s*$/i.test(d))return F(Uu());let M=d.match(/^\/(whatsapp|wa|telegram|tg)\b(?:\s+(.*))?$/i);if(M){let R=M[1].toLowerCase(),I=(M[2]??"").trim(),Z=R==="whatsapp"||R==="wa";if(R==="telegram"||R==="tg"){let ze=I.match(/^token\s+(\S+)\s*$/i);return ze?F(xS(ze[1]??"")):I===""||/^help$/i.test(I)?F(Q(Ou(S()))):F(Gu())}if(Z)return I===""||/^help$/i.test(I)?F(Q(Lu(e))):F(ju())}let j=d.match(/^\/(cowork|cw)\b(?:\s+([\s\S]*))?$/i);if(j){let R=(j[2]??"").trim();return F(await Fh(R,m,e))}let Y=d.match(/^\/watch\b(?:\s+([\s\S]*))?$/i);if(Y){let R=(Y[1]??"").trim(),I=jh(R,m);return I.replies.length===1?F(I.replies[0]):{kind:"texts",bodies:I.replies}}if(d==="/apps"||d.startsWith("/apps "))return F(await MS(d,m,e,i,o));let J=CS(d);if(J!==null)return F(await $S(J,m,e,i,s.mediaSavedPath,u));if(d.startsWith("/bg")){let R=d.slice(3).trim();if(!R)return F(Hu());let I=qp(R);if("error"in I)return F(p(I.error==="empty"?"Usage: /bg <command> or /bg -n <name> <command>.":I.error));let Z=ie(m).cwd,{id:be,meta:ze}=t.spawnJob(e.shell,I.cmd,{cwd:Z,name:I.name,notifyPeerKey:I.notify?m:null}),Un=ze.name?`${be} (${ze.name})`:be,Et=I.notify?`
|
|
399
|
+
Notify on completion: on`:"";return F(p(`Job ${Un} started.
|
|
400
|
+
[cwd: ${Z}]
|
|
401
|
+
/log ${ze.name??be}
|
|
402
|
+
/tail ${ze.name??be}${Et}`))}if(d==="/tunnels")return F(await Vl("list",e,Nn()));let ae=d.match(/^\/tunnel\b(?:\s+([\s\S]*))?$/i);if(ae){let R=(ae[1]??"").trim();return F(await Vl(R||"help",e,Nn()))}if(d==="/jobs"){let R=t.list().slice(0,20);return R.length===0?F(p("(no jobs yet)")):F(p(R.map(I=>{let Z=I.finishedAt&&I.startedAt?`${((Date.parse(I.finishedAt)-Date.parse(I.startedAt))/1e3).toFixed(1)}s`:"\u2026";return`${I.name?`${I.id} ${I.name}`:I.id} ${I.status} exit=${I.exitCode??"?"} ${Z}
|
|
403
|
+
${I.cmd.slice(0,120)}${I.cmd.length>120?"\u2026":""}`}).join(`
|
|
404
|
+
|
|
405
|
+
`)))}let q=d.match(/^\/log\s+(\S+)(?:\s+(\d+))?\s*$/i);if(q){let R=q[1],I=t.resolveJobRef(R);if(!I.ok)return F(p(I.error));let Z=I.id,be=q[2]?Number.parseInt(q[2],10):e.jobLogTailLines,ze=Number.isFinite(be)&&be>0?Math.min(be,500):e.jobLogTailLines;return F(p(t.tailLog(Z,ze)))}let Be=d.match(/^\/tail\s+(\S+)\s*$/i);if(Be){let R=Be[1],I=t.resolveJobRef(R);if(!I.ok)return F(p(I.error));let Z=I.id,be=kS(m,Z),ze=n.get(be)??0,{text:Un,nextOffset:Et}=t.readSince(Z,ze);return n.set(be,Et),F(Un?p(Un.trimEnd()||"(no new output)"):p(`(no new output; offset ${Et})`))}let $e=d.match(/^\/kill\s+(\S+)\s*$/i);if($e){let R=$e[1],I=t.resolveJobRef(R);return I.ok?F(p(t.kill(I.id))):F(p(I.error))}let Le=d.match(/^\/(shortcut|shortcuts|alias|aliases)\b(?:\s+([\s\S]*))?$/i);if(Le){let R=Le[1].toLowerCase(),I=(Le[2]??"").trim();return R==="shortcuts"&&!I?I="list":((R==="alias"||R==="aliases")&&!I||R==="shortcut"&&!I)&&(I="help"),F(await PS(I,m,e))}if(!c){let R=d.trim().match(/^\/([a-zA-Z0-9][a-zA-Z0-9_-]{0,31})\s*$/);if(R){let I=R[1],Z=Wl(m,I);if(Z!==void 0)return await wn(e,t,n,o,r,{...s,text:Z},i,a,l,!0,u)}}return F(qu(e))}let y=d.match(/^>(\S+)\s*(.*)$/s);if(y){let k=y[1],T=y[2]??"",$=await i.writeNamedLine(m,k,T);return $?F(p($)):null}if(await i.writeFocusedLine(m,d))return null;let b=await mh(e,d,m,t,u);return b!==null?b:r.get(m)&&d?F(await Vh(e,m,d)):e.chatLlmFallbackEnabled&&e.chatLlmShellCommand.trim().length>0&&u?.onPlainTextLlmFallback?(u.onPlainTextLlmFallback(m,d),null):F(zu(e,d))}function CS(e){return e==="/run"||e.startsWith("/run ")?e.slice(4).trim():e==="/r"||e.startsWith("/r ")?e.slice(2).trim():null}function RS(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 TS(e){let n=e.trim().match(/^(\S+)\s+([\s\S]+)$/);if(!n)return null;let o=RS(n[2]);return o?{recipe:n[1],task:o.task,queued:o.queued,attach:o.attach}:null}async function $S(e,t,n,o,r,s){let i=e.trim();if(!i||/^help$/i.test(i))return Q(Zu());let a=/^online\b([\s\S]*)$/i.exec(i);if(a)return Mh((a[1]??"").trim(),t,n);let l=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(i);if(l){let b=Eh(t,n,l[1],l[2]??"");if(!b.ok)return p(b.error);let k=await Ao(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:T}=bp(b);if(T)return p(`Unknown /run list suffix: "${T}". Use: list | list --chat | list -p | list --global | list -g`);if(k==="merged")return ed(xp(t,n));let $=vp(t,n,k);return od($,k)}let u=/^show\b([\s\S]*)$/i.exec(i);if(u){let b=(u[1]??"").trim(),{mode:k,remainder:T}=wp(b),$=/^(\S+)\s*$/i.exec(T);if(!$?.[1])return p("Usage: /run show <name> \u2014 or show --global|-g|--chat|-p <name> (-g shared, -p private)");let L=$[1];if(k==="resolved"){let ce=Xe(t,n,L);return ce?ba(ce):ka(L)}let x=k==="global"?"global":"chat",O=jt(x,t,n,L);if(O)return ba(O,x==="global"?"From gateway-shared recipes (--global).":"From this chat only (--chat).");let E=x==="chat"&&jt("global",t,n,L)?`
|
|
406
|
+
(Gateway-shared recipe exists: /run show --global ${L})`:"",K=x==="global"&&jt("chat",t,n,L)?`
|
|
407
|
+
(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}${E}${K}`)}let d=/^add\b([\s\S]*)$/i.exec(i);if(d){let{scope:b,remainder:k}=rl((d[1]??"").trim()),T=k.match(/^(\S+)\s+([\s\S]+)$/);if(!T)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 $=sl(T[2],n.recipesMacroDefaultCommand);fo(t,T[1],$,b);let L=$t(T[1]),x=L.ok?L.normalized:T[1].toLowerCase(),O=jt(b,t,n,x)??Xe(t,n,x);return O?Jo(x,O,b):p("Recipe save failed.")}catch($){return p(String($))}}let m=/^set\b([\s\S]*)$/i.exec(i);if(m){let{scope:b,remainder:k,explicit:T}=ol((m[1]??"").trim()),$=kp(k);if($){if(T)return p("Cannot combine a leading scope flag (-g, --global, --chat, -p) with a trailing scope flag on the same line.");let O=$t($.name);if(!O.ok)return p(O.error);let E=il(t,O.normalized,$.target,n);if(!E.ok)return p(E.error);if(E.kind==="noop")return p(E.message);let K=jt(E.target,t,n,O.normalized)??Xe(t,n,O.normalized);return K?Jo(O.normalized,K,E.target):p("Recipe scope update failed.")}let L=k.match(/^(\S+)\s*$/);if(L?.[1]&&T){let O=$t(L[1]);if(!O.ok)return p(O.error);let E=il(t,O.normalized,b,n);if(!E.ok)return p(E.error);if(E.kind==="noop")return p(E.message);let K=jt(E.target,t,n,O.normalized)??Xe(t,n,O.normalized);return K?Jo(O.normalized,K,E.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=sl(x[2],n.recipesMacroDefaultCommand);fo(t,x[1],O,b);let E=$t(x[1]),K=E.ok?E.normalized:x[1].toLowerCase(),te=jt(b,t,n,K)??Xe(t,n,K);return te?Jo(K,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}=rl((h[1]??"").trim()),T=k.match(/^(\S+)\s*$/);if(!T?.[1])return p(b==="global"?"Usage: /run remove --global <name> (aliases: rm, del)":"Usage: /run remove [--global|-g|--chat|-p] <name>");let $=T[1];return Sp(t,$,b)?td($,b):b==="chat"&&jt("global",t,n,$)?p(`No recipe "${$}" in this chat. There is a gateway-shared recipe with that name \u2014 remove it with:
|
|
408
|
+
/run remove --global ${$}`):nd($,b)}let f=/^queue\s+load\b([\s\S]*)$/i.exec(i);if(f){let b=(f[1]??"").trim(),k=Vm(n),T=null,$=/^json(?:\s+([\s\S]+))?$/i.exec(b);if($){let E=($[1]??"").trim();if(!E)return p('Usage: /run queue load json [{"recipe":"\u2026","task":"\u2026"}, \u2026] \u2014 or { "tasks": [ \u2026 ] }');T=E}else if(b.length>0){let E=b;(E.startsWith('"')&&E.endsWith('"')||E.startsWith("'")&&E.endsWith("'"))&&(E=E.slice(1,-1));let K=ie(t).cwd,te=await pr(K,E);if(te.length===0)return p(`No files matched: ${E}`);if(te.length>1)return p("Queue load: specify a single JSON file.");let ce=await mr(te);if(!ce.ok)return p(ce.error);let Se=Ul(te[0],k);if(!Se.ok)return p(Se.error);T=Se.text}else if(r){let E=Ul(r,k);if(!E.ok)return p(E.error);T=E.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=Xm(T);if(!L.ok)return p(L.error);let x=Zm(t,n,L.jobs);if(!x.ok)return p(x.error);let O=[];for(let E of x.items)O.push(o.enqueueQueuedRun(t,E,n));return p(O.join(`
|
|
409
|
+
`))}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=TS(i);if(g){let{recipe:b,task:k,queued:T,attach:$}=g,L=ll($,n),x=Xe(t,n,b);if(!x)return ka(b);if(x.steps&&x.steps.length>0){let Se=ie(t).cwd,_=x.steps.length,fe=x.steps;if(s?.sendToPeer){let V=s.sendToPeer;(async()=>{let M=[],j=!1;for(let J=0;J<fe.length;J++){let ae=fe[J];if(j){M.push({index:J,label:ae.label??`step ${J+1}`,cmd:ae.cmd,exitCode:null,timedOut:!1,skipped:!0,output:""});continue}n.progressUpdates&&await V(t,`Step ${J+1}/${_}: ${ae.label??`step ${J+1}`}\u2026`).catch(()=>{});let q=await Mn(n.shell,ae.cmd,{timeoutMs:n.syncTimeoutMs,maxBytes:n.syncMaxBytes,cwd:Se}),Be=[q.stdout,q.stderr].filter(Boolean).join(`
|
|
410
|
+
`).trim(),$e=q.code===0&&!q.timedOut,Le={index:J,label:ae.label??`step ${J+1}`,cmd:ae.cmd,exitCode:q.code,timedOut:q.timedOut,skipped:!1,output:Be};M.push(Le),n.progressUpdates&&await V(t,Cp(b,Le,J,_)).catch(()=>{}),!$e&&!ae.continueOnFail&&(j=!0)}let Y=Rp(b,M);await V(t,Y)})().catch(()=>{})}return p(`Runbook "${b}" started (${_} steps). Results will be sent when complete.`)}let O=x.taskEnv??"OMNISH_TASK";if(!ho(x.command,O))return p(`Recipe "${b}" command must reference "$${O}".`);let E=xs(k,n.recipesMaxTaskChars);if(!E.ok)return p(E.error);let K=x.promptTemplate?Cs(x.promptTemplate,O,E.task):E.task,te={[O]:K};if(T){let Se={command:x.command,extraEnv:te,recipeLabel:b,startOptions:L};return p(o.enqueueQueuedRun(t,Se,n))}let ce=Rs(b);return p(o.start(t,ce,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>"):Xe(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 PS(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return Q(ya());let r=/^online\b([\s\S]*)$/i.exec(o);if(r)return Eo((r[1]??"").trim(),t,n,Ph);let s=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(o);if(s){let d=Ah(t,s[1],s[2]??"");if(!d.ok)return p(d.error);let m=await Ao(d.body);return p(m.ok?m.message:m.error)}let i=/^list\b([\s\S]*)$/i.exec(o);if(i){let d=(i[1]??"").trim(),{filter:m,bad:h}=Gm(d);return h?p(`Unknown /shortcut list suffix: "${h}". Use: list | list --chat | list -p | list --global | list -g`):Yu(qm(t,m))}let a=/^show\b([\s\S]*)$/i.exec(o);if(a){let d=(a[1]??"").trim(),{mode:m,remainder:h}=jm(d),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=gi(t,g);if(!L)return Vu(g);let x=L.scope==="global"?"Shared shortcut (all chats use it unless this chat overrides the name).":"This chat only.";return wa(g,L.body,x)}let y=m==="global"?"global":"chat",b=Jt(y,t,g);if(b!==void 0)return wa(g,b,y==="global"?"From the shared shortcut list (--global / -g).":"From this chat only (--chat / -p).");let k=y==="chat"&&Jt("global",t,g)!==void 0?`
|
|
411
|
+
(Shared shortcut exists: /shortcut show --global ${g})`:"",T=y==="global"&&Jt("chat",t,g)!==void 0?`
|
|
412
|
+
(This chat overrides the name: /shortcut show --chat ${g})`:"",$=y==="global"?`Unknown shortcut "${g}" in shared shortcuts.`:`Unknown shortcut "${g}" in this chat.`;return p(`${$}${k}${T}`)}let l=/^add\b([\s\S]*)$/i.exec(o);if(l){let{scope:d,remainder:m}=_l((l[1]??"").trim()),h=m.match(/^(\S+)\s+([\s\S]+)$/);if(!h)return p(d==="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{fr(t,h[1],h[2],d);let f=Mt(h[1]),g=f.ok?f.normalized:h[1].trim().toLowerCase(),y=Jt(d,t,g)??"";return Go(g,y,d)}catch(f){return p(String(f))}}let c=/^set\b([\s\S]*)$/i.exec(o);if(c){let{scope:d,remainder:m,explicit:h}=Fl((c[1]??"").trim()),f=Jm(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=Mt(f.name);if(!b.ok)return p(b.error);let k=Dl(t,b.normalized,f.target);if(!k.ok)return p(k.error);if(k.kind==="noop")return p(k.message);let T=Jt(k.target,t,b.normalized)??"";return Go(b.normalized,T,k.target)}let g=m.match(/^(\S+)\s*$/);if(g?.[1]&&h){let b=Mt(g[1]);if(!b.ok)return p(b.error);let k=Dl(t,b.normalized,d);if(!k.ok)return p(k.error);if(k.kind==="noop")return p(k.message);let T=Jt(k.target,t,b.normalized)??"";return Go(b.normalized,T,k.target)}let y=m.match(/^(\S+)\s+([\s\S]+)$/);if(!y)return p(d==="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{fr(t,y[1],y[2],d);let b=Mt(y[1]),k=b.ok?b.normalized:y[1].trim().toLowerCase(),T=Jt(d,t,k)??"";return Go(k,T,d)}catch(b){return p(String(b))}}let u=/^(?:remove|rm|del)\b([\s\S]*)$/i.exec(o);if(u){let{scope:d,remainder:m}=_l((u[1]??"").trim()),h=m.match(/^(\S+)\s*$/);if(!h?.[1])return p(d==="global"?"Usage: /shortcut remove --global <name> (aliases: rm, del)":"Usage: /shortcut remove [--global|-g|--chat|-p] <name>");let f=h[1];return zm(t,f,d)?Qu(f,d):d==="chat"&&Jt("global",t,f)!==void 0?p(`No shortcut "${f}" in this chat. There is a shared shortcut with that name \u2014 remove it with:
|
|
413
|
+
/shortcut remove --global ${f}`):Xu(f,d)}return Q(ya())}async function MS(e,t,n,o,r){let s=e.slice(5).trim(),i=s.toLowerCase();if(!i||i==="help")return Q(va());let a=s.match(/^(\S+)\s*(.*)$/s);if(!a)return Q(va());let l=a[1].toLowerCase(),c=(a[2]??"").trim();if(l==="online")return Eo(c,t,n,Th);let u=c.match(/^publish\b([\s\S]*)$/i);if(u){let d=l,m=o.getSessionCommand(t,d);if(!m)return p(`No running session "${d}" with a command. /apps list`);let h=Lh(d,m,u[1]??"");if(!h.ok)return p(h.error);let f=await Ao(h.body);return p(f.ok?f.message:f.error)}switch(l){case"start":{let d=c.match(/^(\S+)\s+([\s\S]+)$/);return d?p(o.start(t,d[1],d[2],n)):p("Usage: /apps start <name> <command\u2026>")}case"attach":{let d=c.split(/\s+/)[0];return d?p(o.attach(t,d)):p("Usage: /apps attach <name>")}case"detach":return p(o.detach(t));case"list":return p(o.list(t));case"info":case"get":{let d=c.split(/\s+/)[0];return p(o.info(t,d||void 0))}case"send":{let d=c.match(/^(\S+)\s+([\s\S]+)$/);return d?p(await o.sendText(t,d[1],d[2])):p("Usage: /apps send <name> <text\u2026>")}case"key":{let d=c.match(/^(\S+)\s+([\s\S]+)$/);return d?p(o.sendKey(t,d[1],d[2].trim())):p("Usage: /apps key <name> <KEY[,KEY\u2026]>")}case"tail":{let d=c.match(/^(\S+)(?:\s+(\d+))?\s*$/);if(!d)return p("Usage: /apps tail <name> [lines]");let m=d[2]?Number.parseInt(d[2],10):n.appsLogTailLines;return p(o.tail(t,d[1],m))}case"since":{let d=c.split(/\s+/)[0];if(!d)return p("Usage: /apps since <name>");let m=vS(t,d),h=r.get(m)??0,{text:f,nextOffset:g}=o.readSince(t,d,h);return r.set(m,g),p(f.trimEnd()||"(no new log bytes)")}case"mute":{let d=c.split(/\s+/)[0];return d?p(o.mute(t,d)):p("Usage: /apps mute <name>")}case"unmute":{let d=c.split(/\s+/)[0];return d?p(o.unmute(t,d)):p("Usage: /apps unmute <name>")}case"raw":{let d=c.match(/^(\S+)\s+(on|off)\s*$/i);return d?p(o.setRaw(t,d[1],d[2].toLowerCase()==="on")):p("Usage: /apps raw <name> on|off")}case"resize":{let d=c.trim().split(/\s+/).filter(Boolean);if(d.length<3)return p("Usage: /apps resize <name> <cols> <rows>");let m=d[0],h=Number(d[1]),f=Number(d[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 d=c.split(/\s+/)[0];return d?p(o.stop(t,d)):p("Usage: /apps stop <name>")}case"kill":{let d=c.split(/\s+/)[0];return d?p(o.kill(t,d)):p("Usage: /apps kill <name>")}case"rm":{let d=c.split(/\s+/)[0];return d?p(o.rm(t,d)):p("Usage: /apps rm <name>")}default:return p(`Unknown /apps subcommand "${l}". /apps help`)}}function Xh(e,t,n){return!e.clusterEnabled||oc(t.trim())!==null?!0:n?Rd(n,e):!1}async function Zh(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)}xe();ot();function ef(){return p("Not allowlisted on this omnish device.")}async function vr(e,t,n,o,r,s,i,a,l,c,u,d){if((d?.surface??(s.peerKey.startsWith("tg:")?"telegram":"whatsapp"))==="telegram"){let h=Lr(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:ef()});return}}else{let h=Ir(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)){P.warn({denied:s.peerKey,phone:g,senderKey:l},"denied"),await u({kind:"text",body:ef()});return}}try{if(!!!(s.mediaSavedPath||s.mediaError)&&!Xh(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 wn(e,t,n,o,r,s,i,a,l,!1,c);f!==null&&await Zh(u,f)}}catch(h){P.error({err:String(h)},"inbound handler error"),await u({kind:"text",body:p(`Error: ${String(h)}`)}).catch(()=>{})}}function AS(){if(process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{rc.readFileSync(me,"utf8").trim()===String(process.pid)&&rc.unlinkSync(me)}catch{}}async function tf(e){let t=new Map,n=new Map,o=new Map,r=null,s=new Map,i=async(_,fe)=>{let V=s.get(_);r?.sendReply(_,fe,V)},a=new Gt({onJobExit(_){_.notifyPeerKey&&i(_.notifyPeerKey,Bs(_))}}),l=async(_,fe)=>{let V=_.startsWith("tg:")?"telegram":"whatsapp",M=s.get(_),j=ps(_,{kind:"file",spec:fe},M,V);r?.sendRoutedReply(_,j)},c=async(_,fe)=>{let V=s.get(_),M=_.startsWith("tg:")?"telegram":"whatsapp";r?.sendReply(_,de(p(fe),M).text,V)},u={onPlainTextLlmFallback(_,fe){yo(S(),_,fe,V=>i(_,V))},sendToPeer:i},d=new mn(()=>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 oi({env:e,onReplyError:async(_,fe,V)=>{let M=_.startsWith("tg:")?"telegram":"whatsapp";r?.sendReply(_,de(p(`Error sending: ${fe}`),M).text,V)},onMessage:async _=>{let fe=Rl(),V=_.peerKey;s.set(V,_.messageId);let M=_.surface==="telegram"?"telegram":"whatsapp",j=_.senderE164&&M==="whatsapp"?`wa:${_.senderE164}`:V;Jn()&&P.info({peerKey:V,senderKey:j,surface:M},"platform inbound message");let Y=_.mediaSavedPath,J=_.mediaError;if(_.inboundMedia){let q=wm(fe,V,_.inboundMedia);Y=q.mediaSavedPath??Y,J=q.mediaError??J}let ae={peerKey:V,text:_.text,...Y?{mediaSavedPath:Y}:{},...J?{mediaError:J}:{}};await vr(fe,a,t,n,o,ae,d,m,j,u,async q=>{try{if(q.kind==="bundle"){for(let $e of q.texts??[]){let Le=de($e,M);r?.sendReply(V,Le.text,_.messageId)}for(let $e of q.files??[]){let Le=ps(V,{kind:"file",spec:$e},_.messageId,M);r?.sendRoutedReply(V,Le)}return}if(q.kind==="texts"){for(let $e of q.bodies){let Le=de($e,M);r?.sendReply(V,Le.text,_.messageId)}return}if(q.kind==="text"){let $e=de(q.body,M);r?.sendReply(V,$e.text,_.messageId);return}let Be=ps(V,q,_.messageId,M);r?.sendRoutedReply(V,Be)}catch(Be){r?.sendReply(V,`Error sending file: ${String(Be)}`,_.messageId)}},{surface:M})}}),process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{rc.writeFileSync(me,`${process.pid}
|
|
414
|
+
`,{mode:384})}catch(_){P.warn({err:String(_)},"could not write gateway pidfile")}Vs({getCfg:()=>S(),getWaOutbound:()=>null,getTgSendMedia:()=>null,getTgSendText:()=>null,sendPlatformMedia:l,sendPlatformText:c});let h=null,f=S();if(f.webhookEnabled){let _=f.webhookToken||ES.randomBytes(32).toString("hex");f.webhookToken||N({webhookToken:_}),h=Xs({port:f.webhookPort,host:f.webhookHost,token:_},{sendToPeer:i,getDefaultPeerKey:()=>null}).stop}let g=pi({getRunningVersion:lt,getConfig:S,log:P}),y=Ds({getConfig:S,sendToPeer:i,sendMediaToPeer:l}),b=Jr({getConfig:S,sendToPeer:i}),{deviceId:k,account:T}=await r.connect(),$=await ti(e,T??null),L=5*60*1e3,x=setInterval(()=>{ti(e,null).catch(()=>{})},L);x.unref?.();let O=e.deviceId?.trim();if(O){O!==k&&P.warn({configuredDeviceId:O,registeredDeviceId:k},"platform_device_id does not match registered device id");try{await $l(e,O)}catch(_){P.warn({err:String(_)},"could not set platform default device")}}let E=$?.gatewayMode??T?.gatewayMode,K=$?.connectors.whatsapp??T?.connectors?.whatsapp,te=$?.connectors.telegram??T?.connectors?.telegram,ce=[K?`whatsapp:${K.linked?"linked":K.status}`:null,te?`telegram:${te.linked?"linked":te.status}`:null].filter(Boolean).join(", ");console.error(`omnish attached to platform (device ${k})`+(E?` \u2014 gatewayMode=${E}`:"")+(ce?` \u2014 ${ce}`:""));let Se=()=>{clearInterval(x),y(),b(),g?.(),h?.(),AS(),Co(),r?.stop(),d.dispose(),a.killAllRunning(),Nn().stopAll().catch(()=>{}),console.error(`
|
|
415
|
+
${C(process.stderr,"shutting down\u2026")}`),process.exit(0)};process.on("SIGINT",Se),process.on("SIGTERM",Se),await new Promise(()=>{})}function sc(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,de(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,de(o,"whatsapp").text);else n.kind==="text"&&await e.sendWaText?.(t.waJid,de(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)}}ue();tt();G();function Ii(){if(oe()){let i=S(),a=_t(i);return Ho(a)?{ok:!1,message:`Fix security errors before starting the gateway.
|
|
416
|
+
|
|
417
|
+
${a.filter(u=>u.severity==="error").map(u=>{let d=[u.message];return u.detail&&d.push(u.detail),u.fixHint&&d.push(`Fix: ${u.fixHint}`),d.join(`
|
|
372
418
|
`)}).join(`
|
|
373
419
|
|
|
374
|
-
`)}`}:{ok:!0}}let e=S(),t=e.gatewayMode,n=t==="whatsapp"||t==="both",o=t==="telegram"||t==="both",r=
|
|
375
|
-
config: ${
|
|
420
|
+
`)}`}:{ok:!0}}let e=S(),t=e.gatewayMode,n=t==="whatsapp"||t==="both",o=t==="telegram"||t==="both",r=Pe(e);if(o&&!r)return{ok:!1,message:`Telegram enabled (gatewayMode) but no bot token. Set telegramBotToken in config or TELEGRAM_BOT_TOKEN.
|
|
421
|
+
config: ${W}`};if(n&&!pt())return{ok:!1,message:"WhatsApp enabled but no session. Run `omnish link` first."};let s=_t(e);return Ho(s)?{ok:!1,message:`Fix security errors before starting the gateway (or change gatewayMode / token).
|
|
376
422
|
|
|
377
|
-
${s.filter(l=>l.severity==="error").map(l=>{let
|
|
423
|
+
${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
424
|
`)}).join(`
|
|
379
425
|
|
|
380
|
-
`)}`}:{ok:!0}}
|
|
381
|
-
`))}function
|
|
382
|
-
`));return}let i=
|
|
383
|
-
`));return}if(n==="list"){let o=await
|
|
384
|
-
`))}async function
|
|
385
|
-
`));return}let r=
|
|
386
|
-
`,{mode:384});try{
|
|
387
|
-
`,{mode:384});try{
|
|
388
|
-
`,{mode:384}),o}
|
|
389
|
-
`,{mode:384})}function
|
|
390
|
-
|
|
391
|
-
`);let f=
|
|
392
|
-
|
|
393
|
-
`)});r.on("close",()=>{f()});return}if(r.method==="POST"&&a==="/api/wa/link/start"){let g=(await
|
|
394
|
-
|
|
395
|
-
`)
|
|
396
|
-
`)}
|
|
397
|
-
`)
|
|
398
|
-
|
|
399
|
-
`)}
|
|
400
|
-
|
|
401
|
-
`)))
|
|
402
|
-
`)
|
|
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)
|
|
426
|
+
`)}`}:{ok:!0}}tt();G();tt();import Fi from"node:fs";import _i from"node:path";ue();import{spawn as IS}from"node:child_process";import nf from"node:fs";import{stdout as LS}from"node:process";tt();G();xn();var ic=["tunnelRelayUrl","platformToken","platformDeviceId"],of=[{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 Oi(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(`
|
|
427
|
+
`))}function rf(e){return e.split(/[,\s]+/).map(t=>t.trim()).filter(Boolean)}function ac(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 Li(e,t){if(e==="clusterSenderBindings")return JSON.stringify(t.clusterSenderBindings);let n=String(t[e]??"");return e==="platformToken"||e==="telegramBotToken"?Rn(e,n):n}function sf(e){return e==="tunnelRelayUrl"?{value:Vo(),source:so()}:e==="platformToken"?{value:Rn(e,_a()),source:ro()}:e==="platformDeviceId"?{value:(Wa()??"")||"(empty)",source:Da()}:null}function OS(e){if(e!=="platformToken"&&e!=="tunnelRelayUrl")return;let n=S().platformToken.trim()||Nt()?.token?.trim()||"";n&&Ct({token:n,relayUrl:Vo()})}async function NS(){let e=process.env.VISUAL?.trim()||process.env.EDITOR?.trim()||(process.platform==="win32"?"notepad":"nano"),t=nf.readFileSync(W,"utf8");await new Promise((n,o)=>{let r=IS(e,[W],{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 nf.writeFileSync(W,t,{mode:384}),new Error(`Invalid config after edit: ${n instanceof Error?n.message:String(n)}`)}}async function af(e){let t=LS,n=process.stderr,o=(e[0]??"").trim().toLowerCase(),r=e.slice(1);if(!o||o==="help"||o==="--help"||o==="-h"){Oi(t);return}if(o==="add"||o==="edit"){try{if(o==="edit"&&r.length===0){await NS(),console.log(U(t,`Updated ${W}`));return}let s=ac(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=ns(i);if(!l){console.error(C(n,`Unknown key "${i}". Try: ${pd().slice(0,8).join(", ")}\u2026 (omnish config show *)`)),process.exitCode=1;return}Zn(l,a),OS(l);let c=S(),u=l==="tunnelRelayUrl"?c.tunnelRelayUrl:ic.includes(l)?Rn(l,String(c[l]??"")):Li(l,c);console.log(U(t,`${i} \u2192 ${u}`))}console.log(U(t,W))}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,W),""];for(let m of of){c.push(z(t,m.label));for(let h of m.keys){let f=Li(h,l),g=sf(h);g&&ic.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 u=new Set(of.flatMap(m=>m.keys)),d=Sa.filter(m=>!u.has(m));if(d.length>0){c.push(z(t,"Other"));for(let m of d)c.push(` ${v(t,m)}: ${Li(m,l)}`)}console.log(c.join(`
|
|
428
|
+
`));return}let i=rf(s),a=S();for(let l of i){let c=ns(l);if(!c){console.error(C(n,`Unknown key "${l}".`)),process.exitCode=1;return}let u=Li(c,a),d=sf(c);d&&ic.includes(c)?console.log(`${l}: ${u} (effective: ${d.value}, source: ${d.source})`):console.log(`${l}: ${u}`)}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 rf(s)){let a=ns(i);if(!a){console.error(C(n,`Unknown key "${i}".`)),process.exitCode=1;return}md(a),console.log(U(t,`cleared ${i}`))}console.log(U(t,W))}catch(i){console.error(C(n,i instanceof Error?i.message:String(i))),process.exitCode=1}return}console.error(C(n,`Unknown subcommand "${o}".`)),Oi(t),process.exitCode=1}async function lf(e){let t=oe(),n=[];if(n.push(z(e,"platform")),!t){n.push(` ${w(e,"attached:")} ${we(e,"no")} \u2014 set platform_url + platform_token (omnish config add) or env`);let a=so(),l=ro();return(a==="env"||l==="env")&&n.push(` ${w(e,"note:")} ${X(e,"partial env override (need URL + token)")}`),n}let o=so(),r=ro(),s=Da(),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,Rn("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(()=>(cr(),vm)),l=await a(t);n.push(` ${w(e,"gatewayMode:")} ${v(e,l.gatewayMode)} ${X(e,"(platform)")}`);let c=d=>{let m=l.connectors[d];return m?m.linked?v(e,"linked"):X(e,m.status):we(e,"idle")};n.push(` ${w(e,"whatsapp:")} ${c("whatsapp")}`),n.push(` ${w(e,"telegram:")} ${c("telegram")}`);let u=l.routing.onlineCount;n.push(` ${w(e,"routing:")} default=${l.routing.defaultDeviceId??X(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:")} ${X(e,"(could not fetch /v1/me \u2014 check token and URL)")}`)}return n}cr();tt();function uf(){let e=oe();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 FS(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 cf(e){return e.replace(/\D/g,"")}async function df(){let e=uf();if(!e)return;let t=await On(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 pf(e){let t=uf();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(`
|
|
429
|
+
`));return}if(n==="list"){let o=await On(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 On(t),r=[...o.allowFrom],s=[...o.telegramAllowFrom];for(let a of e.slice(1)){let l=FS(a);if(!l){console.error(C(process.stderr,`Unrecognized entry: ${a}`)),process.exitCode=1;return}if(l.kind==="wa"){let c=cf(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 ei(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=>cf(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 ei(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 mf(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=ac(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}Zn("tunnelRelayUrl",t.replace(/\/$/,"")),Zn("platformToken",n),o&&Zn("platformDeviceId",o),console.log(U(process.stdout,"Platform credentials saved to config.json.")),console.log(" Run: omnish platform status && omnish run")}import _S from"qrcode-terminal";tt();var WS=1e3,DS=120;function hf(){let e=oe();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 cc(e){let t=e.replace(/\/$/,"");return/^https?:\/\//i.test(t)?t:`http://${t}`}async function ff(e){let t=`${cc(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 US(e){let t=await fetch(`${cc(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 gf(e){let t=process.stdout,n=w(t,"\xB7".repeat(42));console.log(X(t,"Scan with WhatsApp \u2192 Linked devices")),console.log(n),_S.generate(e,{small:!0}),console.log(n)}var Ni="",lc=!1;function HS(e){let t=e.status??"";return t==="qr"||t==="connecting"||t==="reconnecting"||t==="pairing_restart"}async function BS(e){for(let t=0;t<DS;t++){let n=await ff(e);if(n.status==="linked"||n.linked)return n;if(n.status==="pairing_restart"&&!lc&&(lc=!0,console.log(w(process.stdout,"Finishing WhatsApp link after scan (server restart) \u2014 wait a few seconds\u2026"))),n.qr&&n.qr!==Ni&&(Ni=n.qr,gf(n.qr)),!HS(n))throw new Error(n.error||n.statusMessage||`Unexpected status: ${n.status}`);await new Promise(o=>setTimeout(o,WS))}throw new Error("Timed out waiting for WhatsApp to link (scan the QR within ~2 minutes).")}async function yf(){let e=hf();if(!e)return;let t=await ff(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")),lc=!1,Ni="",t=await US(e),t.qr&&(gf(t.qr),Ni=t.qr),t=await BS(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 wf(){let e=hf();if(!e)return;let t=await fetch(`${cc(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 jS from"ws";function bf(e,t,n){return new Promise(o=>{let r=new jS(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 kf(e,t,n=12e3){let o=e.replace(/\/$/,""),r=Ro(o),s=await bf(r,t,n),i,a,l="";for(let c of ni(o)){let u=await bf(c,t,n);if(u.ok)return i=new URL(c).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 GS=400,JS=8*1024*1024,qS=2*1024*1024;function zS(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=_i.resolve(e[++r]))}return{authDir:t,force:n,help:o}}function KS(e){let t={},n=0,o=0;function r(s,i){let a;try{a=Fi.readdirSync(i,{withFileTypes:!0})}catch{return}for(let l of a){let c=l.name;if(c==="."||c==="..")continue;let u=_i.join(i,c),m=(s?`${s}/${c}`:c).split(_i.sep).join("/");if(!l.isSymbolicLink()){if(l.isDirectory())r(m,u);else if(l.isFile()){let h=Fi.readFileSync(u);if(h.length>qS)throw new Error(`file too large: ${m}`);if(n+=h.length,n>JS)throw new Error("total auth size exceeds limit (8 MiB)");if(o+=1,o>GS)throw new Error("too many files (max 400)");t[m]=h.toString("base64")}}}}return r("",e),t}function Wi(e){console.log([he(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(`
|
|
430
|
+
`))}async function YS(e){let{authDir:t,force:n,help:o}=zS(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(`
|
|
431
|
+
`));return}let r=oe();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(!Fi.existsSync(t)){console.error(C(process.stderr,`Auth directory not found: ${t}`)),process.exitCode=1;return}se();let s=_i.join(t,"creds.json");if(!Fi.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=KS(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`,u=await fetch(c,{method:"POST",headers:{"content-type":"application/json",Authorization:`Bearer ${r.token}`},body:JSON.stringify({files:i,force:n})}),d=await u.json().catch(()=>({}));if(!u.ok){console.error(C(process.stderr,d.error||`HTTP ${u.status}`)),process.exitCode=1;return}let m=d.qr?" QR emitted \u2014 open the platform dashboard if you need to scan.":"";console.log(U(process.stdout,`Uploaded Baileys auth. Platform connector status: ${d.status??"unknown"}.${m}`))}async function QS(){let e=process.stdout,t=process.stderr,n=oe();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(`${he(e,"Platform probe")} ${w(e,n.platformUrl)}`);let o=await kf(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}:`)} ${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}:`)} ${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(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 vf(e){let t=(e[0]??"").toLowerCase();if(!t||t==="-h"||t==="--help"){Wi(process.stdout);return}if(t==="login"){await mf(e.slice(1));return}if(t==="status"){await df();return}if(t==="allow"){await pf(e.slice(1));return}if(t==="probe"){await QS();return}if(t==="link-whatsapp"){await yf();return}if(t==="unlink-whatsapp"){await wf();return}if(t==="import-whatsapp"){await YS(e.slice(1));return}console.error(C(process.stderr,`Unknown platform subcommand: ${e[0]}`)),Wi(process.stderr),process.exitCode=1}G();import Cf from"node:crypto";import Dn from"node:fs";import Rf from"node:path";function VS(){return Cf.randomBytes(24).toString("hex")}function Sf(){return Cf.randomBytes(32).toString("hex")}function xf(e){try{let t=Dn.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 XS(){let e=xf(bt);if(e)return e;if(e=xf(Lo),e){B(Rf.dirname(bt)),Dn.writeFileSync(bt,JSON.stringify(e,null,2)+`
|
|
432
|
+
`,{mode:384});try{Dn.unlinkSync(Lo)}catch{}return e}return null}function Tf(e){B(Rf.dirname(bt));let t=XS(),n=(e??"").trim();if(n){let r={token:n,secret:t?.secret??Sf()};Dn.writeFileSync(bt,JSON.stringify(r,null,2)+`
|
|
433
|
+
`,{mode:384});try{Dn.existsSync(Lo)&&Dn.unlinkSync(Lo)}catch{}return r}if(t)return t;let o={token:VS(),secret:Sf()};return Dn.writeFileSync(bt,JSON.stringify(o,null,2)+`
|
|
434
|
+
`,{mode:384}),o}ue();import bn from"node:fs";import rx from"node:http";import We from"node:path";import wt from"node:process";import{fileURLToPath as sx}from"node:url";import ix from"node:os";ot();G();import uc from"node:crypto";var dc="omnish_cfg_sess",$f=7*24*60*60*1e3;function pc(e){let t=Date.now()+$f,n=Buffer.from(JSON.stringify({exp:t}),"utf8").toString("base64url"),o=uc.createHmac("sha256",e).update(n).digest("hex");return`${n}.${o}`}function Pf(e,t){let n=ZS(t??"")[dc];if(!n||!n.includes("."))return!1;let o=n.lastIndexOf("."),r=n.slice(0,o),s=n.slice(o+1),i=uc.createHmac("sha256",e).update(r).digest("hex");try{let a=Buffer.from(s,"hex"),l=Buffer.from(i,"hex");if(a.length!==l.length||!uc.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 mc(e){let t=Math.floor($f/1e3);return`${dc}=${e}; HttpOnly; Path=/; SameSite=Lax; Max-Age=${t}`}function Mf(){return`${dc}=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0`}function ZS(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 hc from"node:fs";import Di from"node:process";function Ef(e){try{return Di.kill(e,0),!0}catch{return!1}}function Af(e){let t=Date.now()+e;for(;Date.now()<t;);}function ex(){try{let e=hc.readFileSync(Tr,"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 If(e){hc.writeFileSync(Tr,`${JSON.stringify(e)}
|
|
435
|
+
`,{mode:384})}function Sr(){try{hc.unlinkSync(Tr)}catch{}}function Lf(e){let t=ex();if(t&&t.port===e&&t.pid!==Di.pid){if(!Ef(t.pid)){Sr();return}try{Di.kill(t.pid,"SIGTERM")}catch{}if(Af(350),Ef(t.pid)){try{Di.kill(t.pid,"SIGKILL")}catch{}Af(100)}Sr()}}ue();G();import fc from"node:fs";import tx from"node:process";function nx(e){try{return tx.kill(e,0),!0}catch{return!1}}function ox(){try{let e=fc.readFileSync(me,"utf8").trim(),t=Number.parseInt(e,10);return Number.isFinite(t)&&t>0?t:null}catch{return null}}function Ui(){let e=ox();return e===null?!1:nx(e)}var gc=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&&(vo(this.currentSock),this.currentSock=null)}beginPairing(t){if(this.busy)throw new Error("Pairing already in progress.");if(Ui())throw new Error("Gateway appears to be running (gateway.pid). Stop `omnish run` or your service before pairing from the browser.");if(!t.force&&pt())throw new Error("WhatsApp session already linked. Use \u201CReplace session\u201D or run `omnish link --force` from the CLI.");se(),t.force&&(fc.rmSync(le,{recursive:!0,force:!0}),fc.mkdirSync(le,{recursive:!0,mode:448})),this.busy=!0,this.abort=new AbortController;let n=this.abort.signal;(async()=>{try{await fm({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}})()}},xr=new gc;var pe="application/json; charset=utf-8",Cr=null;function ax(){Sr();let e=Cr;Cr=null,e?e.close(()=>{wt.exit(0)}):wt.exit(0),setTimeout(()=>wt.exit(0),4e3).unref()}function lx(){let e=wt.env.OMNISH_CONFIG_UI_STATIC?.trim(),t=wt.env.OMNISH_UI_STATIC?.trim()||e;if(t&&bn.existsSync(We.join(t,"index.html")))return t;let n=We.dirname(sx(import.meta.url)),o=We.join(n,"ui");if(bn.existsSync(We.join(o,"index.html")))return o;let r=We.join(n,"..","..","dist","ui");if(bn.existsSync(We.join(r,"index.html")))return r;let s=We.join(wt.cwd(),"dist","ui");if(bn.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 cx(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 ux(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 yc(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 dx(e){return sn.includes(e)}function wc(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:!!Pe(e),telegramBotTokenEnvOverride:typeof wt.env.TELEGRAM_BOT_TOKEN=="string"&&wt.env.TELEGRAM_BOT_TOKEN.trim().length>0}}function px(){let e=S();return{version:lt(),dataDir:D,configPath:W,waAuthDir:le,whatsappLinked:pt(),gatewayPidHint:bn.existsSync(We.join(D,"gateway.pid")),gatewayRunning:Ui(),gatewayLogFile:je,gatewayMode:e.gatewayMode,telegramBotTokenMasked:Pe(e).length===0?"":wc(e).telegramBotToken,telegramBotTokenEnvOverride:typeof wt.env.TELEGRAM_BOT_TOKEN=="string"&&wt.env.TELEGRAM_BOT_TOKEN.trim().length>0}}function mx(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=Ar(n).filter(a=>a!=="*"),s=[];for(let a of o){let l=Fe(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(),Ge(i)}function hx(e){return typeof e=="string"?e:typeof e=="boolean"||typeof e=="number"?String(e):JSON.stringify(e)}async function Of(e){let t=lx(),{meta:n}=e;Lf(e.port);let o=rx.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=pc(n.secret);s.statusCode=302,s.setHeader("Location","/"),s.setHeader("Set-Cookie",mc(h)),s.end();return}}if(a.startsWith("/api/")){let m=r.headers.cookie,h=Pf(n.secret,m);if(r.method==="POST"&&a==="/api/session"){let f=await yc(r),g=typeof f?.token=="string"?f.token.trim():"";if(!g||g!==n.token){s.statusCode=401,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!1,error:"Invalid token."}));return}let y=pc(n.secret);s.statusCode=200,s.setHeader("Content-Type",pe),s.setHeader("Set-Cookie",mc(y)),s.end(JSON.stringify({ok:!0}));return}if(r.method==="GET"&&a==="/api/me"){s.statusCode=h?200:401,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:h}));return}if(!h){s.statusCode=401,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!1,error:"Unauthorized."}));return}if(r.method==="GET"&&a==="/api/status"){s.statusCode=200,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!0,...px()}));return}if(r.method==="GET"&&a==="/api/config"){s.statusCode=200,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!0,config:wc(S())}));return}if(r.method==="PUT"&&a==="/api/config"){let f=await yc(r);if(!f||typeof f!="object"){s.statusCode=400,s.setHeader("Content-Type",pe),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(!vt(k))throw new Error("Invalid Telegram bot token format.");ts("telegramBotToken",k);continue}if(!dx(y))throw new Error(`Unknown or unsupported config key: ${y}`);ts(y,hx(b))}if("allowFrom"in g||"telegramAllowFrom"in g){let y=S();mx("allowFrom"in g?g.allowFrom:y.allowFrom,"telegramAllowFrom"in g?g.telegramAllowFrom:y.telegramAllowFrom)}s.statusCode=200,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!0,config:wc(S())}));return}if(r.method==="POST"&&a==="/api/logout"){s.statusCode=200,s.setHeader("Content-Type",pe),s.setHeader("Set-Cookie",Mf()),s.end(JSON.stringify({ok:!0}));return}if(r.method==="POST"&&a==="/api/shutdown"){s.statusCode=200,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!0})),setImmediate(()=>{xr.requestCancel(),ax()});return}if(r.method==="POST"&&a==="/api/gateway/start"){if(Ui()){s.statusCode=409,s.setHeader("Content-Type",pe),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=Ii();if(!f.ok){s.statusCode=400,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!1,error:f.message}));return}let g=je,y=Ys(g);if(!y.ok){s.statusCode=500,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!1,error:y.message}));return}s.statusCode=200,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!0,pid:y.pid,logFile:g}));return}if(r.method==="POST"&&a==="/api/gateway/stop"){let f=Qs();switch(f.outcome){case"no_pidfile":s.statusCode=400,s.setHeader("Content-Type",pe),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",pe),s.end(JSON.stringify({ok:!1,error:"Invalid pidfile (removed)."}));return;case"stale_cleaned":s.statusCode=200,s.setHeader("Content-Type",pe),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",pe),s.end(JSON.stringify({ok:!0,pid:f.pid}));return;case"taskkill_ok":s.statusCode=200,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!0,pid:f.pid,taskkill:!0}));return;case"failed":s.statusCode=500,s.setHeader("Content-Type",pe),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
|
|
436
|
+
|
|
437
|
+
`);let f=xr.subscribe(g=>{s.write(`data: ${JSON.stringify(g)}
|
|
438
|
+
|
|
439
|
+
`)});r.on("close",()=>{f()});return}if(r.method==="POST"&&a==="/api/wa/link/start"){let g=(await yc(r))?.force===!0;try{xr.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",pe),s.end(JSON.stringify({ok:!1,error:b}));return}s.statusCode=202,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!0}));return}if(r.method==="POST"&&a==="/api/wa/link/cancel"){xr.requestCancel(),s.statusCode=200,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!0}));return}s.statusCode=404,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!1,error:"Not found."}));return}let l=a==="/"?"index.html":a.replace(/^\/+/,""),c=ux(t,l),u=We.join(t,"index.html");c&&bn.existsSync(c)&&bn.statSync(c).isFile()&&(u=c);let d=bn.readFileSync(u);s.statusCode=200,s.setHeader("Content-Type",cx(u)),s.setHeader("Cache-Control",We.basename(u)==="index.html"?"no-store":"public, max-age=3600"),s.end(d)}catch(i){s.statusCode=500,s.setHeader("Content-Type",pe),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())}),Cr=o,If({pid:wt.pid,port:e.port,host:e.host,startedAt:new Date().toISOString()}),o.on("close",()=>{Sr(),Cr===o&&(Cr=null)})}function Nf(e){let t=ix.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 yx from"node:readline";import _f from"node:path";import Ie from"node:process";ue();ot();ot();function bc(e){return e.channel==="all"||e.channel==="whatsapp-all"||e.channel==="telegram-all"}function fx(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 gx(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=Fe(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 Ff(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=gx(r);if(!i)return null;let a=fx(s);if(a===null)return null;if(a!==void 0)return{...i,mode:"text",body:a};let l=mi(s);if(!l)return null;let{selectorPart:c,caption:u}=l;return{...i,mode:"media",selectorPart:c,caption:u}}function kc(){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(`
|
|
440
|
+
`)}cr();tt();G();var Io="wa:cli:interactive",wx={onPlainTextLlmFallback(e,t){yo(S(),e,t,async n=>{n.trim()&&console.log(U(Ie.stdout,n))})}};function bx(e){let t=e.trim();if(!t)return null;let n=t.toLowerCase();if(n.startsWith("tg:")||n.startsWith("telegram:")){let s=Fe(t);return s?`tg:${s}`:null}let o=n.startsWith("wa:")?t.slice(3):t,r=ne(o);return r?`wa:${r}`:null}function kx(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=bx(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 vc(e){let t=Ie.cwd(),n=[`${he(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:"),...Lt(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.")}`,"",kc(),"",`${w(e,"cwd:")} ${v(e,`session starts at ${t} (change with !cd or ${S().commandPrefix}cd).`)}`];console.log(n.join(`
|
|
441
|
+
`))}function Wf(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=Fe(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 Df(e){let t="No recipients matched the requested /sendto destination.";return bc(e)?oe()?`${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 vx(e){let t=bc(e)?await Tl():S();if(e.mode==="text"){let{waTargets:d,tgTargets:m}=Wf(e,t);if(d.size===0&&m.size===0)return{error:Df(e)};let h=[];for(let f of d){let g=await Tt({op:"sendText",channel:"whatsapp",e164:f,text:e.body});g&&h.push(`[wa:${f}] ${g}`)}for(let f of m){let g=await Tt({op:"sendText",channel:"telegram",chatId:f,text:e.body});g&&h.push(`[tg:${f}] ${g}`)}return h.length>0?{error:h.join(`
|
|
442
|
+
`)}:{error:null,kind:"text",recipientsSent:d.size+m.size}}let n=ie(Io).cwd,o=await pr(n,e.selectorPart);if(o.length===0)return{error:`No files matched: ${e.selectorPart}`};let r=await mr(o);if(!r.ok)return{error:r.error};let s=o.map(d=>ft(d,t.fileSendMaxBytes));for(let d of s)if("error"in d)return{error:d.error};let{waTargets:i,tgTargets:a}=Wf(e,t);if(i.size===0&&a.size===0)return{error:Df(e)};let l=[];for(let d of i)for(let m of s){if("error"in m)continue;let h=await Tt({op:"sendMedia",channel:"whatsapp",e164:d,absPath:m.absPath,caption:e.caption});h&&l.push(`[wa:${d}] ${m.displayName}: ${h}`)}for(let d of a)for(let m of s){if("error"in m)continue;let h=await Tt({op:"sendMedia",channel:"telegram",chatId:d,absPath:m.absPath,caption:e.caption});h&&l.push(`[tg:${d}] ${m.displayName}: ${h}`)}if(l.length>0)return{error:l.join(`
|
|
443
|
+
`)};let c=i.size+a.size,u=s.length;return{error:null,kind:"media",recipientsSent:c,filesSent:u,messagesSent:c*u}}async function Sx(e,t,n,o,r,s,i){let a=e.trim();if(!a)return;let l=Ff(a);if(l!==null||/^\/sendto(\s|$)/i.test(a)){if(l===null){console.log(C(Ie.stderr,`Invalid /sendto.
|
|
444
|
+
`+kc()));return}let d=await vx(l);if(d.error!==null)console.log(C(Ie.stderr,d.error));else if(d.kind==="text")console.log(U(Ie.stdout,`Sent text to ${d.recipientsSent} recipient(s).`));else{let m=`Sent ${d.filesSent} file(s) to ${d.recipientsSent} recipient(s) (${d.messagesSent} message(s)).`;console.log(U(Ie.stdout,m))}return}let c={peerKey:Io,text:e},u=await wn(S(),t,n,o,r,c,s,void 0,i,!1,wx);u!==null&&await xx(u)}async function xx(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(`
|
|
445
|
+
`)));return}if(e.kind==="bundle"){for(let n of e.texts??[]){let o=de(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=de(e.bodies[n],"whatsapp").text;o.trim()&&(n>0&&console.log(""),console.log(o))}return}if(e.kind!=="text")return;let t=de(e.body,"whatsapp").text;t.trim()&&console.log(t)}async function Uf(e){let t=kx(e);if(t.error==="help"){vc(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??Io;se(),qr(Io,Ie.cwd());let s=new Gt,i=new Map,a=new Map,l=new Map,c=new mn(()=>S(),async(h,f)=>{Ie.stdout.write(f),f.endsWith(`
|
|
446
|
+
`)||Ie.stdout.write(`
|
|
447
|
+
`)}),u=async h=>{try{await Sx(h,s,i,a,l,c,r)}catch(f){console.error(C(Ie.stderr,String(f)))}};if(o!==null){await u(o),c.dispose(),s.killAllRunning();return}let d=yx.createInterface({input:Ie.stdin,output:Ie.stdout}),m=_f.basename(ie(Io).cwd);d.setPrompt(`${m}> `),d.on("line",h=>{u(h).then(()=>{let f=_f.basename(ie(Io).cwd);d.setPrompt(`${f}> `),d.prompt()})}),d.on("close",()=>{c.dispose(),s.killAllRunning(),Ie.stdout.write(`
|
|
448
|
+
`)}),d.prompt()}function Sc(){console.log(`omnish docs \u2014 search bundled documentation (offline)
|
|
405
449
|
|
|
406
450
|
omnish docs help
|
|
407
451
|
omnish docs search <topic>
|
|
@@ -409,22 +453,22 @@ ${s.filter(l=>l.severity==="error").map(l=>{let d=[l.message];return l.detail&&d
|
|
|
409
453
|
omnish docs show <path> e.g. docs/features/tunneling.md
|
|
410
454
|
|
|
411
455
|
Chat: /docs search <topic> \xB7 /docs <n> \xB7 /docs follow <n>
|
|
412
|
-
`)}function
|
|
456
|
+
`)}function Bf(e){let t=(e[0]??"help").toLowerCase(),n=e.slice(1).join(" ").trim();if(t==="help"||t==="-h"||t==="--help"){Sc();return}if(t==="search"){if(!n){console.error("[omnish] Usage: omnish docs search <topic>"),process.exitCode=1;return}let r=Ti(n).map($i);if(Yh(r),r.length===0){console.log(`Search: ${n}
|
|
413
457
|
(no results)`);return}console.log(`Search: ${n}
|
|
414
458
|
`),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=
|
|
416
|
-
`))}function
|
|
417
|
-
`))}function
|
|
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
|
|
419
|
-
`))}function
|
|
420
|
-
`))}function
|
|
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,
|
|
422
|
-
`),console.warn(
|
|
423
|
-
`,{mode:384})}catch(
|
|
424
|
-
`),
|
|
425
|
-
|
|
426
|
-
Updates (last check): ${
|
|
427
|
-
${
|
|
428
|
-
`))}async function
|
|
429
|
-
`));return}await
|
|
430
|
-
`)),r){let f=await
|
|
459
|
+
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=Qh(n);if(!r){console.error("[omnish] No result #"+n+". Run omnish docs search first."),process.exitCode=1;return}let s=kr(r.id);if(!s){console.error("[omnish] Entry missing from index."),process.exitCode=1;return}Hf(s,Number.parseInt(n,10));return}let o=Pi(n);if(!o){console.error(`[omnish] No doc at path "${n}".`),process.exitCode=1;return}Hf(o);return}console.error(`[omnish] Unknown docs subcommand "${t}". Try: omnish docs help`),process.exitCode=1}function Hf(e,t){if(!e)return;let n=Ri(yn.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(Mi(e)),console.log(""),e.relatedCommands.length){console.log("Try:");for(let r of e.relatedCommands.slice(0,8))console.log(` ${r}`)}}Cx.setDefaultResultOrder("ipv4first");function Jf(){let e=process.stdout,t=[`${Ce(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:"),...Lt(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: ${je}).`},{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=oe();n?t.push(z(e,"Attached mode (platform credentials detected):"),` ${w(e,"url:")} ${v(e,n.platformUrl)} ${X(e,`[${so()}]`)}`,` ${w(e,"token:")} ${v(e,Rn("platformToken",n.token))} ${X(e,`[${ro()}]`)}`,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(`
|
|
460
|
+
`))}function qf(){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(Qr)),r=t.map((i,a)=>sa(" ",o,n[a],v(e,i.right))),s=[`${Ce(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:"),...Lt(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:")} ${he(e,"omnish allow tg:<your_user_id>")} ${w(e,"then")} ${he(e,"omnish run")}`,`${w(e,"Config:")} ${v(e,W)}`,""];oe()&&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(`
|
|
461
|
+
`))}function Tx(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"){qi(!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}
|
|
462
|
+
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 Cc(){let e=process.stdout,t=`${Ce(e,"omnish")} ${w(e,`v${lt()}`)}`,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: ${me})`},{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:"),...Lt(e," ",o,s=>w(e,s)),"",z(e,"Commands:"),...Lt(e," ",n,s=>w(e,s)),"",`${w(e,"Config:")} ${v(e,`${W} \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(`
|
|
463
|
+
`))}function $x(e){let t=(e??"").trim().toLowerCase(),n=process.stdout;if(!t){Cc();return}switch(t){case"link":qf();return;case"run":Jf();return;case"service":Kf();return;case"pull":Qa();return;case"i":case"interactive":vc(n);return;case"ui":Yf();return;case"config":Oi(n);return;case"platform":Wi(n);return;case"docs":Sc();return;default:console.error(C(process.stderr,`No detailed help for "${e}". Try: omnish help`)),process.exitCode=1}}function Px(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()!==""?xc.isAbsolute(n)?n:xc.resolve(process.cwd(),n):je;return{background:t,logFile:s,help:o,verbose:r}}function Mx(e,t){let n=Ys(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 Ex(){let e=Qs();switch(e.outcome){case"no_pidfile":console.error(C(process.stderr,`no pidfile at ${me} \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 jf(){if(process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{kn.readFileSync(me,"utf8").trim()===String(process.pid)&&kn.unlinkSync(me)}catch{}}function Ax(e){return e.length<=8?"(set)":`${e.slice(0,4)}\u2026${e.slice(-4)}`}function Ix(){if(!kn.existsSync(me))return"gateway process: not running (no pid file)";let e=kn.readFileSync(me,"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 zf=120;function Kf(){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 ${zf}).`},{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>")}`,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:"),...Lt(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(`
|
|
464
|
+
`))}function Lx(e){let t=process.stdout,n=process.stderr,o=(e[0]??"help").toLowerCase();if(o==="help"||o==="--help"||o==="-h"){Kf();return}if(o==="instructions"){let r=Bt();if(r.error){console.error(C(n,r.error)),process.exitCode=1;return}console.log(rs(r));return}if(o==="status"){let r=Bt(),s=(()=>{try{return kn.existsSync(me)?`gateway.pid: ${kn.readFileSync(me,"utf8").trim()}`:"gateway.pid: (missing)"}catch(d){return`gateway.pid: (read error: ${String(d)})`}})(),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}
|
|
465
|
+
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,D)}`),console.log(`${w(t,"pidfile:")} ${v(t,s)}`),console.log(`${w(t,"default log:")} ${v(t,je)}`),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,zf):80,i=bs(je,s);console.log(`${w(t,"file:")} ${v(t,je)}`),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=gs();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=ys();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 Ox(e){let t=e.trim();if(!t)return null;let n=t.toLowerCase();if(n.startsWith("tg:")||n.startsWith("telegram:")){let s=Fe(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=Ii();t.ok||(console.error(C(process.stderr,t.message)),process.exit(1));let n=oe();if(n){await tf(n);return}let o=S(),r=o.gatewayMode,s=r==="whatsapp"||r==="both",i=r==="telegram"||r==="both",a=Pe(o),l=_t(o),c=process.stderr,u=Tu(l,"warn");if(u.length>0&&(console.warn(`${U(c,`Security (${u.length} finding(s)):`)}
|
|
466
|
+
`),console.warn(ua(u,c))),process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{kn.writeFileSync(me,`${process.pid}
|
|
467
|
+
`,{mode:384})}catch(M){P.warn({err:String(M)},"could not write gateway pidfile")}let d=(M,j)=>{let Y=S();if(!Y.clusterEnabled)return M;let J=at(),ae=(Y.clusterLabel??"").trim()||Gf.hostname(),q=null;if(j.startsWith("tg:"))q=j;else if(j){let $e=ne(j);$e&&(q=`wa:${$e}`)}let Be=q?Wt(Y,q):null;return hd(M,{nodeId:J,label:ae,role:Y.clusterRole,activeNodeId:Be?.nodeId??""})},m=new Map,h=new Map,f=new Map,g=null,y={stop:null,sendText:null,sendMedia:null},b=null,k=!1,T=async(M,j)=>{if(M.startsWith("wa:")){let Y=M.slice(3);g&&await g.sendText(Y,j)}else if(M.startsWith("tg:")){let Y=Number(M.slice(3));y.sendText&&Number.isFinite(Y)&&await y.sendText(Y,p(j))}},$=new Gt({onJobExit(M){M.notifyPeerKey&&T(M.notifyPeerKey,Bs(M))}}),L={onPlainTextLlmFallback(M,j){yo(S(),M,j,Y=>T(M,Y))},sendToPeer:T},x=async(M,j)=>{if(M.startsWith("wa:")){let Y=M.slice(3);g&&await g.sendMedia(Y,j)}else if(M.startsWith("tg:")){let Y=Number(M.slice(3));y.sendMedia&&Number.isFinite(Y)&&await y.sendMedia(Y,j)}},O=()=>new mn(()=>S(),T),E=O();b=E;let K,te={async reload(){try{P.info("gateway reload requested from chat"),await y.stop?.().catch(()=>{}),y.stop=null,y.sendText=null,y.sendMedia=null;let M=S(),j=M.gatewayMode==="telegram"||M.gatewayMode==="both",Y=Pe(M);if(j&&Y){let q=await wl(Y,()=>S(),K,{decorate:d});y.sendText=q.sendText,y.sendMedia=q.sendMedia,y.stop=q.stop}let J=["Reload complete.",`gatewayMode: ${M.gatewayMode}`,j&&Y?"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(`
|
|
468
|
+
`),ae=di(ur());return{ok:!0,summary:ae?`${J}
|
|
469
|
+
|
|
470
|
+
Updates (last check): ${ae}`:J}}catch(M){return{ok:!1,error:String(M)}}}};if(K=async(M,j)=>{let Y=S();await vr(Y,$,m,h,f,M,E,te,M.peerKey,L,sc({sendTg:j},{surface:"telegram"}),{surface:"telegram"})},i){let M=await wl(a,()=>S(),K,{decorate:d});y.sendText=M.sendText,y.sendMedia=M.sendMedia,y.stop=M.stop}Vs({getCfg:()=>S(),getWaOutbound:()=>g,getTgSendMedia:()=>y.sendMedia,getTgSendText:()=>y.sendText,sendPeerMedia:x});let ce=null;{let M=S();if(M.webhookEnabled){let j=M.webhookToken||Rx.randomBytes(32).toString("hex");M.webhookToken||N({webhookToken:j}),ce=Xs({port:M.webhookPort,host:M.webhookHost,token:j},{sendToPeer:T,getDefaultPeerKey:()=>{let J=S();return J.allowFrom.length>0?`wa:${Yt(J.allowFrom[0])}`:J.telegramAllowFrom.length>0?`tg:${J.telegramAllowFrom[0]}`:null}}).stop}}e=pi({getRunningVersion:lt,getConfig:S,log:P});let Se=Ds({getConfig:S,sendToPeer:T,sendMediaToPeer:x}),_=Jr({getConfig:S,sendToPeer:T}),fe=!i,V=()=>{k=!0,Se(),_(),e?.(),e=null,ce?.(),jf(),Co(),y.stop?.().catch(()=>{}),b?.dispose(),$.killAllRunning(),Nn().stopAll().catch(()=>{}),console.error(`
|
|
471
|
+
${C(process.stderr,"shutting down\u2026")}`),process.exit(0)};if(process.on("SIGINT",V),process.on("SIGTERM",V),s)for(;!k;){let M=!1,j;try{j=await Gs({printQr:!1,verbose:Jn()}),await Ln(Js(j),3e5,"Gateway: timed out waiting for WhatsApp connection (5 min).")}catch(J){console.error(C(process.stderr,`connect failed: ${String(J)}`)),await new Promise(ae=>setTimeout(ae,5e3));continue}g=cm(j,{decorate:d});let Y=om(j,async J=>{let ae=S(),q=J.fromE164||ne(J.fromJid)||"",Be=Pd(J),$e=`wa:${q}`;await vr(ae,$,m,h,f,Be,E,te,$e,L,sc({sendWaText:(Le,R)=>g.sendText(Le,R),sendWaMedia:(Le,R)=>g.sendMedia(Le,R)},{surface:"whatsapp",waJid:J.fromJid}),{surface:"whatsapp"})});if(await new Promise(J=>{let ae=q=>{q.connection==="close"&&(fl(q.lastDisconnect)===In.loggedOut&&(M=!0),j.ev.off("connection.update",ae),J())};j.ev.on("connection.update",ae)}),Y(),fe&&(E.dispose(),E=O(),b=E),vo(j),g=null,M&&(console.error(C(process.stderr,"session logged out. Run `omnish link` again.")),Se(),e?.(),e=null,jf(),Co(),y.stop?.().catch(()=>{}),process.exit(1)),k)break;await new Promise(J=>setTimeout(J,3e3))}else for(;!k;)await new Promise(M=>setTimeout(M,500));Se(),e?.(),Co(),y.stop?.().catch(()=>{}),E.dispose()}function Fx(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 Yf(){let e=process.stdout,t=[`${Ce(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:"),...Lt(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 ${bt}).`},{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(`
|
|
472
|
+
`))}async function _x(){let[,,e,...t]=process.argv;if(e==="--version"||e==="-v"||e==="-V"){let n=process.stdout;console.log(`${Ce(n,"omnish")} ${w(n,lt())}`);return}if(e==="--help"||e==="-h"){Cc();return}if(e==="help"){$x(t[0]);return}switch(se(),e){case"link":{let n=Tx(t);if(n.kind==="help"){qf();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(!vt(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=pt()?"both":"telegram";_r(i),console.log([`${he(s,"Telegram")} ${v(s,"bot token saved to")} ${w(s,W)}`,`${w(s,"gatewayMode:")} ${he(s,i)}`,"",v(s,"Next:"),` ${w(s,"1.")} ${v(s,"Find your numeric user id (e.g. t.me/userinfobot), then:")} ${he(s,"omnish allow tg:<id>")}`,` ${w(s,"2.")} ${he(s,"omnish run")}`,""].join(`
|
|
473
|
+
`));return}await gm({verbose:Jn(),force:n.force});return}case"run":{let n=Px(t);if(n.help){Jf();return}if(n.verbose&&qi(!0),n.background){Mx(n.logFile,n.verbose);return}await Nx();return}case"stop":Ex();return;case"logout":{try{kn.rmSync(le,{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=Nr(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=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(C(r,String(s).replace(/^\[omnish\]\s*/,""))),process.exitCode=1}return}case"i":case"interactive":{await Uf(t);return}case"status":{let n=process.stdout,o=S(),r=t.includes("--check-updates"),s=new Gt().list(),i=s.filter(f=>f.status==="running").length,a=Pe(o),l=_t(o),c=o.gatewayMode==="whatsapp"||o.gatewayMode==="both",u=o.gatewayMode==="telegram"||o.gatewayMode==="both",d=pt(),m=d?mm(le):null,h=[];if(h.push(`${Ce(n,"omnish")} ${w(n,lt())}`,`${w(n,"gatewayMode:")} ${v(n,o.gatewayMode)}`,`${w(n,"data dir:")} ${v(n,xc.dirname(le))}`,"",`${w(n,"gateway process:")} ${v(n,Ix().replace(/^gateway process: /,""))}`,"",on(n),he(n,"whatsapp"),` ${w(n,"in use:")} ${c?v(n,"yes"):we(n,"no (gatewayMode is telegram-only)")}`),c){let f=d?v(n,`linked (${le})`):we(n,"missing \u2014 run omnish link");h.push(` ${w(n,"session:")} ${f}`),d&&m&&h.push(` ${w(n,"linked as:")} ${v(n,m)}`),d&&!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("",on(n),he(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,Ax(a)):we(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("",on(n),...await lf(n)),h.push("",on(n),`${he(n,"jobs")} ${w(n,`(recent): ${s.length} total, ${i} running`)}`,Mu(l,n)),console.log(h.join(`
|
|
474
|
+
`)),r){let f=await dr(lt(),o),g=jo(f);console.log(""),console.log(on(n)),console.log(de(g,"whatsapp").text)}if(o.clusterEnabled){let f=at(),g=ke(),y=Object.keys(g.senderBindings).length,b=Object.keys(o.clusterSenderBindings??{}).length;console.log(""),console.log(on(n)),console.log(he(n,"cluster")),console.log(` ${w(n,"\xB7")} ${v(n,`enabled \xB7 label ${o.clusterLabel||Gf.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(xu(Vn(o),n)),console.log(""),console.log(on(n)),console.log(v(n,"Keys editable from chat via /config set (same trust as shell):")),console.log(w(n,sn.join(", "))),console.log(`${w(n,"See also:")} ${v(n,W)}`);return}case"cluster":{let n=process.stdout,o=process.stderr,r=(t[0]??"status").toLowerCase();if(r==="status"){let s=S(),i=at();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:")} ${he(n,i)}`),s.clusterEnabled){let a=ke(),l=Ia(s,null);console.log(""),console.log(v(n,l.wa.replace(/\*([^*]+)\*/g,"$1").replace(/`([^`]+)`/g,"$1"))),console.log(""),console.log(Aa(a,s,null));let c=Object.keys(a.senderBindings).length,u=Object.entries(s.clusterSenderBindings??{});if(c>0){console.log(""),console.log(he(n,"Chat bindings (cluster-local.json)"));for(let[d,m]of Object.entries(a.senderBindings))console.log(` ${w(n,d)} ${X(n,"->")} ${v(n,`${m.nodeId} (${m.source}, since ${m.sinceIso})`)}`)}if(u.length>0){console.log(""),console.log(he(n,"Config defaults (clusterSenderBindings)"));for(let[d,m]of u)console.log(` ${w(n,d)} ${X(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(C(o,"Usage: omnish cluster use <senderE164|tg:id> <label-or-id>")),process.exitCode=1;return}let a=Ox(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}=$d(a,i);if(!c.ok){if(c.reason==="ambiguous-label"){let d=(c.matches??[]).map(m=>`${m.nodeId}(${m.label})`).join(", ");console.error(C(o,`Label "${i}" matches multiple machines: ${d}. 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(`${he(n,"cluster:")} ${v(n,`${a} -> ${c.peer.nodeId} (${c.peer.label}).`)}`);let u=S();console.log(""),console.log(Aa(l,u,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=_t(n),r=t.includes("--json");console.log(r?$u(o):ua(o,process.stdout)),Ho(o)&&(process.exitCode=1);return}case"service":{Lx(t);return}case"pull":{await up(t);return}case"media-exec":{await pp(t);return}case"pull-exec":{await dp(t);return}case"config":{await af(t);return}case"tunnel":{await Lm(t);return}case"platform":{await vf(t);return}case"docs":{Bf(t);return}case"ui":{let n=Fx(t);if(n.help){Yf();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=Tf(n.token);await Of({host:n.host,port:n.port,meta:o});let r=process.stdout,s=Nf(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:")} ${X(r,bt)}`),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:Cc(),e&&(process.exitCode=1)}}_x().catch(e=>{console.error(C(process.stderr,String(e))),process.exit(1)});
|